# 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).

**Historical regression:** the old Preact port resolved an inner island marker to a
self-wrapping component, nesting the preview inside the flex title bar. The current
zudo-react runtime requires one static client target per Island and unique client

do not copy the former `hydrate()` recipe into current code.

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.