From 22bea0edba3e8220c72bb21b154eeaca456c667d Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Mon, 3 Aug 2026 18:54:14 +0530 Subject: [PATCH 1/2] fix: validate WC cart object to prevent fatal error --- .../Core/Components/CartIcon.php | 4 + inc/views/header.php | 4 + tests/stubs/woocommerce-cart.php | 133 ++++++++++++++++ tests/test-neve-cart-guards.php | 146 ++++++++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 tests/stubs/woocommerce-cart.php create mode 100644 tests/test-neve-cart-guards.php diff --git a/header-footer-grid/Core/Components/CartIcon.php b/header-footer-grid/Core/Components/CartIcon.php index 2c55e35dec..eed9f9c95a 100644 --- a/header-footer-grid/Core/Components/CartIcon.php +++ b/header-footer-grid/Core/Components/CartIcon.php @@ -277,6 +277,10 @@ public function add_style( array $css_array = array() ) { * @access public */ public function render_component() { + if ( ! class_exists( 'WooCommerce', false ) || ! WC()->cart instanceof \WC_Cart ) { // @phpstan-ignore-line + return; + } + Main::get_instance()->load( 'components/component-cart-icon' ); } diff --git a/inc/views/header.php b/inc/views/header.php index 0dcc7a7a42..f77fa03728 100644 --- a/inc/views/header.php +++ b/inc/views/header.php @@ -166,6 +166,10 @@ private function get_nav_menu_cart( $responsive = false ) { return ''; } + if ( ! isset( WC()->cart ) || ! WC()->cart instanceof \WC_Cart ) { // @phpstan-ignore-line + return ''; + } + $tag = 'li'; $class = 'menu-item-nav-cart'; if ( $responsive === true ) { diff --git a/tests/stubs/woocommerce-cart.php b/tests/stubs/woocommerce-cart.php new file mode 100644 index 0000000000..82b6b1d9a6 --- /dev/null +++ b/tests/stubs/woocommerce-cart.php @@ -0,0 +1,133 @@ +count = $count; + } + + /** + * Cart contents count. + * + * @return int + */ + public function get_cart_contents_count() { + return $this->count; + } + } +} + +if ( ! class_exists( 'WC_Widget_Cart', false ) ) { + /** + * Stand-in for the mini cart widget rendered inside the cart icon component. + */ + class WC_Widget_Cart extends WP_Widget { + /** + * Constructor. + */ + public function __construct() { + parent::__construct( 'neve_tests_wc_widget_cart', 'Cart', array( 'classname' => 'widget_shopping_cart' ) ); + } + + /** + * Output the widget. + * + * @param array $args widget arguments. + * @param array $instance widget instance settings. + */ + public function widget( $args, $instance ) { + echo '
'; + } + } +} + +if ( ! function_exists( 'WC' ) ) { + /** + * Return the stubbed WooCommerce instance. + * + * @return \WooCommerce + */ + function WC() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid + static $instance = null; + + if ( $instance === null ) { + $instance = new WooCommerce(); + } + + return $instance; + } +} + +if ( ! function_exists( 'wc_get_cart_url' ) ) { + /** + * Cart permalink stub. + * + * @return string + */ + function wc_get_cart_url() { + return home_url( '/cart/' ); + } +} + +if ( ! function_exists( 'is_cart' ) ) { + /** + * The cart page is never the current request in these tests. + * + * @return bool + */ + function is_cart() { + return false; + } +} + +if ( ! function_exists( 'is_checkout' ) ) { + /** + * The checkout page is never the current request in these tests. + * + * @return bool + */ + function is_checkout() { + return false; + } +} diff --git a/tests/test-neve-cart-guards.php b/tests/test-neve-cart-guards.php new file mode 100644 index 0000000000..d4ff9557fe --- /dev/null +++ b/tests/test-neve-cart-guards.php @@ -0,0 +1,146 @@ +setAccessible( true ); + + return $method->invoke( $header, $responsive ); + } + + /** + * Render the builder cart icon component. + * + * @return string + */ + private function render_cart_icon_component() { + $reflection = new ReflectionClass( \HFG\Core\Components\CartIcon::class ); + $component = $reflection->newInstanceWithoutConstructor(); + + ob_start(); + $component->render_component(); + + return (string) ob_get_clean(); + } + + /** + * Load the WooCommerce stubs. + */ + private function require_wc_stubs() { + require_once __DIR__ . '/stubs/woocommerce-cart.php'; + } + + /** + * Skip when the cart state cannot be simulated. + */ + private function skip_unless_cart_is_stubbable() { + if ( defined( 'NEVE_TESTS_WC_CART_STUB' ) ) { + return; + } + + if ( class_exists( 'WooCommerce', false ) || function_exists( 'WC' ) ) { + $this->markTestSkipped( 'A real WooCommerce instance is loaded; the cart state cannot be stubbed.' ); + } + } + + /** + * Skip when WooCommerce is present, stub included. + */ + private function skip_unless_woocommerce_is_absent() { + if ( class_exists( 'WooCommerce', false ) ) { + $this->markTestSkipped( 'WooCommerce is loaded in this environment.' ); + } + } + + /** + * Nothing is rendered when WooCommerce is not active at all. + */ + public function test_nav_menu_cart_is_empty_without_woocommerce() { + $this->skip_unless_woocommerce_is_absent(); + + $this->assertSame( '', $this->render_nav_menu_cart() ); + $this->assertSame( '', $this->render_nav_menu_cart( true ) ); + } + + /** + * The builder cart icon renders nothing when WooCommerce is not active at all. + */ + public function test_cart_icon_component_is_empty_without_woocommerce() { + $this->skip_unless_woocommerce_is_absent(); + + $this->assertSame( '', $this->render_cart_icon_component() ); + } + + /** + * Nothing is rendered when the WooCommerce cart object is not available. + */ + public function test_nav_menu_cart_is_empty_when_cart_object_is_missing() { + $this->skip_unless_cart_is_stubbable(); + $this->require_wc_stubs(); + + $this->assertNull( WC()->cart, 'The stub must expose a null cart to reproduce the crash.' ); + $this->assertSame( '', $this->render_nav_menu_cart() ); + $this->assertSame( '', $this->render_nav_menu_cart( true ) ); + } + + /** + * The builder cart icon renders nothing when the WooCommerce cart object is + * not available. + */ + public function test_cart_icon_component_is_empty_when_cart_object_is_missing() { + $this->skip_unless_cart_is_stubbable(); + $this->require_wc_stubs(); + + $this->assertNull( WC()->cart, 'The stub must expose a null cart to reproduce the crash.' ); + $this->assertSame( '', $this->render_cart_icon_component() ); + } + + /** + * The builder cart icon renders the cart markup when WooCommerce is active + * and the cart holds items. + */ + public function test_cart_icon_component_is_not_empty_when_cart_has_items() { + $this->skip_unless_cart_is_stubbable(); + $this->require_wc_stubs(); + + $previous_cart = WC()->cart; + WC()->cart = new WC_Cart( 3 ); + register_widget( 'WC_Widget_Cart' ); + + try { + $output = $this->render_cart_icon_component(); + } finally { + WC()->cart = $previous_cart; + unregister_widget( 'WC_Widget_Cart' ); + } + + $this->assertNotSame( '', $output ); + $this->assertStringContainsString( 'menu-item-nav-cart', $output ); + $this->assertStringContainsString( 'cart-icon-wrapper', $output ); + $this->assertMatchesRegularExpression( '/class="cart-count">\s*3\s*assertStringNotContainsString( 'cart-is-empty', $output ); + } +} From 52a9b64f75bfe2928a1e83b99a86e6eea87439d9 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Tue, 4 Aug 2026 10:51:57 +0530 Subject: [PATCH 2/2] fix: enhance cart magic tags to handle missing WC cart object --- header-footer-grid/Core/Magic_Tags.php | 10 ++++++ tests/stubs/woocommerce-cart.php | 35 +++++++++++++++++--- tests/test-neve-cart-guards.php | 46 ++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/header-footer-grid/Core/Magic_Tags.php b/header-footer-grid/Core/Magic_Tags.php index b445380d8b..8d53a87216 100644 --- a/header-footer-grid/Core/Magic_Tags.php +++ b/header-footer-grid/Core/Magic_Tags.php @@ -510,6 +510,11 @@ public function cart_total() { if ( ! class_exists( 'WooCommerce' ) ) { return ''; } + + if ( ! WC()->cart instanceof \WC_Cart ) { // @phpstan-ignore-line + return ''; + } + return '' . WC()->cart->get_cart_contents_total() . ''; } @@ -531,6 +536,11 @@ public function cart_total_currency_symbol() { if ( ! class_exists( 'WooCommerce' ) ) { return ''; } + + if ( ! WC()->cart instanceof \WC_Cart ) { // @phpstan-ignore-line + return ''; + } + return '' . WC()->cart->get_cart_total() . ''; } diff --git a/tests/stubs/woocommerce-cart.php b/tests/stubs/woocommerce-cart.php index 82b6b1d9a6..7a9e08a942 100644 --- a/tests/stubs/woocommerce-cart.php +++ b/tests/stubs/woocommerce-cart.php @@ -28,7 +28,7 @@ class WooCommerce { if ( ! class_exists( 'WC_Cart', false ) ) { /** - * Stand-in for the cart, carrying just the contents count. + * Stand-in for the cart, carrying just the contents count and totals. */ class WC_Cart { /** @@ -38,13 +38,22 @@ class WC_Cart { */ private $count; + /** + * Cart total, unformatted. + * + * @var string + */ + private $total; + /** * Constructor. * - * @param int $count number of items in the cart. + * @param int $count number of items in the cart. + * @param string $total cart total, unformatted. */ - public function __construct( $count = 0 ) { + public function __construct( $count = 0, $total = '0' ) { $this->count = $count; + $this->total = $total; } /** @@ -55,6 +64,24 @@ public function __construct( $count = 0 ) { public function get_cart_contents_count() { return $this->count; } + + /** + * Cart contents total, unformatted. + * + * @return string + */ + public function get_cart_contents_total() { + return $this->total; + } + + /** + * Cart total, formatted with the currency symbol. + * + * @return string + */ + public function get_cart_total() { + return '$' . $this->total; + } } } @@ -88,7 +115,7 @@ public function widget( $args, $instance ) { * * @return \WooCommerce */ - function WC() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid + function WC() { static $instance = null; if ( $instance === null ) { diff --git a/tests/test-neve-cart-guards.php b/tests/test-neve-cart-guards.php index d4ff9557fe..fd52b5f32c 100644 --- a/tests/test-neve-cart-guards.php +++ b/tests/test-neve-cart-guards.php @@ -1,7 +1,7 @@ assertMatchesRegularExpression( '/class="cart-count">\s*3\s*assertStringNotContainsString( 'cart-is-empty', $output ); } + + /** + * The cart total magic tags resolve to nothing when the WooCommerce cart + * object is not available. + */ + public function test_cart_magic_tags_are_empty_when_cart_object_is_missing() { + $this->skip_unless_cart_is_stubbable(); + $this->require_wc_stubs(); + + $magic_tags = \HFG\Core\Magic_Tags::get_instance(); + + $this->assertNull( WC()->cart, 'The stub must expose a null cart to reproduce the crash.' ); + $this->assertSame( '', $magic_tags->cart_total() ); + $this->assertSame( '', $magic_tags->cart_total_currency_symbol() ); + $this->assertSame( '', $magic_tags->do_magic_tags( '{cart_total}' ) ); + $this->assertSame( '', $magic_tags->do_magic_tags( '{cart_total_currency_symbol}' ) ); + } + + /** + * The cart total magic tags resolve to the totals when WooCommerce is active + * and the cart holds items. + */ + public function test_cart_magic_tags_render_when_cart_has_items() { + $this->skip_unless_cart_is_stubbable(); + $this->require_wc_stubs(); + + $magic_tags = \HFG\Core\Magic_Tags::get_instance(); + $previous_cart = WC()->cart; + WC()->cart = new WC_Cart( 3, '42' ); + + try { + $total = $magic_tags->cart_total(); + $total_currency = $magic_tags->cart_total_currency_symbol(); + $parsed_total = $magic_tags->do_magic_tags( '{cart_total}' ); + } finally { + WC()->cart = $previous_cart; + } + + $this->assertSame( '42', $total ); + $this->assertSame( '$42', $total_currency ); + $this->assertStringContainsString( '42', $parsed_total ); + } }