zudo-test-wisdom
GitHub repository

Type to search...

to open search from anywhere

Level 5: Deterministic + Visual Verification

Deterministic computed-style assertions paired with an informal visual screenshot review.

What Level 5 Tests

Level 5 covers UI/CSS verification through two distinct techniques, used together:

  1. Deterministic computed-style assertions -- extract exact CSS values from a running page and compare them against expected values. No LLM interpretation involved.

  2. Screenshot capture for visual review -- an informal look at the rendered page to catch composition problems (overlap, stacking context) that computed styles alone can't reveal.

The two tools:

Why Both Are Needed

Computed style checks alone can miss visual issues that come from element interaction (overlapping, stacking context). Screenshots alone rely on human/LLM interpretation, which can have confirmation bias. Used together:

ApproachStrengthWeakness
Computed-style assertionsDeterministic, exact valuesCannot see visual composition
Screenshot reviewShows the full visual resultRelies on interpretation, not a pass/fail signal
Both combinedDeterministic values + an informal visual sanity checkStill blind to canvas/non-DOM surfaces (see Blind Spots)

Part 1: Deterministic Computed-Style Assertions

Run verify-styles.mjs against the element in question. It navigates to the page, extracts computed styles and the bounding box for the selector, and dumps the result as JSON -- there is no --check flag. The assertion step is comparing the JSON values against what you expect.

LOGDIR=$(node $HOME/.claude/scripts/get-logdir.js)
mkdir -p "$LOGDIR"
node $HOME/.claude/skills/verify-ui/scripts/verify-styles.mjs \
  "http://localhost:3000" ".hero-title" "$LOGDIR/verify-ui" "1200" "light"

Relevant excerpt of the JSON output:

{
  "styles": {
    "font-size": "48px",
    "color": "rgb(255, 255, 255)",
    "display": "block"
  }
}

Compare each value against the requirement:

[PASS] font-size: 48px (expected: 48px)
[PASS] color: rgb(255, 255, 255) (expected: rgb(255, 255, 255))
[PASS] display: block (expected: block)

The same script also reveals visibility problems that Level 1/2 tests cannot see -- see "The Common Failure This Catches" below.

Part 2: Screenshot Capture for Visual Review

Note

This is an informal visual review, not a deterministic pixel-diff check. This guide's current skill set has no pixel-diff tool -- capturing and reading a screenshot is a sanity look, useful for catching composition problems computed styles alone can't reveal, not a repeatable regression gate. For a deterministic, CI-gradable version of this screenshot half, see Deterministic Visual Regression with Committed Baselines.

# Full-page screenshot
node $HOME/.claude/skills/headless-browser/scripts/headless-check.js \
  --url http://localhost:3000 --screenshot full

# Viewport-only screenshot
node $HOME/.claude/skills/headless-browser/scripts/headless-check.js \
  --url http://localhost:3000 --screenshot viewport

There is no --width/--height flag. For a specific device viewport, pass it via stdin JSON instead:

echo '{"url":"http://localhost:3000","viewport":{"width":375,"height":812},"captureScreenshot":"viewport"}' \
  | node $HOME/.claude/skills/headless-browser/scripts/headless-check.js

For responsive checks across several widths in one run, use verify-styles.mjs's WIDTHS argument instead -- it captures a screenshot at every width x scheme combination alongside the computed-style dump:

node $HOME/.claude/skills/verify-ui/scripts/verify-styles.mjs \
  "http://localhost:3000" ".hero" "$LOGDIR/verify-ui" "375,800,1200" "light,dark"

The Common Failure This Catches

Consider the classic scenario:

  1. An AI agent adds a notification banner component

  2. Unit test confirms the component renders (Level 1 passes)

  3. DOM test confirms the element is in the tree (Level 2 passes)

  4. But the parent container has overflow: hidden and height: 0

  5. The banner exists in the DOM but is completely invisible

Level 5 catches this. Running verify-styles.mjs against .notification-container returns:

{
  "styles": {
    "height": "0px",
    "overflow": "hidden"
  }
}

getComputedStyle always resolves height to a pixel value -- it is never auto. Assert that it is not 0px (or matches the specific pixel height the layout requires), not height: auto:

[FAIL] height: 0px (expected: not 0px)
[FAIL] overflow: hidden (expected: visible)

The screenshot from headless-check.js gives the same conclusion visually -- the banner is not visible on the page.

Capture Visual Intent Before You Pick a Metric

