Skip to content
Open

Release #4560

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions header-footer-grid/Core/Components/CartIcon.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
}

Expand Down
10 changes: 10 additions & 0 deletions header-footer-grid/Core/Magic_Tags.php
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,11 @@ public function cart_total() {
if ( ! class_exists( 'WooCommerce' ) ) {
return '';
}

if ( ! WC()->cart instanceof \WC_Cart ) { // @phpstan-ignore-line
return '';
}

return '<span class="nv-cart-icon-total-plain">' . WC()->cart->get_cart_contents_total() . '</span>';
}

Expand All @@ -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 '<span class="nv-cart-icon-total-currency">' . WC()->cart->get_cart_total() . '</span>';
}

Expand Down
4 changes: 4 additions & 0 deletions inc/views/header.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand Down
160 changes: 160 additions & 0 deletions tests/stubs/woocommerce-cart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php
/**
* Minimal WooCommerce stubs for the cart markup tests.
*
* `WooCommerce::$cart` starts out null, reproducing the "plugin loaded, cart
* missing" state; tests that need a usable cart assign a `WC_Cart` themselves.
*
* @package neve
*/

if ( ! defined( 'NEVE_TESTS_WC_CART_STUB' ) ) {
define( 'NEVE_TESTS_WC_CART_STUB', true );
}

if ( ! class_exists( 'WooCommerce', false ) ) {
/**
* Stand-in for the WooCommerce main class, with no cart object.
*/
class WooCommerce {
/**
* The cart instance, null until a test assigns one.
*
* @var \WC_Cart|null
*/
public $cart = null;
}
}

if ( ! class_exists( 'WC_Cart', false ) ) {
/**
* Stand-in for the cart, carrying just the contents count and totals.
*/
class WC_Cart {
/**
* Number of items in the cart.
*
* @var int
*/
private $count;

/**
* Cart total, unformatted.
*
* @var string
*/
private $total;

/**
* Constructor.
*
* @param int $count number of items in the cart.
* @param string $total cart total, unformatted.
*/
public function __construct( $count = 0, $total = '0' ) {
$this->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 '<div class="widget_shopping_cart_content"></div>';
}
}
}

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;
}
}
188 changes: 188 additions & 0 deletions tests/test-neve-cart-guards.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php
/**
* Tests for the cart markup guards: the legacy primary-navigation cart item, the
* header/footer builder cart icon component and the cart total magic tags.
*
* Both entry points live in one test case on purpose. The null-cart scenario has
* to declare a global `WooCommerce` class, which cannot be undeclared afterwards,
* so the tests that need WooCommerce absent must be guaranteed to run first -
* something only method order inside a single class gives us.
*
* @package neve
*/

/**
* Class TestNeveCartGuards
*/
class TestNeveCartGuards extends WP_UnitTestCase {

/**
* Render the nav menu cart markup.
*
* @param bool $responsive whether to render the responsive variant.
*
* @return string
*/
private function render_nav_menu_cart( $responsive = false ) {
$header = new \Neve\Views\Header();
$method = new ReflectionMethod( $header, 'get_nav_menu_cart' );
$method->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( '<span class="nv-cart-icon-total-plain">42</span>', $total );
$this->assertSame( '<span class="nv-cart-icon-total-currency">$42</span>', $total_currency );
$this->assertStringContainsString( '42', $parsed_total );
}
}
Loading