From 78bbaa95a37af4ef59ea92389d291ee5c5dc3e3b Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Fri, 25 Sep 2026 17:22:44 +0200 Subject: [PATCH] fix: keep what Alpine hides hidden until Alpine starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elements hidden with x-show were shown until Alpine initialised them, so the cart drawer, the mobile menu, the add-to-cart modal and the product accordion flashed on every page load. Seven elements already carried x-cloak, but no stylesheet rule hid [x-cloak], so it did nothing; the others had none. Measured with the theme's scripts held back: 10 elements Alpine hides were on screen on the home page, 11 on a product page. The stylesheet hides [x-cloak], and every x-show element carries it — but the billing address card, which is shown from the start when the address is prefilled, as the fields next to it already did. The dead first-hidden-state class, which no rule ever styled, goes. A browser spec holds the theme's scripts back and expects nothing Alpine hides on screen, on the home page and on a product page. Measured on a site with the rule injected: none before Alpine starts; once it has, no x-cloak is left and the accordion still opens. --- bin/tests/e2e/alpine.spec.ts | 47 +++++++++++++++++++ resources/assets/css/app.css | 6 +++ .../views/parts/header/cart-drawer.blade.php | 2 +- .../views/parts/header/mobile-nav.blade.php | 2 +- .../views/parts/header/overlay.blade.php | 2 +- .../parts/header/search-overlay.blade.php | 4 +- .../parts/header/search-results.blade.php | 10 ++-- .../views/parts/header/top-bar.blade.php | 2 +- .../views/parts/toast-container.blade.php | 2 +- .../checkout/form-billing.blade.php | 2 +- .../woocommerce/global/form-login.blade.php | 6 +-- .../parts/modal-add-to-cart.blade.php | 12 ++--- .../single-product/accordion.blade.php | 2 +- .../single-product/sticky-bar.blade.php | 4 +- .../single-product/tabs/tabs.blade.php | 2 +- 15 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 bin/tests/e2e/alpine.spec.ts diff --git a/bin/tests/e2e/alpine.spec.ts b/bin/tests/e2e/alpine.spec.ts new file mode 100644 index 0000000..a751f4a --- /dev/null +++ b/bin/tests/e2e/alpine.spec.ts @@ -0,0 +1,47 @@ +import { expect, test } from '@wordpress/e2e-test-utils-playwright'; +import type { Page } from '@playwright/test'; +import { homeUrl, runId } from '../../support/site'; + +/** + * What a visitor sees before Alpine starts. With the theme's scripts held back, as they + * are for a moment on every load, nothing Alpine hides with x-show may be on screen: + * every such element carries x-cloak, which the theme's stylesheet hides until Alpine + * removes it. Without that, the cart drawer, the mobile menu, the add-to-cart modal and + * the product accordion flashed on each page load. + */ + +type Product = { id: number; permalink: string }; + +let product: Product; + +test.beforeAll(async ({ requestUtils }) => { + product = await requestUtils.rest({ + method: 'POST', + path: '/wc/v3/products', + data: { name: `E2E cloak ${runId}`, type: 'simple', regular_price: '5.00', status: 'publish', description: 'Described.' }, + }); +}); + +test.afterAll(async ({ requestUtils }) => { + await requestUtils.rest({ method: 'DELETE', path: `/wc/v3/products/${product.id}`, params: { force: true } }); +}); + +test.use({ storageState: { cookies: [], origins: [] } }); + +async function visibleBeforeAlpine(page: Page, url: string): Promise { + // The theme's built scripts, Alpine among them; its stylesheet still loads. + await page.route(/\/build\/theme\/[^/]+\/assets\/.+\.js(\?.*)?$/, (route) => route.abort()); + await page.goto(url); + + return page.evaluate(() => [...document.querySelectorAll('[x-show]')] + .filter((element) => element.checkVisibility()) + .map((element) => `<${element.tagName.toLowerCase()} x-show="${element.getAttribute('x-show')}">`)); +} + +test('the home page shows nothing Alpine hides, before Alpine starts', async ({ page }) => { + expect(await visibleBeforeAlpine(page, homeUrl('/'))).toEqual([]); +}); + +test('a product page shows nothing Alpine hides, before Alpine starts', async ({ page }) => { + expect(await visibleBeforeAlpine(page, product.permalink)).toEqual([]); +}); diff --git a/resources/assets/css/app.css b/resources/assets/css/app.css index 26d6584..597a9f7 100644 --- a/resources/assets/css/app.css +++ b/resources/assets/css/app.css @@ -16,6 +16,12 @@ @import './frontend/components/wc-blocks.css'; @import './frontend/layout/single-product.css'; +/* Alpine removes x-cloak once it has initialised an element. Until then, + whatever it hides with x-show stays hidden instead of flashing on load. */ +[x-cloak] { + display: none !important; +} + @theme { /* Primary action color — buttons, links, active states */ --color-primary: var(--wp--preset--color--primary, #1f2937); diff --git a/resources/views/parts/header/cart-drawer.blade.php b/resources/views/parts/header/cart-drawer.blade.php index ee708fd..2cacb4f 100644 --- a/resources/views/parts/header/cart-drawer.blade.php +++ b/resources/views/parts/header/cart-drawer.blade.php @@ -4,7 +4,7 @@ x-on:panel-close.window="open = false" @keydown.window.escape="if (open) { window.dispatchEvent(new CustomEvent('panel-close')); }" class="fixed inset-y-0 right-0 pl-4 sm:pl-10 max-w-full flex z-40"> -