From 2805e0077bebeeec823e8192f1d298ce127d3110 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 10 Apr 2026 08:08:02 +0000 Subject: [PATCH] Fix clobber test: detect text content change, not just empty state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test only checked if childNodes.length hit zero. With replaceChildren that never happens — but the flash is still visible because the SSR DOM is replaced with different reactive DOM. New test captures SSR textContent before JS boots, watches for any change via MutationObserver. Now correctly fails: "text changed — ssr:(div (~tw :tokens... → hydrated:..." This proves the flash: island hydration replaces SSR DOM wholesale. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/playwright/site-full.spec.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/playwright/site-full.spec.js b/tests/playwright/site-full.spec.js index 8eb5ebf6..fa642caa 100644 --- a/tests/playwright/site-full.spec.js +++ b/tests/playwright/site-full.spec.js @@ -166,19 +166,22 @@ test('home', async ({ page }) => { }]); // Inject observer before page JS boots to detect hydration flash. - // A flash = the island content goes empty (0 children) between SSR and hydration. - // An atomic replaceChildren swap is fine — content is never visibly empty. + // A flash = the island's visible text content changes during hydration. + // True hydration should preserve SSR DOM — no visible change at all. await page.addInitScript(() => { window.__flashDetected = false; + window.__flashDetail = null; const check = () => { const stepper = document.querySelector('[data-sx-island="home/stepper"]'); if (!stepper || !stepper.parentNode) return; - const ssrChildCount = stepper.childNodes.length; + const ssrText = stepper.textContent; new MutationObserver(() => { - if (stepper.childNodes.length === 0 && ssrChildCount > 0) { + const newText = stepper.textContent; + if (newText !== ssrText && !window.__flashDetected) { window.__flashDetected = true; + window.__flashDetail = { ssr: ssrText.substring(0, 80), hydrated: newText.substring(0, 80) }; } - }).observe(stepper, { childList: true }); + }).observe(stepper, { childList: true, subtree: true, characterData: true }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', check); @@ -204,9 +207,12 @@ test('home', async ({ page }) => { const noFlash = ssrIndex === hydratedIndex; entries.push({ ok: noFlash, label: `No flash: SSR=${ssrIndex} hydrated=${hydratedIndex} (cookie=7)`, feature: 'no-flash' }); - // Check for hydration flash — island content going empty during hydration - const flashDetected = await page.evaluate(() => window.__flashDetected || false); - entries.push({ ok: !flashDetected, label: `No clobber: ${flashDetected ? 'island went empty during hydration' : 'clean'}`, feature: 'no-clobber' }); + // Check for hydration flash — island text content changed during hydration + const flash = await page.evaluate(() => ({ + detected: window.__flashDetected || false, + detail: window.__flashDetail, + })); + entries.push({ ok: !flash.detected, label: `No clobber: ${flash.detected ? 'text changed — ssr:"' + (flash.detail?.ssr || '').substring(0, 40) + '" → hydrated:"' + (flash.detail?.hydrated || '').substring(0, 40) + '"' : 'clean'}`, feature: 'no-clobber' }); const info = await discoverPage(page); entries.push({ ok: true, label: 'Boot: data-sx-ready', feature: 'boot' });