Visual regression TDDs (overflow / chrome-overlap / proportion / sibling-collision)
Every Koder UI surface — web, Flutter (mobile/desktop), Android native, iOS native, TV — MUST ship visual-regression TDDs covering four categories: (1) viewport overflow, (2) chrome overlap (browser URL bar / OS nav bar / IME / notch), (3) decorative-element proportion, and (4) sibling decorative collision (intra-container shapes piling onto each other at narrow viewports). Companion to specs/develop/docs-mobile-responsiveness.kmd (which covers structural responsiveness) and specs/app-layout/safe-area.kmd (which covers system chrome insets). This spec formalizes the TDD contract — what to generate, what to assert, which viewports, when to run.
Spec — Visual regression TDDs
Scope
Every interactive Koder surface MUST ship the three TDD categories below under tests/regression/visual/. Targets:
- *eb* docs site, landing pages, web apps (admin/dashboards).
- *lutter* mobile + desktop + web + TV.
- *ndroid native* Kotlin/Compose apps.
- *OS native* when shipped.
- *LI / TUI* column-width adaptation only (category C overflow
variant); other categories N/A.
Out of scope: static text files, server-only services with no UI, markdown rendered without page chrome.
R1 — Categories (the four TDDs)
Category A — Viewport overflow
For every named layout state (default, draweropen, modalopen, keyboard-open, etc.) at every R2 viewport, assert that *o rendered element crosses the viewport edge* Concretely:
∀ element e ∈ document.querySelectorAll('*'):
rect = e.getBoundingClientRect()
assert rect.right ≤ viewport.width
assert rect.left ≥ 0
assert rect.bottom ≤ visualViewport.height + scrollY
assert rect.top ≥ 0 - scrollYExemptions: explicitly opted-out elements may carry the data attribute data-visual-overflow="allowed-by:<spec-ref>" (e.g. allowed-by:components/carousel.kmd § R3 marquee). Audit reads the attribute and skips. No unmarked exemptions — bugs hide there.
Category B — Chrome overlap
Specifically tests dynamic chrome that grows / shrinks at runtime:
- *eb mobile (Chrome / Safari)* URL bar enters during scroll-up.
Test that
position: fixed; bottom: 0controls + last items of sidebar drawers + bottom sheets + FABs stay reachable. - *OS Safari* home indicator gesture area.
- *ndroid system chrome* status bar, 3-button nav bar, gesture
pill, IME (keyboard).
- *isplay cutouts* notch, hole-punch, Dynamic Island in landscape.
Required assertions, per viewport per chrome state:
- *he last item of any drawer/sidebar is fully visible*—
lastChild.getBoundingClientRect().bottom ≤ visualViewport.height. - *o critical control sits under chrome*— buttons, links, form
fields tested for intersection with the chrome regions reported by the OS or simulated by the test harness (
window.visualViewportfor web,MediaQuery.viewPaddingfor Flutter). - *100dvh` containers shrink correctly*— web only; if the layout
sets
height: 100vh(nodvhfallback above it) the assertion fails with a hint pointing to specsapp-layoutsafe-area.kmd § Web.
Category C — Decorative-element proportion
Decorative shapes (hero blobs, mascot illustrations, divider ornaments) must scale proportionally to their container, NOT bottom out on a fixed min value that overshoots small viewports.
Assertions:
- *o element with
data-kds-decorative="true"*exceeds 33% ofits container width on Compact (< 600 dp) viewports.
- *isual-area parity*— sibling decoratives in the same row must
not differ in perceived width by more than 12% (`Math.abs(a.width
- b.width) / Math.max(a.width, b.width) ≤ 0.12`).
- *clamp()` floor sanity*— extract the px floor of every
width: clamp(MIN, vw, MAX)declaration. Floor MUST be ≤ vw value at 360 dp viewport (otherwise the floor wins and the shape doesn't scale below tablet). Audit grep:tools/koder-css-audit clamp-floor --viewport 360.
- *ecorative margin — silhouette never touches container edges.*
Per viewport in R2, for every
data-kds-decorative="true"element measuregetBoundingClientRect()against its nearest positioned ancestor (the "container"). Assert:- At rest:
rect.top ≥ container.top + 4% × container.height - At rest:
rect.bottom ≤ container.bottom − 4% × container.height - At rest:
rect.left ≥ container.left + 4% × container.width - At rest:
rect.right ≤ container.right − 4% × container.width - *t every keyframe peak*(sample at 25 / 50 / 75% of each
named animation), the same 4% margin holds. The harness drives
Element.getAnimations()[i].currentTimeto each peak before measuring.
- At rest:
Rationale: keyframe peaks routinely push silhouettes past the resting bounding box (translate Y −55%, scale 1.15 etc.); checking only with animation-play-state: paused misses the worst frames. The 4% floor is a soft minimum — designers may raise it per surface via --kds-decorative-margin: 6%; on the container, which the audit reads via getComputedStyle(container).getPropertyValue(...).
Category D — Sibling decorative collision
Decorative siblings (hero shapes, mascot trio, illustration cluster) inside the same positioned container MUST NOT pile onto each other when the viewport shrinks. Category C guarantees each shape stays within its own clamp budget; Category D guarantees the cluster remains visually parseable as N separate shapes.
Concretely, for every container holding ≥ 2 elements with data-kds-decorative="true" (or matching the same data-kds-decorative-group="<name>"), per viewport in R2:
∀ par (a, b) de shapes irmãs no mesmo container, em cada keyframe peak:
rect_a = a.getBoundingClientRect()
rect_b = b.getBoundingClientRect()
overlap_area = max(0, min(a.right, b.right) - max(a.left, b.left))
× max(0, min(a.bottom, b.bottom) - max(a.top, b.top))
min_area = min(rect_a.width × rect_a.height, rect_b.width × rect_b.height)
assert overlap_area / min_area ≤ 0.05 # ≤ 5% of the smaller shapeFloor is *%*of the smaller shape's area — small grazing touch (decorative blend) tolerated; pileon (the pinkblue-yellow trio in the user-reported #057 KDS hero bug at 390 dp) flagged. Surface may raise the floor per-container with --kds-decorative-collision-floor: 12%; for intentional overlapping collages (audit reads via getComputedStyle); raising the floor requires a spec § reference in the same PR (mirror R6 allow-list rule).
Run at the * phone rows*of R2 plus phone-360l (landscape — notch state often triggers extra collision). Sample at keyframe peaks via the same Element.getAnimations() mechanism as Category C R1.C.4.
Rationale: a hero cluster sized for desktop (3 shapes laid out horizontally with 24 dp gaps) collapses to a vertical-ish pile on a 360 dp portrait viewport when the container narrows below the sum of the shapes' min widths. Either the layout reflows (preferable — column stack with gap reset) or the shapes scale down further (must preserve gap proportionally). Category D fails on both regression modes; the fix is structural, not per-shape clamp tweaks.
R2 — Test viewport matrix (mandatory)
Mirrors docs-mobile-responsiveness.kmd § R5 extended with two landscape rows:
| ID | Width × Height | Device class | Notes |
|---|---|---|---|
phone-360p |
360 × 800 | Android phone portrait | The cheap-end baseline |
phone-390p |
390 × 844 | iPhone 15 portrait | iOS Safari URL-bar dance |
phone-360l |
800 × 360 | Phone landscape | Notch cutout test |
phone-se |
320 × 568 | iPhone SE | Smallest reasonable mobile |
tablet-p |
768 × 1024 | Tablet portrait | Medium class |
tablet-l |
1024 × 768 | Tablet landscape | |
laptop |
1280 × 800 | Laptop | Expanded class |
desktop |
1440 × 900 | Desktop | Wide |
ultrawide |
1920 × 1080 | Large class baseline |
CI MUST run categories A and B at every row. Category C runs at the 3 phone rows (decoratives matter on the floor). Category D runs at the 3 phone rows + phone-360l (landscape notch surfaces extra collision states).
R3 — Chrome states to enumerate (Category B)
Per R2 phone row, simulate each chrome state:
| State | Web (visualViewport.height) |
Flutter (viewPadding) |
|---|---|---|
| URL bar visible | width × (height - 56) (Chrome Android) / (height - 44) (Safari iOS) |
n/a (handled by SDK) |
| URL bar hidden | width × height |
n/a |
| IME open | width × (height - 280) typical Latin keyboard |
bottom: 280 |
| 3-button nav | bottom: 48 reserved |
already in viewPadding.bottom |
| Gesture pill | bottom: 24 reserved |
already in viewPadding.bottom |
| Notch landscape (390l) | left: 44 or right: 44 |
left/right: 44 |
The audit harness drives the viewport through every state and runs the A/B assertions for each.
R4 — Generation contract
/k-test (TDD generator) MUST emit the visual-regression suite when the module has any of:
- A
koder.toml[ui]block declaringsurfaces = [...]. - A
pubspec.yamlwithflutterSDK dep. - A
package.jsonwithreact,vue,svelte,@playwright/test,templ(Go), or@koder/web-kitdep. - An
android/app/src/main/AndroidManifest.xmlwith<activity>.
Generated tests live under <module>/tests/regression/visual/. File naming: <viewport-id>-<category>.spec.{ts,dart,kt}. Snapshots live in the sibling __snapshots__/ directory and ARE committed (per policies/regression-tests.kmd — snapshots are golden, not artifacts).
Template engines:
- Web → Playwright + the snippet in
specs/web-apps/responsive-smoke.test.js,extended with category ABC assertions.
- Flutter →
flutter_test+golden_toolkitfor snapshots +MediaQueryoverrides for viewport states. - Android native → Compose UI Test (
createComposeRule()) withsetContent+onRoot().assertExists()overflow checks.
R5 — Run cadence
| Trigger | Categories | Action on failure |
|---|---|---|
| Pre |
A + B + D --fast (3 phone rows only) |
Block commit |
| Pre |
A + B + C + D all rows | Block release |
| Nightly CI | A + B + C + D all rows + chrome states | Open ticket auto |
/k-housekeep |
C + D (clamp |
Open ticket auto |
R6 — Allowlist antipattern
If a bug is found that the audit doesn't catch, the fix is to *dd a new assertion* not to allowlist the failing element. Allowlist entries (R1 datavisualoverflow attribute) require a spec reference and reviewer sign-off (PR description must link the spec § allowing the exemption).
R7 — Existing live gaps (snapshot at ratification)
At time of ratification (20260521), the following surfaces have NO visual-regression suite:
tools/design-gen(KDS docs site) — depended on Lighthouse Mobile+ manual review. Gap caught by user-reported bugs same day: (1) hero shapes overshooting bounding box at 22vw / 18vw clamp floors on Compact, (2) sidebar drawer last item clipped by Chrome Android URL bar, (3) topbar gear icon clipping right viewport edge, (4) hero-shape trio (pink square + blue triangle + yellow circle) piling onto each other at 390 dp portrait — *aught by new Category D*(item 4 added 2026
0521 evening following user- reported mobile screenshot). Open ticket:tools/design-gen#057follow-up.- All
engines/sdk/koder_kitFlutter consumers — handled byKoderSafeScaffoldfor chrome, but no overflow audit. - All Android native apps — manual screenshot review only.
Track closure in meta/docs/stack/registries/visual-regression-coverage.md (new — created with this spec).
Tests of the test contract
| ID | Test |
|---|---|
| T1 | Every UI module has tests/regression/visual/ populated by /k-test --gen-only. |
| T2 | CI matrix runs categories A + B at all R2 viewports per module. |
| T3 | /k-commit blocks on a fresh element overflow regression. |
| T4 | Allow |
| T5 | Visual-area parity (R1.C.2) fails on a deliberate 30% size delta between sibling decoratives (regression test for the regression test). |
| T6 | clamp() floor sanity (R1.C.3) fails on a width: clamp(200px, 18vw, 240px) at 360 dp (18vw = 65 < 200, floor wins, audit alerts). |
| T7 | Decorative margin (R1.C.4) fails on a hero card whose triangular shape has its keyframe peak at translate(-50%, -55%) pushing the top edge to −2 px relative to the container — audit reports the offending keyframe % + element + computed delta. |
| T8 | Siblingmin(area_a, area_b). Regression test for the KDS |
Relationship to existing specs
- *ubset, not duplicate, of*
docs-mobile-responsiveness.kmd §R4—that spec covers Lighthouse-scope structural checks; this spec adds per-element overflow / chrome / proportion.
- *ross
references*`safearea.kmd` — chrome overlap (Category B)uses the same insets API; this spec adds the test that verifies consumption.
- *ound by*
policies/regression-tests.kmd— fixwithouttestrule applies: any visual fix must ship a Category-ABC test that would have caught it.
Open follow-ups
- Build
tools/koder-css-audit clamp-floor(referenced in R1.C.3). - Build
tools/koder-overflow-auditfor Category A web — runs asPlaywright assertion library.
- Extend
/k-testto detect surfaces per R4 and stub the suites.