From 191caf5a583d48707eebfcfcded7c60e7d6c4e51 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Fri, 25 Sep 2026 17:08:15 +0200 Subject: [PATCH 1/2] fix: show the header cart badge after an add to cart The badge renders hidden on an empty cart. After an add, WooCommerce's fragment replaced the .cart-count element, and the MutationObserver meant to reveal the badge watched the element that had just been removed: the count read 1, the badge kept its hidden class. Measured in a browser on an empty cart: count "1", class "hidden", not displayed. The badge lives in a Blade partial that decides its own visibility, included by the header and returned whole as the fragment. The observer goes. The shop spec checks the badge after an add, without a reload. --- app/inc/woocommerce/cart.php | 6 +++-- bin/tests/e2e/shop.spec.ts | 5 +++++ resources/assets/js/frontend/cart.js | 22 ------------------- .../views/parts/header/cart-badge.blade.php | 10 +++++++++ .../views/parts/header/top-bar.blade.php | 2 +- 5 files changed, 20 insertions(+), 25 deletions(-) create mode 100644 resources/views/parts/header/cart-badge.blade.php 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..e06846c 100644 --- a/bin/tests/e2e/shop.spec.ts +++ b/bin/tests/e2e/shop.spec.ts @@ -84,9 +84,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 }); 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') From 1f898876329d1811f49ef6ac05f0433a77e57190 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Fri, 25 Sep 2026 17:16:53 +0200 Subject: [PATCH 2/2] test: add to the cart from a product grid and watch the header badge The archive add-to-cart goes through WooCommerce's own Ajax handler, not the single product form, and it is where the badge defect was seen first. The spec creates a category holding only its products, adds one from that grid and expects the badge shown with 1, without a reload. Replayed on a site: without the previous commit's fix, the badge stays hidden and the test fails. --- bin/tests/e2e/shop.spec.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/bin/tests/e2e/shop.spec.ts b/bin/tests/e2e/shop.spec.ts index e06846c..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. @@ -103,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);