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/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 ''; } @@ -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 ''; } 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..7a9e08a942 --- /dev/null +++ b/tests/stubs/woocommerce-cart.php @@ -0,0 +1,160 @@ +count = $count; + $this->total = $total; + } + + /** + * Cart contents count. + * + * @return int + */ + 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; + } + } +} + +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() { + 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..fd52b5f32c --- /dev/null +++ b/tests/test-neve-cart-guards.php @@ -0,0 +1,188 @@ +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*', $output, 'The cart contents count is rendered.' ); + $this->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( '', $total ); + $this->assertSame( '', $total_currency ); + $this->assertStringContainsString( '42', $parsed_total ); + } +}