Fix clobber test: detect text content change, not just empty state

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 08:08:02 +00:00
parent 737964be89
commit 2805e0077b

View File

@@ -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' });