Part 1's assertions are only as trustworthy as the values you chose to measure. There is a failure mode that survives every deterministic check: you assert a metric that is convenient to extract instead of the one that encodes the actual visual requirement, it comes back green, and verify-ui reports PASS on a layout that is still wrong. The JSON is exact; it just answers the wrong question. This is confirmation bias in deterministic clothing -- and it is exactly what /verify-ui exists to prevent, so getting the choice of signal right is the whole game.

Capture the visual intent before writing a single assertion.

Name the Expected and Forbidden states first

Before measuring anything, write down two states in plain language:

  • Expected -- the target visual relationship or layout the change is supposed to produce.

  • Forbidden -- the previous broken state, or a tempting partial fix that looks closer but still does not satisfy the requirement.

The Forbidden state is what catches false positives. A proxy metric can drift toward Expected while the Forbidden layout is still fully on screen; if you only assert the positive, nothing fails.

Read the screenshots before the DOM

If expected.png, now.png, or any reference screenshot exists, inspect those first -- before you open the DOM or decide what to select. Describe the diff between them in plain language ("the AI reason should sit above the budget; right now they are side by side"), then derive the deterministic assertions from that sentence. Deriving assertions the other way around -- from implementation intuition, or from whatever happens to be easy to query in the DOM -- is precisely how a proxy metric sneaks in.

Assert the Forbidden state, not just the Expected one

For any known-bad layout, add a negative assertion: a check that the Forbidden state is absent. Then apply the rule.

Warning

If any observed signal matches the old broken state or contradicts the expected screenshot, the verdict is FAIL or INCONCLUSIVE -- never PASS. A present Forbidden state forces the verdict; a suspicious mismatch signal cannot be waved away to reach a green result.

A false positive from a convenient proxy

A ReadyCrew triage row showed an AI reason and a budget side by side. The fix was to stack the AI reason above the budget. The first attempt only widened the AI-reason text and left the side-by-side layout intact -- yet verification reported PASS, because it checked a proxy:

{
  "reasonSpansTriageWidth": true,
  "reasonConfinedToAiColumn": false,
  "currentIsSideBySide": true
}

The first two fields are true, and on their own they look done: the reason now spans the triage width and is no longer boxed into the narrow AI column. But currentIsSideBySide: true is the exact state the screenshot was correcting -- the Forbidden state, still present. Widening text inside a side-by-side layout is a partial improvement, not the requested change. The correct verdict was FAIL, and the proxy metric hid it.

The contract should have asserted the Expected relationship directly, including the negative check on the Forbidden layout:

{
  "aiBudgetSideBySide": false,
  "aiAboveBudget": true,
  "sameColumn": true
}

Read this block as the required contract values. The still-present side-by-side layout measures aiBudgetSideBySide as true, which fails the required false -- the broken state trips the contract instead of slipping past it. This is the same trap as the "The Common Failure This Catches" section above: a lower signal passes while the thing the user actually asked for is still broken.

End with a diff verdict, not a metric dump

Do not report the raw JSON and stop. Close every visual verification with an explicit diff verdict that ties the numbers back to the two states you named:

Expected layout: AI reason stacked directly above the budget, same column
Observed layout: AI reason widened but still left of the budget (side by side)
Still different: yes -- the side-by-side (Forbidden) layout is still present
Verdict: FAIL

Report INCONCLUSIVE when the signals can neither confirm the Expected state nor rule out the Forbidden one -- for example when the selector did not resolve or the screenshot is ambiguous. Report PASS only when every Expected assertion holds and every Forbidden assertion is absent.

Combined Workflow

For UI/CSS changes, the recommended verification workflow is:

  1. Make the CSS/layout change

  2. Run verify-styles.mjs to extract exact computed values

  3. Capture a screenshot with headless-check.js for an informal visual look

  4. If computed styles pass but the screenshot looks wrong, investigate stacking/composition issues

  5. If the screenshot looks right but a computed-style value fails, re-derive the expected value from the original requirement. Update the expectation ONLY if the requirement changed -- never to silence a failing check based on a screenshot impression

Note

This two-step approach eliminates both false positives (screenshot looks fine but values are wrong) and false negatives (values look right but visual composition is broken).

Blind Spots

  • Font rendering differences across operating systems

  • Sub-pixel rendering variations

  • Animation timing (mid-frame states)

  • Browser-specific quirks not present in the test browser

  • Canvas-rendered content or any surface with no stable DOM bounding rect -- computed-style checks don't apply to <canvas> internals, and screenshot comparison is too noisy for anti-aliased or zoomed content. See Level 6.

When to Use Level 5

ScenarioLevel 5 Appropriate?
CSS change not taking effectYes
Element not visible despite being in DOMYes
Layout spacing looks wrongYes
Color or font-size incorrectYes
Responsive breakpoint issueYes

Revision History

CreatedUpdated