test-flow-html-preview-hydration
AI-judged verification that the HTML Preview component (`data-zfb-island="HtmlPreviewWrapperInner"`) hydrates into the correct vertical-stack layout — title bar on top, preview iframe below, code togg...
Test flow: html-preview island hydration layout
Background
@takazudo/ renders the interactive HTML Preview component used by the <HtmlPreview> MDX tag. It is a zfb island hydrated with when="visible".
Each preview block has this SSR structure:
<div data-zfb-island="HtmlPreviewWrapperInner" ...> ← island marker
<div class="border ... rounded-lg overflow-hidden"> ← OUTER CONTAINER
<div class="flex items-center justify-between ..."> ← TITLE BAR (flex row)
<span>{title}</span>
<div class="flex">{Mobile/Tablet/Full buttons}</div>
</div>
<div class="bg-surface p-hsp-lg">…<iframe>…</div> ← PREVIEW AREA (block)
<div class="border-t ...">…Show code…</div> ← CODE SECTION (block)
</div>
</div> The outer container's three children are block <div>s that MUST stack vertically (title bar on top, preview below, code beneath).
The bug under verification (Astro→zfb migration regression): the bare inner component carried the outer wrapper's name as its displayName ("HtmlPreviewWrapper") and was not exported, so the SSG marker resolved to the exported self-wrapping HtmlPreviewWrapper. On the client hydrate(<HtmlPreviewWrapper/>, markerDiv) re-emitted another data-zfb-island wrapper and Preact reused the SSR'd children one level off — re-parenting the preview + code sections INSIDE the flex title bar. Result: the title bar + buttons squished on the LEFT, preview iframe floating on the RIGHT (a broken side-by-side / flex-row layout). The fix gives the bare inner component its OWN name+marker (HtmlPreviewWrapperInner) and exports it, while HtmlPreviewWrapper stays the <Island> wrapper that MDX registers — so the bundle hydrates the bare component in-place.
The raw SSR DOM (JS disabled) was ALWAYS correct — the breakage appears only AFTER hydration. So the test MUST run with JS enabled and MUST scroll the page so the when="visible" islands actually hydrate before measuring.
Companion spec
e2e/ (added #2527, "HtmlPreview: post-hydration structure") transcribes this same per-block childCount/display/y check into a deterministic Playwright assertion, iterating every [data-zfb-island="HtmlPreviewWrapperInner"] found on the page (counted dynamically, not hardcoded) and clicking each "Show code" toggle to force hydration before measuring. Prefer that spec for everyday regression coverage — this L6 skill is the escalation path for cases the deterministic spec can't catch (genuine visual side-by-side mis-render, novel layouts), not the primary regression gate.
Scenario
Run against the URL in Inputs.previewUrl (default http:). Use viewport 1280 x 900. Use / (Playwright CLI / a small Playwright script) — / single-page computed-style reads are not enough because this needs scroll-to-hydrate plus per-block DOM-tree walks.
Launch chromium (JS enabled), viewport 1280x900, goto previewUrl,
waitUntil: networkidle.Scroll the full page top→bottom in ~400px steps (≈60ms each), then back to top, and wait ~1.2s. This triggers the IntersectionObserver that hydrates every
when="visible"island.For EVERY
[data-zfb-island="HtmlPreviewWrapperInner"]element on the page, locate its outer container (:scope > div) and measure (see Measurements). Count the islands withdocument.querySelectorAll('[data-zfb-island="HtmlPreviewWrapperInner"]').length— do NOT hardcode a fixed number, the count changes as the fixture page grows.Capture a full-page screenshot AND a cropped screenshot of the FIRST preview block (
element.screenshot()on the island).
Measurements (per preview block, post-hydration)
For each island's outer container C:
childCount= number of element children ofC.child0=C.children[0](expected: the title bar).child0.display=getComputedStyle(child0).display.child0.height=child0.getBoundingClientRect().height.child1=C.children[1](expected: the preview area).child1.display, andchild1.rect.y(top).titleBarBottom=child0.rect.y + child0.rect.height.
Verdict criteria (mechanical first, visual second)
A block PASSES when ALL of:
childCount === 3(the three siblings are NOT collapsed/re-parented).child0.display === "flex"ANDchild0.height <= 80(the title bar is just a bar — in the bug it absorbs the whole component and is 200px+ tall).child1.display === "block"ANDchild1.rect.y >= titleBarBottom - 2(preview area sits BELOW the title bar — vertical stack, not beside it).
The overall run PASSES only when every preview block passes (blocksPassed === blocksTotal, whatever blocksTotal actually is on the page — do not assume a fixed count) AND the visual screenshot check confirms: each block shows a horizontal title bar on top (title left, Mobile/Tablet/Full pills right, "Full" highlighted) with the preview area full-width below it — and NONE show the title bar/buttons squished to the left with the iframe on the right.
FAIL if any block has childCount !== 3, a tall title bar, a side-by-side arrangement, or the screenshot shows the broken left/right split.
Output schema
Return a structured result with exactly these fields:
{
blocksTotal: number, // islands actually found on the page —
// count them, do not hardcode a number
blocksPassed: number, // blocks meeting all 3 mechanical criteria
perBlock: [ // one entry per island
{ index, childCount, child0Display, child0Height, child1Display, child1Y, titleBarBottom, pass }
],
mechanicalVerdict: "PASS" | "FAIL",
visualVerdict: "PASS" | "FAIL",
verdict: "PASS" | "FAIL", // PASS only if BOTH mechanical and visual PASS
summary: string, // one-line human verdict
fullPageScreenshot: string, // path
firstBlockScreenshot: string, // path
toolUsed: "headless-browser"
}Notes
If
blocksTotalis 0, the page didn't load or the marker changed — report FAIL with that detail, do not silently pass.Do not measure with JS disabled — the SSR DOM is always correct and would produce a false PASS that misses the hydration regression.