diff --git a/app/inc/woocommerce/cart.php b/app/inc/woocommerce/cart.php
index 9803d04..eab347a 100644
--- a/app/inc/woocommerce/cart.php
+++ b/app/inc/woocommerce/cart.php
@@ -26,9 +26,11 @@ function wc_get_cart_item_count()
}
}
-// Update the header cart badge count via WooCommerce's AJAX fragment system
+// Update the header cart badge via WooCommerce's AJAX fragment system. The
+// whole badge is replaced, not only its count: it starts hidden on an empty
+// cart, and a fragment carrying the count alone left it hidden after an add.
Filter::add('woocommerce_add_to_cart_fragments', function ($fragments) {
- $fragments['.cart-count'] = ''.wc_get_cart_item_count().'';
+ $fragments['.cart-badge'] = view('parts.header.cart-badge')->render();
return $fragments;
});
diff --git a/bin/tests/e2e/shop.spec.ts b/bin/tests/e2e/shop.spec.ts
index 4c09c23..58c0f2f 100644
--- a/bin/tests/e2e/shop.spec.ts
+++ b/bin/tests/e2e/shop.spec.ts
@@ -8,11 +8,15 @@ import { homeUrl, runId, wp } from '../../support/site';
*/
type Product = { id: number; name: string; permalink: string };
+type Category = { id: number; slug: string };
const simpleName = `E2E simple ${runId}`;
const variableName = `E2E variable ${runId}`;
const products: number[] = [];
+// A category holding only this run's products: its archive is a product grid the test controls.
+let category: Category;
+
// The cart and checkout slugs follow the site's language.
const cartUrl = wp('eval', 'echo wc_get_page_permalink("cart");');
const checkoutUrl = wp('eval', 'echo wc_get_page_permalink("checkout");');
@@ -42,10 +46,12 @@ function watchErrors(page: Page): string[] {
}
test.beforeAll(async ({ requestUtils }) => {
+ category = await requestUtils.rest({ method: 'POST', path: '/wc/v3/products/categories', data: { name: `E2E category ${runId}` } });
+
simple = await requestUtils.rest({
method: 'POST',
path: '/wc/v3/products',
- data: { name: simpleName, type: 'simple', regular_price: '12.00', status: 'publish' },
+ data: { name: simpleName, type: 'simple', regular_price: '12.00', status: 'publish', categories: [{ id: category.id }] },
});
products.push(simple.id);
@@ -74,6 +80,10 @@ test.afterAll(async ({ requestUtils }) => {
for (const id of products) {
await requestUtils.rest({ method: 'DELETE', path: `/wc/v3/products/${id}`, params: { force: true } });
}
+
+ if (category) {
+ await requestUtils.rest({ method: 'DELETE', path: `/wc/v3/products/categories/${category.id}`, params: { force: true } });
+ }
});
// A fresh visitor per test: the cart lives in the session cookie.
@@ -84,9 +94,14 @@ test('a simple product goes from its page to the cart and the checkout', async (
await page.goto(simple.permalink);
await expect(page.locator('h1.product_title')).toHaveText(simpleName);
+ await expect(page.locator('.cart-badge'), 'the header badge starts hidden on an empty cart').toBeHidden();
await page.locator('form.cart .single_add_to_cart_button').click();
await expectCartLines(page, 1);
+ // Without a reload: the badge comes back as a WooCommerce fragment.
+ await expect(page.locator('.cart-badge')).toBeVisible();
+ await expect(page.locator('.cart-badge .cart-count')).toHaveText('1');
+
// Scoped to the cart itself: an empty cart lists new products, this one among them.
await page.goto(cartUrl);
await expect(page.locator(cart).first()).toContainText(simpleName, { timeout: 15_000 });
@@ -98,6 +113,22 @@ test('a simple product goes from its page to the cart and the checkout', async (
expect(errors, 'no uncaught page error').toEqual([]);
});
+test('a product added from a product grid updates the header badge', async ({ page }) => {
+ const errors = watchErrors(page);
+
+ await page.goto(wp('eval', `echo get_term_link(${category.id}, "product_cat");`));
+ await expect(page.locator('.cart-badge'), 'the header badge starts hidden on an empty cart').toBeHidden();
+
+ await page.locator(`a.ajax_add_to_cart[data-product_id="${simple.id}"]`).click();
+ await expectCartLines(page, 1);
+
+ // Without a reload: WooCommerce applies the fragments after its Ajax add.
+ await expect(page.locator('.cart-badge')).toBeVisible();
+ await expect(page.locator('.cart-badge .cart-count')).toHaveText('1');
+
+ expect(errors, 'no uncaught page error').toEqual([]);
+});
+
test('a variation is chosen, priced and carried to the cart', async ({ page }) => {
const errors = watchErrors(page);
diff --git a/resources/assets/js/frontend/cart.js b/resources/assets/js/frontend/cart.js
index 104f5e0..526ff8a 100644
--- a/resources/assets/js/frontend/cart.js
+++ b/resources/assets/js/frontend/cart.js
@@ -220,25 +220,3 @@ if (typeof jQuery !== 'undefined' && document.querySelector('.woocommerce-cart-f
}, 600);
});
}
-
-// ─── Cart badge visibility ───
-const badge = document.querySelector('.cart-badge');
-const count = document.querySelector('.cart-count');
-
-if (badge && count) {
- const updateBadge = () => {
- const value = parseInt(count.textContent, 10) || 0;
- badge.classList.toggle('flex', value > 0);
- badge.classList.toggle('hidden', value === 0);
- };
-
- const badgeObserver = new MutationObserver(updateBadge);
- badgeObserver.observe(count, {
- childList: true,
- characterData: true,
- subtree: true,
- });
-
- // Cleanup on turbo/SPA navigation if applicable
- document.addEventListener('turbo:before-render', () => badgeObserver.disconnect(), { once: true });
-}
diff --git a/resources/views/parts/header/cart-badge.blade.php b/resources/views/parts/header/cart-badge.blade.php
new file mode 100644
index 0000000..9a43a3c
--- /dev/null
+++ b/resources/views/parts/header/cart-badge.blade.php
@@ -0,0 +1,10 @@
+{{--
+ * Header cart count
+ *
+ * Rendered with the page, and again as a WooCommerce fragment after every
+ * add to cart, so its visibility is decided here, in one place.
+ *
+ * @package %theme_namespace%
+ --}}
+@php($count = wc_get_cart_item_count())
+{{ $count }}
diff --git a/resources/views/parts/header/top-bar.blade.php b/resources/views/parts/header/top-bar.blade.php
index 045f58d..6006094 100644
--- a/resources/views/parts/header/top-bar.blade.php
+++ b/resources/views/parts/header/top-bar.blade.php
@@ -94,7 +94,7 @@ class="group relative p-2 text-muted hover:text-foreground transition-colors">
- {{ wc_get_cart_item_count() }}
+ @include('parts.header.cart-badge')