Skip to content
Merged
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
6 changes: 4 additions & 2 deletions app/inc/woocommerce/cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] = '<span class="cart-count">'.wc_get_cart_item_count().'</span>';
$fragments['.cart-badge'] = view('parts.header.cart-badge')->render();

return $fragments;
});
Expand Down
33 changes: 32 additions & 1 deletion bin/tests/e2e/shop.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");');
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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.
Expand All @@ -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 });
Expand All @@ -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);

Expand Down
22 changes: 0 additions & 22 deletions resources/assets/js/frontend/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
10 changes: 10 additions & 0 deletions resources/views/parts/header/cart-badge.blade.php
Original file line number Diff line number Diff line change
@@ -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())
<span class="cart-badge absolute -top-0.5 -right-0.5 items-center justify-center size-[18px] text-[10px] font-bold text-white bg-primary rounded-full {{ $count > 0 ? 'flex' : 'hidden' }}"><span class="cart-count">{{ $count }}</span></span>
2 changes: 1 addition & 1 deletion resources/views/parts/header/top-bar.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class="group relative p-2 text-muted hover:text-foreground transition-colors">
<svg class="w-5.5 h-5.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"/>
</svg>
<span class="cart-badge absolute -top-0.5 -right-0.5 items-center justify-center size-[18px] text-[10px] font-bold text-white bg-primary rounded-full {{ wc_get_cart_item_count() > 0 ? 'flex' : 'hidden' }}"><span class="cart-count">{{ wc_get_cart_item_count() }}</span></span>
@include('parts.header.cart-badge')
</button>
</div>
</div>
Expand Down
Loading