# Computed-style snapshot harness The app CSS is an ordered multi-file cascade. Hundreds of selectors are declared more than once and `!important` appears throughout, so the rendered result is a function of **source order**. Extracting a block into its own file, reordering `` tags, or moving an `@media` rule can silently change which declaration wins, and nothing else in the suite would notice. This harness makes that falsifiable. It captures `getComputedStyle` over a fixed element inventory, hashes the result, and compares it to a committed baseline. It moves no CSS itself. ## What it covers | Dimension | Values | |---|---| | Pages | `static/index.html` (app shell, 76 elements), `static/login.html` (14), the bench (586 selectors) | | Viewports | 1440x900, 820x1000, 768x1024 (touch), 390x844 (touch) | | Themes | dark (default) and `:root.light` | | Density | default, `:root.density-compact`, `:root.density-spacious` | | Properties | 122 pinned properties per element, plus every custom property on `:root` and `body` | That is 676 elements x 24 variants = 16,224 element snapshots per run, in about 21 seconds. The **app shell** page measures real elements in the markup the server sends, including modals - each one revealed on its own and re-hidden straight after, so the measurements stay independent. The **bench** page measures one synthesised element per selector, built from the selector itself. Its selector list is evidence-driven: every selector declared **more than once** in the app cascade that can be expressed as a static compound chain (551 of them), plus a curated set covering chat, documents, email, notes, calendar, settings, cookbook and gallery. Redeclared selectors are the ones a reorder can actually flip, so they are the ones worth benching. A bench element pins the cascade for that class combination; it does not pin the markup that the JS produces. Selectors the bench grammar cannot express are the gap: selector lists (`a, b`), pseudo-elements, pseudo-classes, `:not()` and `:has()`. They are skipped rather than approximated. ## Files | File | Role | |---|---| | `inventory.json` | The fixed inventory: properties, variants, pages, elements, bench selectors | | `baseline.json` | The committed digest plus per-element and per-variant hashes | | `capture.mjs` | Playwright capture; raw values on stdout | | `bench.html` | Empty page that loads the stylesheet; the capture mounts bench nodes into it | | `../test_css_computed_style_snapshot.py` | The regression test | | `../../scripts/css_snapshot.py` | Hashing, comparison, and the CLI | ## Running it ```bash ./venv/bin/python -m pytest tests/test_css_computed_style_snapshot.py ./venv/bin/python scripts/css_snapshot.py --check # same comparison, standalone ./venv/bin/python scripts/css_snapshot.py --write-baseline # re-record ``` The CLI serves the repository on an ephemeral port itself, so it does not need pytest. Under pytest the session static server is reused through `ODYSSEUS_TEST_STATIC_ORIGIN`. `npm ci` is required: the capture drives Playwright's Chromium. Without it the browser tests skip. ## When the test fails The failure names the elements and the variants whose hashes moved. To see which *property* moved, capture both sides and diff: ```bash ./venv/bin/python scripts/css_snapshot.py --dump after.json git stash && ./venv/bin/python scripts/css_snapshot.py --dump before.json && git stash pop diff <(python -m json.tool before.json) <(python -m json.tool after.json) ``` Re-record the baseline only when the change in rendered style is **intended** and reviewed. On a mechanical CSS extraction it never should be: an extraction that preserves order produces an identical digest, and one that does not has changed the UI. ## Determinism The digest is only worth having if an unchanged stylesheet always produces the same bytes, so the capture: - strips every `