Visual regression TDDs (overflow / chrome-overlap / proportion / sibling-collision)

mandatory

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 - scrollY

Exemptions: 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: 0 controls + 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:

  1. *he last item of any drawer/sidebar is fully visible*—

    lastChild.getBoundingClientRect().bottom ≤ visualViewport.height.

  2. *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.visualViewport for web, MediaQuery.viewPadding for Flutter).

  3. *100dvh` containers shrink correctly*— web only; if the layout

    sets height: 100vh (no dvh fallback 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:

  1. *o element with data-kds-decorative="true"*exceeds 33% of

    its container width on Compact (< 600 dp) viewports.

  2. *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`).
  3. *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.

  1. *ecorative margin — silhouette never touches container edges.*

    Per viewport in R2, for every data-kds-decorative="true" element measure getBoundingClientRect() 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].currentTime to each peak before measuring.

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 shape

Floor 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 declaring surfaces = [...].
  • A pubspec.yaml with flutter SDK dep.
  • A package.json with react, vue, svelte, @playwright/test,

    templ (Go), or @koder/web-kit dep.

  • An android/app/src/main/AndroidManifest.xml with <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_toolkit for snapshots +

    MediaQuery overrides for viewport states.

  • Android native → Compose UI Test (createComposeRule()) with

    setContent + onRoot().assertExists() overflow checks.

R5 — Run cadence

Trigger Categories Action on failure
Precommit (`/kcommit` § 5c) A + B + D --fast (3 phone rows only) Block commit
Prerelease (`/kship` per module) 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 (clampfloor + siblingcollision sanity) 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 20260521 evening following user- reported mobile screenshot). Open ticket: tools/design-gen#057 follow-up.

  • All engines/sdk/koder_kit Flutter consumers — handled by

    KoderSafeScaffold for 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 Allowlist audit: every `datavisualoverflow="allowedby:..."` references an existing spec § (else fail).
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 Siblingcollision (R1.D) fails on a 3shape decorative cluster at 390 dp portrait whose bounding boxes overlap > 5% of the smallest shape's area — audit reports the offending pair (a, b), the overlap area in px², and the % of min(area_a, area_b). Regression test for the KDShero pinkblueyellow pileon bug.

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.

  • *rossreferences*`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 — fixwithouttest

    rule 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-audit for Category A web — runs as

    Playwright assertion library.

  • Extend /k-test to detect surfaces per R4 and stub the suites.

Source: ../home/koder/dev/koder/meta/docs/stack/specs/develop/visual-regression-tdds.kmd