test-flow-sidebar-width-restore
AI-judged verification that the docs-page sidebar's persisted drag-width is restored on page reload before first paint, with no visible flash to the CSS-default width. Use when /verify-ui-ai dispatche...
Test flow: sidebar-resizer width restore on reload
Background
@takazudo/ writes the user-chosen sidebar width
to localStorage["zudo-doc-sidebar-width"]. A sibling
SidebarResizerRestore component emits a pre-paint inline <script> in
<head> that reads the persisted value, validates and clamps it to
[192, 448], and sets --zd-sidebar-w on :root.style before first
paint. The docs layout consumes --zd-sidebar-w via the sidebar's
w-[var(--zd-sidebar-w)] and the content wrapper's
ml-[var(--zd-sidebar-w)] classes.
The bug under verification: prior to the fix, reload after a drag
showed the CSS-default width (clamp(14rem, 20vw, 22rem)) instead of
the persisted value — the value existed in localStorage but nothing
applied it.
Scenario
All steps run against the local dev server at the URL passed in
Inputs.previewUrl (default
http:). The page must include the
desktop sidebar (it does on / routes at viewport widths
≥ Tailwind lg ≥ 1024px). Use viewport 1400 x 900 (or the size in
Inputs.viewport).
For each scenario below, capture a screenshot AND measure the live DOM right after reload settles.
Scenario A — baseline (no persisted value)
Open
previewUrlat the configured viewport.Clear:
localStorage.removeItem("zudo-doc-sidebar-width").Reload the page; wait for
domcontentloadedplus a 200 ms settle.Capture screenshot to
baselineScreenshot.Measure:
inline = document.documentElement.style.getPropertyValue("--zd-sidebar-w")computed = getComputedStyle(document.documentElement).getPropertyValue("--zd-sidebar-w").trim()rectWidth = document.getElementById("desktop-sidebar").getBoundingClientRect().width
Scenario B — restored to 400 px
Set:
localStorage.setItem("zudo-doc-sidebar-width", "400").Reload; wait as above.
Capture screenshot to
restored400Screenshot.Measure the same three fields as Scenario A; record into the
restored400block of the output.
Scenario C — out-of-range clamp (99999)
Set:
localStorage.setItem("zudo-doc-sidebar-width", "99999").Reload; wait as above.
Capture screenshot to
clamp448Screenshot.Measure the same three fields; record into
clamp448.
Scenario D — garbage value falls through (NaN-garbage)
Set:
localStorage.setItem("zudo-doc-sidebar-width", "NaN-garbage").Reload; wait as above.
Capture screenshot to
garbageFallbackScreenshot.Measure the same three fields; record into
garbageFallback.
After all scenarios, restore a clean state with
localStorage.removeItem("zudo-doc-sidebar-width") and close the
browser session.
Verdict criteria
All four scenarios must pass. Each scenario is a mechanical-plus-visual check; the mechanical numbers are the source of truth, the screenshot is the human-verifiable confirmation.
Scenario A (baseline)
Mechanical:
inline === ""ANDcomputed === "clamp(14rem, 20vw, 22rem)"ANDrectWidth ∈ [224, 352](the clamp's resolved band at any reasonable viewport).Visual: the sidebar fills a clearly NARROW column relative to the Scenario B screenshot; the Guides nav items wrap into the column at the default width.
Scenario B (restored 400 px) — THE PRIMARY ASSERTION
Mechanical:
inline === "400px"ANDcomputed === "400px"AND|rectWidth − 400| ≤ 1(one-pixel sub-pixel tolerance).Visual: the sidebar is VISIBLY WIDER than in Scenario A. The restored width must look the same as what the user got immediately after dragging to 400 px, NOT the narrower default.
Scenario C (clamp to MAX_W = 448)
Mechanical:
inline === "448px"ANDcomputed === "448px"AND|rectWidth − 448| ≤ 1.Visual: the sidebar is the widest of all four scenarios.
Scenario D (garbage falls through to default)
Mechanical: same as Scenario A (inline empty, computed is the clamp string, rectWidth in
[224, 352]).Visual: indistinguishable from Scenario A — the script silently no-ops on a non-numeric value, so the page renders exactly the default.
Overall verdict
PASS if all four scenarios pass their own criteria. FAIL otherwise.
Include in summary a one-line human-readable result, e.g.
"PASS: 400/448 px persisted widths visually restored on reload;
baseline + garbage values fall through to the CSS default" or, for a
fail, "FAIL: Scenario B rectWidth=224 (expected 400) — restore script
did not apply".
Tools
Primary:
/— Playwright CLI forheadless- browser goto,eval,reload,screenshot,resize,close-all. This task is multi-step interactive so/(single-page computed-style reads) does not fit.verify- ui The Playwright CLI does NOT accept a
--viewport-sizeflag onopen— use theresize <w> <h>subcommand AFTERopen.
Inputs (passed from the parent agent)
previewUrl— full URL to drive (defaulthttp:)./ / localhost: 3000/ docs/ guides/ i18n/ viewport—WxH(default1400x900).screenshotDir— directory to write screenshots into (default$HOME/).cclogs/ zudo- doc/ headless- screenshots/
Output schema
Return a single structured result with EXACTLY these fields:
{
baseline: { inline, computed, rectWidth, screenshot },
restored400: { inline, computed, rectWidth, screenshot },
clamp448: { inline, computed, rectWidth, screenshot },
garbageFallback: { inline, computed, rectWidth, screenshot },
verdict: "PASS" | "FAIL",
summary: "<one-line verdict>",
toolUsed: "headless-browser"
}screenshot is the absolute file path written by npx
@playwright/cli@latest screenshot --filename <path>. The four paths
should be distinct.
Don'ts
Don't change the verdict tolerance numbers — they are locked here.
Don't skip scenarios — all four are required for a PASS verdict.
Don't restart the dev server — assume it is already running at the URL provided in
Inputs.previewUrl.Don't post anywhere — return the structured result to the parent; the parent decides how to surface it.