@exponential-at/ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +54 -0
- package/README.md +360 -2
- package/catalog/a11y.json +668 -0
- package/catalog/basic-map.json +104 -0
- package/catalog/code.json +58 -0
- package/catalog/core.catalog.json +4752 -0
- package/catalog/core.schema.json +22667 -0
- package/catalog/host.json +64 -0
- package/catalog/layout.json +20 -0
- package/catalog/limits.json +9 -0
- package/catalog/locale.json +48 -0
- package/catalog/macros.json +3417 -0
- package/catalog/recipes.json +61 -0
- package/catalog/strings.json +72 -0
- package/catalog/style.json +128 -0
- package/catalog/theme.schema.json +42543 -0
- package/catalog/tokens.json +47 -0
- package/conformance/README.md +217 -0
- package/conformance/baseline.ts +101 -0
- package/conformance/browser-page.ts +146 -0
- package/conformance/browser-suites.ts +683 -0
- package/conformance/compare.ts +201 -0
- package/conformance/dom.ts +96 -0
- package/conformance/dump.ts +163 -0
- package/conformance/fonts.json +154 -0
- package/conformance/manifest.json +224 -0
- package/conformance/parity-compare.ts +191 -0
- package/conformance/parity-report.schema.json +73 -0
- package/conformance/parity.ts +412 -0
- package/conformance/pixel.ts +177 -0
- package/conformance/primitives-page.ts +49 -0
- package/conformance/report.schema.json +66 -0
- package/conformance/run.ts +118 -0
- package/dist/chunks/index-98npjvsd.js +3940 -0
- package/dist/chunks/index-98npjvsd.js.map +27 -0
- package/dist/chunks/index-gwkt14w3.js +7314 -0
- package/dist/chunks/index-gwkt14w3.js.map +10 -0
- package/dist/chunks/index-njt7yzzm.js +134 -0
- package/dist/chunks/index-njt7yzzm.js.map +9 -0
- package/dist/chunks/index-we7va6v6.js +7984 -0
- package/dist/chunks/index-we7va6v6.js.map +15 -0
- package/dist/chunks/index-xrm6f7wc.js +2563 -0
- package/dist/chunks/index-xrm6f7wc.js.map +10 -0
- package/dist/conformance.js +182749 -0
- package/dist/conformance.js.map +10 -0
- package/dist/entries/prompt.js +251 -0
- package/dist/entries/prompt.js.map +11 -0
- package/dist/entries/schema.js +1725 -0
- package/dist/entries/schema.js.map +11 -0
- package/dist/host/index.js +122 -0
- package/dist/host/index.js.map +9 -0
- package/dist/index.js +3213 -0
- package/dist/index.js.map +27 -0
- package/dist/types/src/a11y.d.ts +21 -0
- package/dist/types/src/animation.d.ts +76 -0
- package/dist/types/src/basic-map.d.ts +38 -0
- package/dist/types/src/builder.d.ts +50 -0
- package/dist/types/src/catalog.d.ts +54 -0
- package/dist/types/src/catalog.described.d.ts +33 -0
- package/dist/types/src/catalog.generated.d.ts +829 -0
- package/dist/types/src/chart.d.ts +46 -0
- package/dist/types/src/code.d.ts +44 -0
- package/dist/types/src/color.d.ts +20 -0
- package/dist/types/src/conformance.d.ts +39 -0
- package/dist/types/src/connector/app.d.ts +14 -0
- package/dist/types/src/connector/connector.d.ts +77 -0
- package/dist/types/src/connector/index.d.ts +4 -0
- package/dist/types/src/css.d.ts +29 -0
- package/dist/types/src/define.d.ts +66 -0
- package/dist/types/src/direction.d.ts +16 -0
- package/dist/types/src/dynamic.d.ts +159 -0
- package/dist/types/src/entries/prompt.d.ts +11 -0
- package/dist/types/src/entries/schema.d.ts +8 -0
- package/dist/types/src/expr.d.ts +71 -0
- package/dist/types/src/extension.d.ts +4 -0
- package/dist/types/src/format.d.ts +112 -0
- package/dist/types/src/geometry.d.ts +40 -0
- package/dist/types/src/host/contract.d.ts +188 -0
- package/dist/types/src/host/decoders.d.ts +61 -0
- package/dist/types/src/host/index.d.ts +11 -0
- package/dist/types/src/host/package.d.ts +58 -0
- package/dist/types/src/host/policy.d.ts +105 -0
- package/dist/types/src/host/router.d.ts +34 -0
- package/dist/types/src/host/runtime.d.ts +205 -0
- package/dist/types/src/host/sources.d.ts +15 -0
- package/dist/types/src/host/transports.d.ts +109 -0
- package/dist/types/src/index.d.ts +63 -0
- package/dist/types/src/layout.d.ts +20 -0
- package/dist/types/src/limits.d.ts +21 -0
- package/dist/types/src/list.d.ts +133 -0
- package/dist/types/src/locale.d.ts +22 -0
- package/dist/types/src/macros.d.ts +29 -0
- package/dist/types/src/overlay.d.ts +27 -0
- package/dist/types/src/prompt.d.ts +44 -0
- package/dist/types/src/recipes.d.ts +17 -0
- package/dist/types/src/reducer.d.ts +41 -0
- package/dist/types/src/resizable.d.ts +38 -0
- package/dist/types/src/schema.d.ts +7 -0
- package/dist/types/src/strings.d.ts +13 -0
- package/dist/types/src/style.d.ts +201 -0
- package/dist/types/src/test-preload.d.ts +1 -0
- package/dist/types/src/text-shimmer.d.ts +24 -0
- package/dist/types/src/theme-types.d.ts +162 -0
- package/dist/types/src/theme.d.ts +73 -0
- package/dist/types/src/themes.d.ts +22 -0
- package/dist/types/src/tree-guides.d.ts +17 -0
- package/dist/types/src/types.d.ts +314 -0
- package/dist/types/src/validate.d.ts +31 -0
- package/dist/types/src/web/base-css.d.ts +1 -0
- package/dist/types/src/web/box-css.d.ts +55 -0
- package/dist/types/src/web/coerce.d.ts +32 -0
- package/dist/types/src/web/data.d.ts +105 -0
- package/dist/types/src/web/dates.d.ts +12 -0
- package/dist/types/src/web/form.d.ts +44 -0
- package/dist/types/src/web/host-types.d.ts +119 -0
- package/dist/types/src/web/index.d.ts +28 -0
- package/dist/types/src/web/inputs.d.ts +55 -0
- package/dist/types/src/web/list.d.ts +13 -0
- package/dist/types/src/web/listbox.d.ts +14 -0
- package/dist/types/src/web/markdown.d.ts +96 -0
- package/dist/types/src/web/media.d.ts +62 -0
- package/dist/types/src/web/parts.d.ts +4 -0
- package/dist/types/src/web/primitives/alert-dialog.d.ts +10 -0
- package/dist/types/src/web/primitives/alert.d.ts +9 -0
- package/dist/types/src/web/primitives/avatar-color.d.ts +2 -0
- package/dist/types/src/web/primitives/avatar.d.ts +13 -0
- package/dist/types/src/web/primitives/badge.d.ts +10 -0
- package/dist/types/src/web/primitives/breadcrumb.d.ts +10 -0
- package/dist/types/src/web/primitives/button.d.ts +6 -0
- package/dist/types/src/web/primitives/calendar.d.ts +43 -0
- package/dist/types/src/web/primitives/card.d.ts +9 -0
- package/dist/types/src/web/primitives/checkbox.d.ts +5 -0
- package/dist/types/src/web/primitives/choice-tiles.d.ts +32 -0
- package/dist/types/src/web/primitives/cn.d.ts +3 -0
- package/dist/types/src/web/primitives/code-block.d.ts +24 -0
- package/dist/types/src/web/primitives/command.d.ts +134 -0
- package/dist/types/src/web/primitives/context-ring.d.ts +24 -0
- package/dist/types/src/web/primitives/dialog-arms.d.ts +6 -0
- package/dist/types/src/web/primitives/dialog.d.ts +24 -0
- package/dist/types/src/web/primitives/disclosure-header.d.ts +5 -0
- package/dist/types/src/web/primitives/dock.d.ts +42 -0
- package/dist/types/src/web/primitives/dropdown-menu.d.ts +14 -0
- package/dist/types/src/web/primitives/empty-state.d.ts +15 -0
- package/dist/types/src/web/primitives/glass-card.d.ts +4 -0
- package/dist/types/src/web/primitives/hover-card.d.ts +7 -0
- package/dist/types/src/web/primitives/icon-disc.d.ts +12 -0
- package/dist/types/src/web/primitives/index.d.ts +48 -0
- package/dist/types/src/web/primitives/input.d.ts +5 -0
- package/dist/types/src/web/primitives/kbd.d.ts +4 -0
- package/dist/types/src/web/primitives/label.d.ts +3 -0
- package/dist/types/src/web/primitives/menu-surface.d.ts +17 -0
- package/dist/types/src/web/primitives/meter.d.ts +21 -0
- package/dist/types/src/web/primitives/popover.d.ts +3 -0
- package/dist/types/src/web/primitives/progress-ring.d.ts +5 -0
- package/dist/types/src/web/primitives/progress.d.ts +8 -0
- package/dist/types/src/web/primitives/scroll-area.d.ts +49 -0
- package/dist/types/src/web/primitives/segmented-control.d.ts +5 -0
- package/dist/types/src/web/primitives/select.d.ts +47 -0
- package/dist/types/src/web/primitives/separator.d.ts +3 -0
- package/dist/types/src/web/primitives/sheet-chrome.d.ts +4 -0
- package/dist/types/src/web/primitives/sheet.d.ts +20 -0
- package/dist/types/src/web/primitives/skeleton.d.ts +3 -0
- package/dist/types/src/web/primitives/spinner.d.ts +3 -0
- package/dist/types/src/web/primitives/switch.d.ts +4 -0
- package/dist/types/src/web/primitives/table.d.ts +11 -0
- package/dist/types/src/web/primitives/tabs.d.ts +13 -0
- package/dist/types/src/web/primitives/textarea.d.ts +3 -0
- package/dist/types/src/web/primitives/toast.d.ts +163 -0
- package/dist/types/src/web/primitives/tooltip.d.ts +3 -0
- package/dist/types/src/web/primitives/truncate.d.ts +7 -0
- package/dist/types/src/web/primitives/typeahead-portal.d.ts +12 -0
- package/dist/types/src/web/share.d.ts +6 -0
- package/dist/types/src/web/table.d.ts +20 -0
- package/dist/types/src/web/theme-css.d.ts +69 -0
- package/dist/types/src/web/urls.d.ts +21 -0
- package/dist/web/index.js +2303 -0
- package/dist/web/index.js.map +26 -0
- package/dist/web/primitives/index.js +1450 -0
- package/dist/web/primitives/index.js.map +57 -0
- package/docs/components.generated.json +6238 -0
- package/docs/round-1-contract.md +679 -0
- package/docs/round-2-contract.md +476 -0
- package/docs/round-3-contract.md +174 -0
- package/docs/themes.generated.json +3503 -0
- package/fixtures/animations.json +2890 -0
- package/fixtures/bench-list.json +58 -0
- package/fixtures/bind-time.json +30664 -0
- package/fixtures/catalog-basic-map.json +1692 -0
- package/fixtures/catalog-components.json +9323 -0
- package/fixtures/catalog-extension.json +620 -0
- package/fixtures/catalog-macros.json +36370 -0
- package/fixtures/code-tokens.json +1993 -0
- package/fixtures/conformance-baseline.json +55544 -0
- package/fixtures/conformance-cases.json +49 -0
- package/fixtures/conformance-known.json +2123 -0
- package/fixtures/control-geometry.json +4200 -0
- package/fixtures/dashboard.data.json +74 -0
- package/fixtures/dashboard.json +752 -0
- package/fixtures/demo-surface.json +47 -0
- package/fixtures/format.json +870 -0
- package/fixtures/host-policy.json +1067 -0
- package/fixtures/host-router.json +1152 -0
- package/fixtures/host-transport.json +590 -0
- package/fixtures/interactions-round1.json +773 -0
- package/fixtures/kitchen-sink.expanded.json +10844 -0
- package/fixtures/kitchen-sink.json +3049 -0
- package/fixtures/layout-geometry-round1.json +7278 -0
- package/fixtures/layout-geometry.json +2072 -0
- package/fixtures/overlay-geometry.json +272 -0
- package/fixtures/primitive-specimens.json +58 -0
- package/fixtures/prompt-budget.json +33 -0
- package/fixtures/resizable.json +714 -0
- package/fixtures/round4-contract.json +2985 -0
- package/fixtures/specimens.json +9832 -0
- package/fixtures/style-conditions.json +336 -0
- package/fixtures/template-items.json +2215 -0
- package/fixtures/text-direction.json +309 -0
- package/fixtures/text-shimmer.json +96 -0
- package/fixtures/theme-extends.json +573 -0
- package/fixtures/theme-invalid.json +1296 -0
- package/fixtures/theme-recipes.json +58600 -0
- package/fixtures/theme-resolved.json +23949 -0
- package/fixtures/toast-stack.json +215 -0
- package/fixtures/tree-guides.json +224 -0
- package/fixtures/virtual-list.json +733 -0
- package/generated/ExponentialUICatalog.generated.kt +67 -0
- package/generated/ExponentialUICatalog.generated.swift +67 -0
- package/generated/ExponentialUIThemes.generated.kt +23 -0
- package/generated/ExponentialUIThemes.generated.swift +23 -0
- package/generated/catalog.descriptions.generated.rs +6 -0
- package/generated/catalog.generated.rs +80 -0
- package/generated/catalog.themes.generated.rs +34 -0
- package/generated/core.descriptions.json +2505 -0
- package/generated/core.runtime.json +3609 -0
- package/package.json +87 -4
- package/themes/exponential.recipes.json +49 -0
- package/themes/exponential.theme.json +755 -0
- package/themes/neutral.theme.json +5676 -0
- package/themes/orange.theme.json +43 -0
- package/themes/playful.theme.json +219 -0
- package/vendor/a2ui/LICENSE.txt +203 -0
- package/vendor/a2ui/README.md +27 -0
- package/vendor/a2ui/v0_9/basic_catalog.json +1387 -0
- package/vendor/a2ui/v0_9/basic_catalog_rules.txt +5 -0
- package/vendor/a2ui/v0_9/client_capabilities.json +97 -0
- package/vendor/a2ui/v0_9/client_to_server.json +104 -0
- package/vendor/a2ui/v0_9/common_types.json +315 -0
- package/vendor/a2ui/v0_9/server_to_client.json +132 -0
- package/vendor/json-render/LICENSE.txt +201 -0
- package/vendor/json-render/README.md +13 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "VAPP-85: the token NAMES the core catalog's macros and Box styles reference as `$<group>.<name>` (e.g. `$color.primary`, `$spacing.md`). VALUES come from a theme (VAPP-92); this file only fixes the vocabulary. `color` = the shadcn semantic set plus success/warning/info and the eight chart hues (round 1 added chart6..8); `spacing`/`radius`/`type` mirror packages/design-tokens' scales by NAME (xl2 because 2xl is not an identifier in Swift/Kotlin/Rust); `control` = the per-kind control heights the VAPP-4 spike asked for instead of px (round 1 adds `appBar`, `tabBar`, `toast`); `shadow` = the boxShadow presets, ALSO the elevation scale (none < sm < md < lg < xl: flat, card, popover, dialog, toast; paint order inside a layer stays tree order, overlays stack by layer, never by zIndex); `opacity` = the text-emphasis ladder; VAPP-92 adds `border` (the border-width ladder, px) and `motion` (durations, ms). Round 1 (docs/round-1-contract.md) adds `breakpoint` (px, the ONLY tokens a style CONDITION may reference: `\"@media (min-width: $breakpoint.md)\"`; resolved against the theme before matching), `ease` (cubic-bezier control points [x1, y1, x2, y2], referenced by `transitionEasing`) and `density` (multipliers over `control` and `spacing` for the surface density `compact`/`comfortable`; `default` = 1, never a token). Round 2 (docs/round-2-contract.md §2) adds `blur` (px radii, the ONLY values `backdropBlur` takes). EXP-1259 (EXP-1243, packages/design-tokens motion.scale) adds `scale` (unitless multipliers the PAINTERS read, never a style value): `press` = what a pressed button scales to, `popoverFrom` = what a popover or menu scales FROM while it fades in (never from 0); reduce motion = no scale. Every group is append-only: a theme is validated against these names.",
|
|
3
|
+
"color": [
|
|
4
|
+
"background", "foreground",
|
|
5
|
+
"card", "cardForeground",
|
|
6
|
+
"popover", "popoverForeground",
|
|
7
|
+
"primary", "primaryForeground",
|
|
8
|
+
"secondary", "secondaryForeground",
|
|
9
|
+
"muted", "mutedForeground",
|
|
10
|
+
"accent", "accentForeground",
|
|
11
|
+
"destructive", "destructiveForeground",
|
|
12
|
+
"success", "successForeground",
|
|
13
|
+
"warning", "warningForeground",
|
|
14
|
+
"info", "infoForeground",
|
|
15
|
+
"border", "input", "ring",
|
|
16
|
+
"chart1", "chart2", "chart3", "chart4", "chart5", "chart6", "chart7", "chart8"
|
|
17
|
+
],
|
|
18
|
+
"spacing": ["none", "xxs", "xs", "sm", "md", "lg", "xl", "xl2", "xl3"],
|
|
19
|
+
"radius": ["none", "sm", "md", "lg", "xl", "xl2", "xl3", "full"],
|
|
20
|
+
"type": {
|
|
21
|
+
"size": ["xs", "sm", "base", "lg", "xl", "xl2", "xl3"],
|
|
22
|
+
"lineHeight": ["xs", "sm", "base", "lg", "xl", "xl2", "xl3"],
|
|
23
|
+
"weight": ["regular", "medium", "semibold", "bold"],
|
|
24
|
+
"family": ["sans", "mono"]
|
|
25
|
+
},
|
|
26
|
+
"control": [
|
|
27
|
+
"buttonSm", "buttonMd", "buttonLg", "buttonIcon",
|
|
28
|
+
"input", "textarea",
|
|
29
|
+
"row", "rowCompact",
|
|
30
|
+
"pill", "badge", "chip",
|
|
31
|
+
"toggle", "switch", "checkbox", "slider",
|
|
32
|
+
"progress", "meter",
|
|
33
|
+
"avatarSm", "avatarMd", "avatarLg",
|
|
34
|
+
"iconSm", "iconMd", "iconLg",
|
|
35
|
+
"hairline",
|
|
36
|
+
"appBar", "tabBar", "toast"
|
|
37
|
+
],
|
|
38
|
+
"shadow": ["none", "sm", "md", "lg", "xl"],
|
|
39
|
+
"opacity": ["primary", "secondary", "tertiary", "quaternary", "disabled"],
|
|
40
|
+
"border": ["none", "hairline", "thick"],
|
|
41
|
+
"motion": ["fast", "standard", "slow"],
|
|
42
|
+
"breakpoint": ["sm", "md", "lg", "xl"],
|
|
43
|
+
"ease": ["standard", "decelerate", "accelerate"],
|
|
44
|
+
"density": ["compact", "comfortable"],
|
|
45
|
+
"blur": ["sm", "md", "lg", "xl"],
|
|
46
|
+
"scale": ["press", "popoverFrom"]
|
|
47
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Exponential UI conformance
|
|
2
|
+
|
|
3
|
+
A renderer is **Exponential UI conformant** when its runner replays every
|
|
4
|
+
suite of [`manifest.json`](manifest.json) (GENERATED from the fixtures,
|
|
5
|
+
drift-gated) and the report passes:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun run --filter @exponential-at/ui conformance:check path/to/report.json
|
|
9
|
+
# CONFORMANT my-renderer (flutter 0.3.0): <n> cases passed in <m> suites
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Every suite present, every case run (the manifest's `cases` count), nothing
|
|
13
|
+
failed or skipped. The fixtures under `../fixtures/` are the contract; the
|
|
14
|
+
TS reference (`../src/`) generated their `expected` values and the Rust core
|
|
15
|
+
replays them byte for byte.
|
|
16
|
+
|
|
17
|
+
## The suites
|
|
18
|
+
|
|
19
|
+
| suite | fixture | a runner does, per case |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `catalog` | `catalog-components.json` | reduce the nested node (no issues), PAINT it: no crash, no `Unknown` |
|
|
22
|
+
| `macros` | `catalog-macros.json` | expand: the tree equals `expanded` |
|
|
23
|
+
| `basic-map` | `catalog-basic-map.json` | reduce the A2UI basic surface: tree + issues equal |
|
|
24
|
+
| `extension` | `catalog-extension.json` | register the extension, reduce: tree + issues equal |
|
|
25
|
+
| `theme-resolved` | `theme-resolved.json` | load each built-in theme: equal |
|
|
26
|
+
| `theme-recipes` | `theme-recipes.json` | resolve the part recipe per theme × mode × state: equal |
|
|
27
|
+
| `theme-extends` | `theme-extends.json` | load over the parents: chain + probes equal |
|
|
28
|
+
| `theme-invalid` | `theme-invalid.json` | validate: the same issues, never a crash |
|
|
29
|
+
| `control-geometry` | `control-geometry.json` | measure YOUR painted control: every fixed key within 0.5 (with `minHeight`, height is only a floor) |
|
|
30
|
+
| `layout` | `layout-geometry.json` | lay out with the fixed measure: frames exact (a CSS renderer: within 1 px) |
|
|
31
|
+
| `overlay` | `overlay-geometry.json` | place the overlay: side, flip, position within `tolerancePx` |
|
|
32
|
+
| `replay` | `kitchen-sink.json` | paint the sink per built-in theme × mode: the expanded tree, no `Unknown`, paint/a11y order = pre-order |
|
|
33
|
+
| `host-transport` | `host-transport.json` | JSONL / SSE chunks through ONE decoder, the MCP carrier: messages + issues equal |
|
|
34
|
+
| `host-policy` | `host-policy.json` | the function gate, combine, URL policy, media request, source URIs, catalog negotiation |
|
|
35
|
+
| `host-router` | `host-router.json` | package validation; message flows → the ops, in order |
|
|
36
|
+
| `bind` | `bind-time.json` | expand, run the bind pass per dataset: bound tree, `set` presses, per-row slot cells |
|
|
37
|
+
| `style-conditions` | `style-conditions.json` | flatten the style per context |
|
|
38
|
+
| `code-tokens` | `code-tokens.json` | tokenize: the lines of tokens |
|
|
39
|
+
| `format` | `format.json` | format calls through the English fallback (`zoned`: at `offsetMinutes`), exact; display values. A platform Formatter may run too (U+202F, U+00A0 → space) |
|
|
40
|
+
| `template-items` | `template-items.json` | item and row keys, template instances + escaped suffixes, reduce with templates lifted (cycles reported, kept in place) |
|
|
41
|
+
| `text-direction` | `text-direction.json` | every node's direction and physical text alignment |
|
|
42
|
+
| `resizable` | `resizable.json` | panel sizes (normalize, drag, keys, extents, px → percent) within 1e-6 |
|
|
43
|
+
| `virtual-list` | `virtual-list.json` | the window, scrollToIndex (sectioned: data index → row), sections, the sticky header |
|
|
44
|
+
| `animations` | `animations.json` | timing + frames per theme within 1e-3; painted opacity = own × frame |
|
|
45
|
+
|
|
46
|
+
`manifest.json` says how each count is derived (`unit`). A renderer built on
|
|
47
|
+
the Rust core (the facade, `exponential-ui-ffi`) gets the pure suites through
|
|
48
|
+
its bindings; one on the TS package gets them from `@exponential-at/ui`;
|
|
49
|
+
anything else implements `catalog/host.json` and the reducer itself.
|
|
50
|
+
|
|
51
|
+
## The report
|
|
52
|
+
|
|
53
|
+
[`report.schema.json`](report.schema.json):
|
|
54
|
+
`{renderer, platform, version, conformanceVersion, suites: {<id>: {cases,
|
|
55
|
+
passed, failed: [names], skipped?}}}`. Our runners write it to
|
|
56
|
+
`$EXPONENTIAL_UI_CONFORMANCE_REPORT` (default `<repo>/.conformance/<name>.json`):
|
|
57
|
+
|
|
58
|
+
| renderer | runner | command |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `exponential-ui` (Rust core) | `apps/desktop/crates/exponential-ui/tests/conformance.rs` | `cargo test -p exponential-ui --test conformance` |
|
|
61
|
+
| `@exponential-at/ui-react` | `packages/exponential-ui-react/browser/suites.test.ts` | `bun run --filter @exponential-at/ui-react test:conformance` (headless Chromium) |
|
|
62
|
+
| `@exponential-at/ui-solid` | `packages/exponential-ui-solid/browser/suites.test.ts` | `bun run --filter @exponential-at/ui-solid test:conformance` (headless Chromium) |
|
|
63
|
+
| `@exponential-at/ui-element` | `packages/exponential-ui-element/browser/suites.test.ts` | `bun run --filter @exponential-at/ui-element test:conformance` (headless Chromium) |
|
|
64
|
+
| `exponential-ui-gpui` | `apps/desktop/crates/exponential-ui-gpui/tests/suites.rs` | `cargo test -p exponential-ui-gpui --test suites` |
|
|
65
|
+
| `ExponentialUI` (SwiftUI) | `packages/exponential-ui-swift/Tests/ExponentialUITests/ConformanceTests.swift` | `swift test` |
|
|
66
|
+
| `at.exponential:ui-compose` | `packages/exponential-ui-compose/ui-compose/src/test/…/ConformanceTest.kt` | `./gradlew :ui-compose:testDebugUnitTest` |
|
|
67
|
+
|
|
68
|
+
CI (`.github/workflows/exponential-ui.yml`) runs all six renderers plus the
|
|
69
|
+
core on every SDK pull request and fails unless every report is conformant;
|
|
70
|
+
the two non-React web renderers (Solid, the web component) must also pass
|
|
71
|
+
the web-parity gate against React (below).
|
|
72
|
+
|
|
73
|
+
Version 2 (round 2, `docs/round-2-contract.md` §8) added the last nine
|
|
74
|
+
suites: round 1's bind/style/code fixtures every runner already replayed,
|
|
75
|
+
and the round-2 contract fixtures.
|
|
76
|
+
|
|
77
|
+
## With the real-font harness (VAPP-98, round 1)
|
|
78
|
+
|
|
79
|
+
Round 1 (#VAPP-98) adds the other half to this directory: `run.ts` dumps the
|
|
80
|
+
cases of `fixtures/conformance-cases.json` from the web and desktop renderers
|
|
81
|
+
with the conformance fonts and compares them with
|
|
82
|
+
`fixtures/conformance-baseline.json` (geometry with real text, plus a reported
|
|
83
|
+
pixel step). The two halves do not overlap: this manifest locks the CONTRACT
|
|
84
|
+
every renderer replays case by case (no fonts involved), the harness locks
|
|
85
|
+
what real text does to it. Once both are on master, the harness's comparison
|
|
86
|
+
becomes suite `real-font` here (cases = its matrix, a renderer passes when its
|
|
87
|
+
dump has no divergence beyond `conformance-known.json`), so
|
|
88
|
+
`conformance:check` stays the one verdict. Runner files are named `suites`
|
|
89
|
+
(`browser/suites.test.ts`, gpui `tests/suites.rs`) beside the harness's own
|
|
90
|
+
`conformance` ones.
|
|
91
|
+
|
|
92
|
+
### The matrix
|
|
93
|
+
|
|
94
|
+
Every renderer runs the FULL cross product of `conformance-cases.json`
|
|
95
|
+
(`allCases` in `dump.ts`; the Rust, Swift and Kotlin runners build the same
|
|
96
|
+
keys `<fixture>/<theme>/<mode>/<width>/<direction>`), so adding a fixture, a
|
|
97
|
+
theme, a mode or a width there widens every runner at once:
|
|
98
|
+
|
|
99
|
+
| axis | values |
|
|
100
|
+
|---|---|
|
|
101
|
+
| fixtures | `kitchen-sink` (every component), `responsive` (the kitchen sink's responsive section, `layout-geometry-round1.json`), `dashboard` (`dashboard.json` + `dashboard.data.json`: an agent project dashboard, VAPP-103) |
|
|
102
|
+
| themes | the built-ins: `exponential`, `neutral`, `playful`, `orange` |
|
|
103
|
+
| modes | `dark`, `light` (a mode changes colours only by contract; the light cases lock that no theme mode or recipe moves a box) |
|
|
104
|
+
| widths | 390, 600, 900, 1280 |
|
|
105
|
+
| directions | `ltr`, `rtl` |
|
|
106
|
+
|
|
107
|
+
The web dump of the whole matrix takes well under a minute; the Swift and
|
|
108
|
+
Compose ratchets a few seconds each, so the matrix is not sampled.
|
|
109
|
+
|
|
110
|
+
### Recording
|
|
111
|
+
|
|
112
|
+
| what | where | command |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| the web baseline | any machine with Chromium | `bun run --filter @exponential-at/ui conformance -- --write-baseline --skip-desktop` (whole matrix only) |
|
|
115
|
+
| the SwiftUI ratchet | macOS | `EXPONENTIAL_UI_WRITE_FIXTURES=1 swift test --filter RealFont` (`packages/exponential-ui-swift`) |
|
|
116
|
+
| the Compose ratchet | ubuntu (CI verifies there) | `.github/workflows/record-compose-fixtures.yml`, artifact `compose-fixtures` (`EXP_UI_WRITE_FIXTURES=1 ./gradlew :ui-compose:testDebugUnitTest --tests '*RealFontConformanceTest'`) |
|
|
117
|
+
| the gpui ratchet | Linux only (`tests/conformance.rs` builds there) | the same workflow's `gpui` job, artifact `gpui-ratchet` (`EXP_UI_WRITE_FIXTURES=1 cargo test -p exponential-ui-gpui --test conformance`) |
|
|
118
|
+
|
|
119
|
+
A new case without a budget fails every ratchet, so a matrix change re-records
|
|
120
|
+
all four in the same change.
|
|
121
|
+
|
|
122
|
+
Round 2: the dump measures LAYOUT boxes (transforms and animations off,
|
|
123
|
+
`LAYOUT_ONLY_CSS`), skips inactive carousel pages (`data-xui-inactive`),
|
|
124
|
+
and every painted part carries `data-xui-id="<node id>.<part>[.<index or
|
|
125
|
+
row key>]"` + `data-xui-part` (catalog/recipes.json lists the parts), so the
|
|
126
|
+
web and gpui dumps cover the same nodes. Nunito and Fira Code are real
|
|
127
|
+
faces (`fonts/`, SIL OFL 1.1). `conformance-known.json` carries the
|
|
128
|
+
divergence decisions (`causes`, `rules`) beside the ratchet's counts.
|
|
129
|
+
|
|
130
|
+
## Shared browser runner
|
|
131
|
+
|
|
132
|
+
Every WEB renderer runs the manifest through ONE runner,
|
|
133
|
+
[`browser-suites.ts`](browser-suites.ts) `runBrowserSuites(options)`: the
|
|
134
|
+
pure suites through the core and the shared web layer
|
|
135
|
+
(`@exponential-at/ui/web`), the painted ones in headless Chromium against the
|
|
136
|
+
renderer's harness ([`browser-page.ts`](browser-page.ts): `bunHarness`,
|
|
137
|
+
`openPage`, `EXPONENTIAL_UI_CHROME` = an explicit Chromium executable). A
|
|
138
|
+
renderer plugs in with:
|
|
139
|
+
|
|
140
|
+
- **a harness**: a `serve.ts` that honours `PORT` and answers `/` with a page
|
|
141
|
+
speaking the React harness's protocol (`?view=` kitchen-sink, geometry,
|
|
142
|
+
overlay, catalog, conditions, tree, controls, specimen, mount, bench,
|
|
143
|
+
round2; `theme`/`mode`/`rtl`/`width`/`echo`; the `window.__xui*` probes);
|
|
144
|
+
- **its logic**: `templateItems`, `itemInstance`, `runNodeAction` (the rest
|
|
145
|
+
is shared).
|
|
146
|
+
|
|
147
|
+
The function returns `{report, failures}` (no vitest in the core); the
|
|
148
|
+
renderer's `browser/suites.test.ts` writes the report and asserts
|
|
149
|
+
`checkReport`, as React's does in about 30 lines.
|
|
150
|
+
|
|
151
|
+
## Web parity
|
|
152
|
+
|
|
153
|
+
A separate, web-only gate (not a manifest suite: the natives keep the
|
|
154
|
+
manifest's list): [`parity.ts`](parity.ts) `runParity({reference,
|
|
155
|
+
candidate})` loads two web renderers side by side in one headless Chromium
|
|
156
|
+
(software raster, `--font-render-hinting=none`, the conformance fonts,
|
|
157
|
+
device scale 1, reduced motion, animations and transitions off) and compares
|
|
158
|
+
every case EXACTLY:
|
|
159
|
+
|
|
160
|
+
| check | what |
|
|
161
|
+
|---|---|
|
|
162
|
+
| dom | the `.xui-surface` subtrees (tag, attributes by name, text); generated ids (`useId`, Radix, Solid `cl-N`) in id attributes → `#id-N` in first-seen order |
|
|
163
|
+
| style | the full computed style of every `[data-xui-id]` element (and every overlay part) |
|
|
164
|
+
| frame | their `getBoundingClientRect`, 0 px tolerance |
|
|
165
|
+
| pixels | full-page screenshots, pixelmatch threshold 0 (AA excluded): 0 differing pixels; native `<video>`/`<audio>` boxes masked (the browser's own spinner) |
|
|
166
|
+
|
|
167
|
+
Cases (390): the matrix through `?view=tree` (192), every specimen at 900 px
|
|
168
|
+
(75), the control specimens with `data-xs` forced to hover, focus, pressed,
|
|
169
|
+
checked, open, disabled, invalid on every part (112), the overlay cases open
|
|
170
|
+
(11). The report ([`parity-report.schema.json`](parity-report.schema.json))
|
|
171
|
+
keeps the FIRST difference of each kind per case; differing screenshots land
|
|
172
|
+
in `.conformance/parity/`.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
bun run --filter @exponential-at/ui conformance:parity # React vs React, the self-check (~3 min)
|
|
176
|
+
bun run --filter @exponential-at/ui conformance:parity -- --candidate packages/exponential-ui-solid/harness/serve.ts
|
|
177
|
+
bun run --filter @exponential-at/ui conformance:parity -- --candidate packages/exponential-ui-element/harness/serve.ts
|
|
178
|
+
bun run --filter @exponential-at/ui parity:check .conformance/exponential-ui-parity-react-react.json
|
|
179
|
+
bun run --filter @exponential-at/ui-solid test:parity # Solid's gate calls runParity
|
|
180
|
+
bun run --filter @exponential-at/ui-element test:parity # the web component's gate, the same call
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Primitive cases (FEED-131, group `primitive`).** The same gate also
|
|
184
|
+
compares the PRIMITIVE sets: `@exponential-at/ui-react/primitives` (Radix +
|
|
185
|
+
`dist/exponential-ui-react.css`) against `@exponential-at/ui-solid/primitives`
|
|
186
|
+
(+ `dist/exponential-ui-solid.css`, compiled from the SAME `tailwind.css`,
|
|
187
|
+
byte-identical: `src/primitives/styles.test.ts`). Every row of
|
|
188
|
+
[`primitive-specimens.json`](../fixtures/primitive-specimens.json) runs ×
|
|
189
|
+
[`exponential` light, `orange` dark] (`primitive:<id>:<theme>-<mode>`, plus
|
|
190
|
+
`…:<hover|focus>` when the row names an `interact`: the first
|
|
191
|
+
`[data-xui-target]` is hovered / focused on both sides) through both
|
|
192
|
+
harnesses' `?view=primitives&id=&theme=&mode=&width=`
|
|
193
|
+
([`primitives-page.ts`](primitives-page.ts): `globalsCss` + the package's
|
|
194
|
+
own sheet in `<head>`, `class="dark"` on `<html>`, `data-xui-parity-root` +
|
|
195
|
+
`data-xui-mode` on `<body>`, the specimen in `<div data-xui-specimen
|
|
196
|
+
style="padding: 16px;">`). On such a page the probe compares the WHOLE
|
|
197
|
+
`<body>` (its attributes too: Radix's scroll lock writes them; `<script>`
|
|
198
|
+
and the gate's own styles skipped) and EVERY element under it, keyed by its
|
|
199
|
+
path (`body/div[0]/button[1]`). Specimens live in one module per lane,
|
|
200
|
+
`harness/primitives/{smoke,plain,controls,overlays,data,calendar}.tsx` in
|
|
201
|
+
BOTH packages (`SPECIMENS: Record<id, component>`); the jsdom twin is
|
|
202
|
+
`expectElementParity` (ui-solid `src/test/parity.ts`).
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
bun run --filter @exponential-at/ui-solid test:parity:primitives # the primitive group alone (seconds)
|
|
206
|
+
EXPONENTIAL_UI_PARITY_GROUP=matrix,overlay bun run --filter @exponential-at/ui-solid test:parity
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Listing
|
|
210
|
+
|
|
211
|
+
Once two platforms pass, the A2UI ecosystem's renderers page gets an entry
|
|
212
|
+
(a pull request to google/A2UI, a person's call: it is outward-facing):
|
|
213
|
+
|
|
214
|
+
> **Exponential UI** (React, SwiftUI, Jetpack Compose, gpui): native renderers
|
|
215
|
+
> of the A2UI v0.9 basic catalog plus an 81-component core catalog, runtime
|
|
216
|
+
> themes, extension catalogs and a host API (transports, functions, bindings,
|
|
217
|
+
> policy). Apache-2.0. https://ui.exponential.at
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The committed web BASELINE (fixtures/conformance-baseline.json): the React
|
|
2
|
+
// renderer's frame dump of the whole matrix, stored compactly — per fixture
|
|
3
|
+
// ONE node table (the union of every case's nodes: id, component, part,
|
|
4
|
+
// parent, text) and per case one frame row per placed node
|
|
5
|
+
// `[node, x, y, w, h, lines?, lh?]`, one row per line so a layout change
|
|
6
|
+
// diffs as the rows it moved. `decodeBaseline` gives the plain dump back;
|
|
7
|
+
// the Rust gate (tests/conformance.rs) decodes the same shape.
|
|
8
|
+
|
|
9
|
+
import { BASELINE_PATH, type CaseDump, type Dump, type DumpNode } from "./dump"
|
|
10
|
+
|
|
11
|
+
export const BASELINE_FILE = BASELINE_PATH
|
|
12
|
+
export const BASELINE_FORMAT = `xui-conformance-baseline/1`
|
|
13
|
+
|
|
14
|
+
type NodeRow = [id: string, component: string, part: string | null, parent: string | null, text: string | null]
|
|
15
|
+
type FrameRow = [node: number, x: number, y: number, w: number, h: number, lines?: number, lh?: number]
|
|
16
|
+
|
|
17
|
+
export interface Baseline {
|
|
18
|
+
$comment: string
|
|
19
|
+
format: string
|
|
20
|
+
renderer: string
|
|
21
|
+
fonts: string
|
|
22
|
+
/** How the baseline was produced (browser version, launch flags). */
|
|
23
|
+
producer: string
|
|
24
|
+
fixtures: Record<string, { nodes: NodeRow[] }>
|
|
25
|
+
cases: Record<string, { width: number; height: number; frames: FrameRow[] }>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fixtureOf = (key: string) => key.split(`/`)[0]
|
|
29
|
+
|
|
30
|
+
export function toBaseline(dump: Dump, producer: string): Baseline {
|
|
31
|
+
const fixtures: Baseline[`fixtures`] = {}
|
|
32
|
+
const index = new Map<string, Map<string, number>>()
|
|
33
|
+
const cases: Baseline[`cases`] = {}
|
|
34
|
+
for (const [key, c] of Object.entries(dump.cases)) {
|
|
35
|
+
const fx = fixtureOf(key)
|
|
36
|
+
fixtures[fx] ??= { nodes: [] }
|
|
37
|
+
const ids = index.get(fx) ?? new Map<string, number>()
|
|
38
|
+
index.set(fx, ids)
|
|
39
|
+
const frames: FrameRow[] = []
|
|
40
|
+
for (const n of c.nodes) {
|
|
41
|
+
const sig = JSON.stringify([n.id, n.component, n.part ?? null, n.parent ?? null, n.text ?? null])
|
|
42
|
+
let i = ids.get(sig)
|
|
43
|
+
if (i === undefined) {
|
|
44
|
+
i = fixtures[fx].nodes.length
|
|
45
|
+
fixtures[fx].nodes.push(JSON.parse(sig) as NodeRow)
|
|
46
|
+
ids.set(sig, i)
|
|
47
|
+
}
|
|
48
|
+
const row: FrameRow = [i, n.x, n.y, n.w, n.h]
|
|
49
|
+
if (n.lines !== undefined) row.push(n.lines, n.lh ?? 0)
|
|
50
|
+
frames.push(row)
|
|
51
|
+
}
|
|
52
|
+
cases[key] = { width: c.width, height: c.height, frames }
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
$comment: `GENERATED by \`bun run --filter @exponential-at/ui conformance -- --write-baseline\` (packages/exponential-ui/conformance): the React renderer's frame dump of fixtures/conformance-cases.json in headless Chromium with the conformance font set. Decoded by conformance/baseline.ts and the Rust gate. Never hand-edit.`,
|
|
56
|
+
format: BASELINE_FORMAT,
|
|
57
|
+
renderer: dump.renderer,
|
|
58
|
+
fonts: dump.fonts,
|
|
59
|
+
producer,
|
|
60
|
+
fixtures,
|
|
61
|
+
cases,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function decodeBaseline(b: Baseline): Dump {
|
|
66
|
+
if (b.format !== BASELINE_FORMAT) throw new Error(`baseline format ${b.format} ≠ ${BASELINE_FORMAT}`)
|
|
67
|
+
const cases: Record<string, CaseDump> = {}
|
|
68
|
+
for (const [key, c] of Object.entries(b.cases)) {
|
|
69
|
+
const table = b.fixtures[fixtureOf(key)].nodes
|
|
70
|
+
cases[key] = {
|
|
71
|
+
width: c.width,
|
|
72
|
+
height: c.height,
|
|
73
|
+
nodes: c.frames.map(([i, x, y, w, h, lines, lh]) => {
|
|
74
|
+
const [id, component, part, parent, text] = table[i]
|
|
75
|
+
const n: DumpNode = { id, component, x, y, w, h }
|
|
76
|
+
if (part !== null) n.part = part
|
|
77
|
+
if (parent !== null) n.parent = parent
|
|
78
|
+
if (text !== null) n.text = text
|
|
79
|
+
if (lines !== undefined) {
|
|
80
|
+
n.lines = lines
|
|
81
|
+
n.lh = lh
|
|
82
|
+
}
|
|
83
|
+
return n
|
|
84
|
+
}),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { format: `xui-frame-dump/1`, renderer: b.renderer, fonts: b.fonts, cases }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The baseline as committed: the envelope pretty, one node row / frame row per line. */
|
|
91
|
+
export function encodeBaseline(dump: Dump, producer: string): string {
|
|
92
|
+
const b = toBaseline(dump, producer)
|
|
93
|
+
const rows = (items: unknown[], indent: string) => (items.length ? `[\n${items.map((r) => `${indent} ${JSON.stringify(r)}`).join(`,\n`)}\n${indent}]` : `[]`)
|
|
94
|
+
const fixtures = Object.entries(b.fixtures)
|
|
95
|
+
.map(([k, f]) => ` ${JSON.stringify(k)}: { "nodes": ${rows(f.nodes, ` `)} }`)
|
|
96
|
+
.join(`,\n`)
|
|
97
|
+
const cases = Object.entries(b.cases)
|
|
98
|
+
.map(([k, c]) => ` ${JSON.stringify(k)}: { "width": ${c.width}, "height": ${c.height}, "frames": ${rows(c.frames, ` `)} }`)
|
|
99
|
+
.join(`,\n`)
|
|
100
|
+
return `{\n "$comment": ${JSON.stringify(b.$comment)},\n "format": ${JSON.stringify(b.format)},\n "renderer": ${JSON.stringify(b.renderer)},\n "fonts": ${JSON.stringify(b.fonts)},\n "producer": ${JSON.stringify(b.producer)},\n "fixtures": {\n${fixtures}\n },\n "cases": {\n${cases}\n }\n}\n`
|
|
101
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// FEED-128: the Playwright side of the shared WEB conformance tooling — one
|
|
2
|
+
// headless Chromium (`EXPONENTIAL_UI_CHROME` = an explicit executable, for a
|
|
3
|
+
// machine whose Playwright build is not installed), a renderer HARNESS served
|
|
4
|
+
// on a loopback port, and the page opener every painted suite uses. Node-safe
|
|
5
|
+
// (vitest runs the runners under Node): a harness is a `bun <serve.ts>`
|
|
6
|
+
// child process, never an in-process `Bun.build`.
|
|
7
|
+
//
|
|
8
|
+
// The harness protocol (packages/exponential-ui-react/harness/main.tsx is
|
|
9
|
+
// the reference): `<serve.ts>` honours `PORT`, answers `/` with a page whose
|
|
10
|
+
// module script is the bundle, and that page reads `?view=…` (kitchen-sink,
|
|
11
|
+
// geometry, overlay, catalog, conditions, tree, controls, specimen, mount,
|
|
12
|
+
// bench, round2, primitives = FEED-131's primitives-page.ts) plus
|
|
13
|
+
// theme/mode/rtl/width/echo and exposes the `window.__xui*` probes.
|
|
14
|
+
|
|
15
|
+
import { chromium, type Browser, type BrowserContext, type Page } from "playwright"
|
|
16
|
+
import { spawn, type ChildProcess } from "node:child_process"
|
|
17
|
+
import { createServer } from "node:net"
|
|
18
|
+
|
|
19
|
+
export type Query = Record<string, string | number | undefined>
|
|
20
|
+
|
|
21
|
+
/** A renderer's harness: `start()` serves its bundle on a loopback port. */
|
|
22
|
+
export interface WebHarness {
|
|
23
|
+
start(): Promise<RunningHarness>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RunningHarness {
|
|
27
|
+
baseUrl: string
|
|
28
|
+
stop(): void | Promise<void>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The Chromium executable override (`EXPONENTIAL_UI_CHROME`), else
|
|
32
|
+
* Playwright's own build. */
|
|
33
|
+
export const chromeExecutable = (): string | undefined => process.env.EXPONENTIAL_UI_CHROME || undefined
|
|
34
|
+
|
|
35
|
+
export async function launchChromium(args: string[] = [], executablePath = chromeExecutable()): Promise<Browser> {
|
|
36
|
+
return chromium.launch({ executablePath, args })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `${baseUrl}/?<query>` (undefined values dropped). */
|
|
40
|
+
export function pageUrl(baseUrl: string, query: Query): string {
|
|
41
|
+
const q = new URLSearchParams()
|
|
42
|
+
for (const [k, v] of Object.entries(query)) if (v !== undefined) q.set(k, String(v))
|
|
43
|
+
return `${baseUrl}/?${q.toString()}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A free loopback TCP port. */
|
|
47
|
+
export function freePort(): Promise<number> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const srv = createServer()
|
|
50
|
+
srv.unref()
|
|
51
|
+
srv.on(`error`, reject)
|
|
52
|
+
srv.listen(0, `127.0.0.1`, () => {
|
|
53
|
+
const addr = srv.address()
|
|
54
|
+
const port = typeof addr === `object` && addr ? addr.port : 0
|
|
55
|
+
srv.close(() => resolve(port))
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The harness of a `serve.ts` that honours `PORT`: spawned with `bun`,
|
|
61
|
+
* ready once `/` answers and the module script it names bundles (a 500 =
|
|
62
|
+
* the bundler's error, thrown). A child that dies before `stop()` (killed
|
|
63
|
+
* from outside on a busy machine) is respawned on the same port. */
|
|
64
|
+
export function bunHarness(serveScript: string, opts: { timeoutMs?: number } = {}): WebHarness {
|
|
65
|
+
return {
|
|
66
|
+
async start() {
|
|
67
|
+
const port = await freePort()
|
|
68
|
+
const baseUrl = `http://127.0.0.1:${port}`
|
|
69
|
+
let stderr = ``
|
|
70
|
+
let stopping = false
|
|
71
|
+
let proc: ChildProcess
|
|
72
|
+
const spawnChild = () => {
|
|
73
|
+
proc = spawn(`bun`, [serveScript], { env: { ...process.env, PORT: String(port) }, stdio: [`ignore`, `pipe`, `pipe`] })
|
|
74
|
+
proc.stderr?.on(`data`, (d) => (stderr += String(d)))
|
|
75
|
+
proc.on(`exit`, (code, signal) => {
|
|
76
|
+
if (stopping) return
|
|
77
|
+
stderr += `${serveScript} exited (${code ?? signal}); respawned\n`
|
|
78
|
+
spawnChild()
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
spawnChild()
|
|
82
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 60_000)
|
|
83
|
+
for (;;) {
|
|
84
|
+
let ready = false
|
|
85
|
+
try {
|
|
86
|
+
const page = await fetch(`${baseUrl}/`)
|
|
87
|
+
if (page.ok) {
|
|
88
|
+
const src = /<script[^>]*src="([^"]+)"/.exec(await page.text())?.[1] ?? `/dist/main.js`
|
|
89
|
+
const r = await fetch(new URL(src, baseUrl))
|
|
90
|
+
if (r.status === 500) {
|
|
91
|
+
stopping = true
|
|
92
|
+
proc!.kill()
|
|
93
|
+
throw new Error(`${serveScript}: bundle failed: ${await r.text()}`)
|
|
94
|
+
}
|
|
95
|
+
ready = r.ok
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (String(e).includes(`bundle failed`)) throw e
|
|
99
|
+
}
|
|
100
|
+
if (ready) break
|
|
101
|
+
if (Date.now() > deadline) {
|
|
102
|
+
stopping = true
|
|
103
|
+
proc!.kill()
|
|
104
|
+
throw new Error(`${serveScript} did not start on ${baseUrl}: ${stderr}`)
|
|
105
|
+
}
|
|
106
|
+
await new Promise((r) => setTimeout(r, 150))
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
baseUrl,
|
|
110
|
+
stop: () => {
|
|
111
|
+
stopping = true
|
|
112
|
+
proc.kill()
|
|
113
|
+
if (stderr.trim()) console.error(stderr)
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface OpenPageOptions {
|
|
121
|
+
viewport?: { width: number; height: number }
|
|
122
|
+
/** Wait for a `.xui-surface` (default true). */
|
|
123
|
+
waitForSurface?: boolean
|
|
124
|
+
/** Extra settle time after the surface attached, ms (default 250). */
|
|
125
|
+
settleMs?: number
|
|
126
|
+
context?: Parameters<Browser[`newContext`]>[0]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A fresh context + page on the harness at `query`; throws on page errors. */
|
|
130
|
+
export async function openPage(browser: Browser, baseUrl: string, query: Query, viewport = { width: 1200, height: 900 }, opts: Omit<OpenPageOptions, `viewport`> = {}): Promise<Page> {
|
|
131
|
+
const context: BrowserContext = await browser.newContext({ viewport, deviceScaleFactor: 1, ...opts.context })
|
|
132
|
+
const page = await context.newPage()
|
|
133
|
+
const errors: string[] = []
|
|
134
|
+
page.on(`pageerror`, (e) => errors.push(String(e)))
|
|
135
|
+
page.on(`console`, (m) => {
|
|
136
|
+
if (m.type() === `error`) errors.push(m.text())
|
|
137
|
+
})
|
|
138
|
+
await page.goto(pageUrl(baseUrl, query))
|
|
139
|
+
if (opts.waitForSurface !== false) await page.waitForSelector(`.xui-surface`, { timeout: 30_000, state: `attached` })
|
|
140
|
+
await page.waitForTimeout(opts.settleMs ?? 250)
|
|
141
|
+
if (errors.length) {
|
|
142
|
+
await context.close()
|
|
143
|
+
throw new Error(`page errors: ${errors.join(`\n`)}`)
|
|
144
|
+
}
|
|
145
|
+
return page
|
|
146
|
+
}
|