Conformance
What it means
Section titled “What it means”A renderer is eUI conformant when its runner replays every suite of the manifest and the report passes: every suite present, every case run, nothing failed or skipped. A surface then means the same thing on it as on ours. The same tree comes out of the reducer, every macro expands the same way, a theme resolves to the same colours, a control measures to the same box, and the host makes the same policy decisions.
The fixtures in fixtures/ are the contract. The TypeScript reference generated their expected values, and the Rust core replays them byte for byte. A renderer built on the Rust core gets the pure suites through its bindings, one built on the TypeScript package gets them from @exponential_app/eui, and anything else implements the reducer and host.json itself.
The suites
Section titled “The suites”Generated from the fixtures into conformance/manifest.json, which drift-gates the case counts. A suite’s unit says what one case is.
| Suite | Unit | Cases | A runner checks, per case |
|---|---|---|---|
catalog | case | 605 | Reduce the case's nested node with the core catalog: no issues. Paint it under the default theme: no crash, no Unknown placeholder, every visible node painted. |
macros | case | 315 | Expand the case: the tree equals expanded (JSON-equal, recipes included). |
basic-map | case | 13 | Reduce the A2UI basic surface: the tree and the issues equal the expected ones. |
extension | case | 4 | Register the extension; reduce each case: tree + issues equal; its natives reach the extension painter (an Extension leaf of that kind). |
theme-resolved | theme | 4 | Load each built-in theme: the resolved theme equals the fixture. |
theme-recipes | case | 481 | Resolve the part's recipe for the case's theme, props, mode and states: the visual equals the fixture. |
theme-extends | case | 3 | Load the theme over its parents: the chain and the probed styles equal the fixture. |
theme-invalid | case | 26 | Validate the bad theme: the same issue paths, never a crash. |
control-geometry | theme × control × case | 400 | Measure the control under the theme: the box (height, padding, gap, border, radius) equals the fixture. |
layout | width × direction | 4 | Lay the surface out with the fixture's fixed measure: every node's frame equals the fixture (exact for the Rust core and native painters, within 1 px for a CSS renderer). |
overlay | case | 11 | Place the overlay: side, flip and frame equal the fixture within tolerancePx. |
replay | theme × mode | 8 | Paint the kitchen sink under each built-in theme and mode: the reduced tree equals kitchen-sink.expanded.json, no Unknown, the accessibility order is the pre-order of the painted nodes. |
host-transport | case | 20 | Feed the chunks through ONE decoder (JSONL / SSE), or decode the MCP result, or build the MCP action call: messages + issues equal expected. |
host-policy | case | 120 | Decide the function / combine / url, build the media request, parse the source, append the bind action's params to a source, negotiate the catalog ids: each equals expected. |
host-router | package, screen or flow | 57 | Validate each package; validate each record against its kind's schema; resolve each screen (resolveScreen) and navigate target (navigateTarget, VAPP-218); for each flow build ONE router with extensionIds, install packages (issues = installIssues), route every step: the ops equal expected. |
bind | case | 73 | Expand the input; for every dataset run the bind pass: the bound tree, every set press and the per-row slot cells equal the fixture (reducer issues too for extra). |
style-conditions | case | 10 | Flatten the style for every context: equal to expected (key order irrelevant). |
code-tokens | case | 23 | Tokenize the code: the lines of tokens equal expected. |
format | call or value | 86 | Resolve each format call through the English fallback (zoned: at the case's offsetMinutes): the string equals expected exactly; display each value in a text prop: equals expected. A platform Formatter may run too, at en-US / UTC (zoned: timeZone), comparing after U+202F and U+00A0 → space. |
template-items | case | 26 | Key the items / rows, list the template instances (path, key, accumulated suffix), reduce the surfaces (templates lifted): equal to expected. |
text-direction | surface direction | 2 | Reduce the tree; every node's resolved direction and physical text alignment equal expected. |
resizable | case | 38 | Normalize / resize / key / measure / convert: the numbers equal expected within 1e-6. |
virtual-list | case | 26 | Window the extents, compute the scrollToIndex offset (sectioned: data index → row), group the sections, pin the sticky header: equal to expected. |
animations | theme × animation | 38 | Time each animation under the theme and sample its frames (and the reduced-motion frame): equal to expected within 1e-3; opacity: the painted opacity = the node's own × the frame's, within 1e-3. |
pointer | write or read | 16 | Write each writes case: expected + its read-back read, or error with the data unchanged; read each reads case: expected. |
read-only | case | 4 | Reduce the case; the read-only pass disables exactly disabled (root, then each lifted template by key). A painting runner mounts it with readOnly: every inert id paints disabled, every live id enabled; without readOnly the disabled ids paint enabled. |
Total: 2,413 cases in 26 suites (manifest version 2).
Run it on your renderer
Section titled “Run it on your renderer”A third-party renderer (Flutter, Qt, a terminal UI) becomes conformant in four steps:
- Read the manifest. It lists each suite’s fixture files, the unit and the case count. It ships in the npm package as
@exponential_app/eui/conformance/manifest.json, next to the fixtures. - Replay every case the way the suite’s check says: reduce, expand, resolve, measure, lay out, place, paint, decode or decide, and compare with the fixture’s expected value. Geometry suites take your painter’s own measurements (controls within 0.5, a CSS layout within 1 px).
- Write the report (below), one entry per suite, naming every failed case.
- Check it. The checker compares the report with the manifest and prints the verdict:
bun run --filter @exponential_app/eui conformance:check path/to/report.json# CONFORMANT my-renderer (flutter 0.12.0): 2413 cases passed in 26 suitesOutside the monorepo, checkReport() from @exponential_app/eui gives the same verdict in code. A report with a missing suite, a short case count or a skipped case fails.
The report
Section titled “The report”One JSON object per run, validated by report.schema.json:
{ "renderer": "my-renderer", "platform": "web", "version": "0.12.0", "conformanceVersion": 2, "suites": { "catalog": { "cases": 605, "passed": 605, "failed": [] }, "macros": { "cases": 315, "passed": 315, "failed": [] }, … }}renderer,platform,version: who ran it, where.conformanceVersion: the manifest version the runner was written against.suites.<id>:casesrun,passed,failed(the failed cases’ names with a short reason) and, optionally,skipped.
Our runners write it to $EXPONENTIAL_UI_CONFORMANCE_REPORT (default .conformance/<name>.json at the repo root).
Our runners and CI
Section titled “Our runners and CI”The core and all six first-party renderers run the full suite. The three web renderers run it in headless Chromium, and Solid and the web component must also match React exactly: the same DOM, computed styles, frames and pixels for every case and, for Solid, every primitive specimen, compared over the whole page body.
| Renderer | Runner | Command |
|---|---|---|
| eui-core (Rust core) | conformance.rs |
cargo test -p eui-core --test conformance |
@exponential_app/eui-react |
suites.test.ts |
bun run --filter @exponential_app/eui-react test:conformance |
@exponential_app/eui-solid |
suites.test.ts |
bun run --filter @exponential_app/eui-solid test:conformance |
@exponential_app/eui-solid (web parity with React: renderer + primitives) |
parity.test.ts |
bun run --filter @exponential_app/eui-solid test:parity |
@exponential_app/eui-element |
suites.test.ts |
bun run --filter @exponential_app/eui-element test:conformance |
@exponential_app/eui-element (web parity with React) |
parity.test.ts |
bun run --filter @exponential_app/eui-element test:parity |
| eui-gpui | suites.rs |
cargo test -p eui-gpui --test suites |
| ExponentialUI (SwiftUI) | ConformanceTests.swift |
swift test |
at.exponential:eui-compose |
ConformanceTest.kt |
./gradlew :ui-compose:testDebugUnitTest |
CI (exponential-ui.yml) runs all of them on every SDK pull request and fails unless every report is conformant. A release tag is only cut from a commit where that workflow is green.
The real-font harness
Section titled “The real-font harness”The suites lock the contract with a fixed fake measure. The real-font harness locks what real text does to it: the kitchen sink, the responsive cases and a project dashboard in both modes, rendered by React (headless Chromium) and gpui with the same committed fonts (Inter, Geist, JetBrains Mono, Nunito, Fira Code), compared frame by frame against a committed web baseline.
| Step | What it checks |
|---|---|
| Frame dumps | Every placed part of every case (fixture × theme × mode × width × direction): frame, text, line count. |
| Comparison | Frames within a pixel tolerance; a text may wrap one line more or less. Unexplained divergences are origins: the fix list. |
| Known divergences | conformance-known.json: per cause, which side is right and who fixes it; the ratchet only goes down. |
| Pixel step | The web surface beside a real gpui window: pixel ratio and SSIM. Reported only; it does not gate. |
bun run --filter @exponential_app/eui conformance -- --only kitchen-sink/playful/dark/390/ltrThe full harness (the matrix, recording, the shared browser runner and the web-parity details) is documented in conformance/README.md.