zudo-doc
GitHub repository

Type to search...

to open search from anywhere

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/zudo-doc/html-preview-wrapper 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/smoke-html-preview.spec.ts (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://localhost:8899/docs/components/html-preview/). Use viewport 1280 x 900. Use /headless-browser (Playwright CLI / a small Playwright script) — /verify-ui single-page computed-style reads are not enough because this needs scroll-to-hydrate plus per-block DOM-tree walks.

  1. Launch chromium (JS enabled), viewport 1280x900, goto previewUrl, waitUntil: networkidle.

  2. 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.

  3. For EVERY [data-zfb-island="HtmlPreviewWrapperInner"] element on the page, locate its outer container (:scope > div) and measure (see Measurements). Count the islands with document.querySelectorAll('[data-zfb-island="HtmlPreviewWrapperInner"]').length — do NOT hardcode a fixed number, the count changes as the fixture page grows.

  4. 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 of C.

  • 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, and child1.rect.y (top).

  • titleBarBottom = child0.rect.y + child0.rect.height.

Verdict criteria (mechanical first, visual second)

A block PASSES when ALL of:

  1. childCount === 3 (the three siblings are NOT collapsed/re-parented).

  2. child0.display === "flex" AND child0.height <= 80 (the title bar is just a bar — in the bug it absorbs the whole component and is 200px+ tall).

  3. child1.display === "block" AND child1.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 blocksTotal is 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.

Revision History

CreatedUpdated

AI Assistant

Ask a question about the documentation.

Preview theme

Loading theme previews…