@ultimat3/testing 13.0.0 → 15.0.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/CLAUDE.md CHANGED
@@ -31,8 +31,11 @@ is its own entry point and not part of the barrel.
31
31
  | `test` here is `fixtureTest`, and it takes no timeout | `(name, body)`, nothing else (`fixtures.ts:248`, exported as `test` by `index.ts:107`) — `bunTest` is called with two arguments, so bun's own third one is unreachable and no case can ask for more than the default 5s. Slow work goes in `beforeAll(fn, 60_000)`, which is `bun:test`'s own re-export and does take one; that is where both generated island tests and `examples/dummy/apps/web/app/settings/settings.island.test.ts` build their chunk, once, for every case to share — a Babel pass plus a browser bundle is seconds. A case that needs 60s of its own is work sitting in the wrong place |
32
32
  | Injection | `SqlRunner` and `connect` are parameters, so unit tests need no server |
33
33
  | Fixtures | the preload registers the whole framework bag — an app registers only what the framework cannot know (`seed`, `actorFor`) |
34
- | e2e without a driver | `e2eTest` becomes `test.skip`, and the gate reports the step GREEN over it — `bun test` exits 0 on a skip and the exit code is the only channel between the step and the child that registers the driver. `hasE2eDriver()` is what a harness asks instead of reading an all-skipped run as a pass. Zero drivers are registered `As of 2026-08` |
34
+ | e2e without a driver | `e2eTest` becomes `test.skip`, and the gate reports the step GREEN over it — `bun test` exits 0 on a skip and the exit code is the only channel between the step and the child that registers the driver. `hasE2eDriver()` is what a harness asks instead of reading an all-skipped run as a pass. Zero drivers are registered BY DEFAULT and that is the design, not a gap: `@ultimat3/cli`'s `installE2eDriver()` is the one that exists (`packages/cli/src/e2e-driver.ts`), an app's test preload is what calls it, and CI has no browser |
35
+ | The seam has an inverse, `As of 2026-08-25` | `resetE2eDriver()`. `useE2eDriver` writes MODULE scope and `bun test` is one process, so a file that installed a browser handed every later file in the run an `e2eTest` that opened a page nobody asked for — `test-types.test.ts` was itself doing it, at module scope, with nothing to undo it. Same shape and same reason as `@ultimat3/scraping`'s `resetScrapeDriver()` |
35
36
  | Built vs declared | `clock` `mail` `network` `runJobs` `statements` `subscribe` are built in-process; `page` `budget` `signIn` `deploy` are declared and wait for a driver (`X_TEST_FIXTURE_UNAVAILABLE`). The four left all need a browser or a second build — things the framework genuinely cannot bundle |
37
+ | `page` HAS a driver now, and the other three still do not, `As of 2026-08-25` | `installE2eDriver({ page, baseUrl })` registers `page` over its declaration and leaves `budget`, `signIn` and `deploy` refusing, on purpose: byte counts come off a built `dist/`, a sign-in route is the APP's, and a new build id is a fact about the SERVER — a page port can answer for none of the three, and a fixture that silently no-opped would make the assertion after it read as proof |
38
+ | `network` is THIS process's fetch, and an e2e page is not in this process | `sealed-network.ts` patches `globalThis.fetch` here; a browser's requests never pass through it. So `network.offline()` in a test that also destructures `page` is a **no-op on the browser** — the app's online page passing an offline test. `E2eFixtures.offline()` is the browser-side spelling and the CDP driver REFUSES it (`X_TEST_FIXTURE_UNAVAILABLE`), because `CdpPageLike` declares no `setOfflineMode`. Two words, two mechanisms, and only one of them can put a browser offline |
36
39
  | `subscribe` is a whole `sync` node | `live-node.ts` assembles what `x dev --role sync` assembles minus the listener — real `LiveQueryRegistry`, real `liveQueryDefinition` bridge, real per-subscriber gate, real cursor — over a socket that is two objects handing each other the JSON a WebSocket would. `live-replicator.ts` feeds it from `@ultimat3/entity`'s `setRowObserver`, which is the change SOURCE a test process never had: PGlite has no walsender and the memory driver no log, so `InMemoryChangeFeed` had nothing upstream of it. The WAL decoder is the only thing substituted; everything downstream of it is production code |
37
40
  | What `subscribe` does NOT hold | a client store, an offline queue or a rebase log — so `feed.local()` answers `undefined` rather than the server row. A twin reported as applied whether or not a mutator ran is coverage that reads as proof, which is worse than none. That half is `useMutation` / `useMutationQueue` and an e2e |
38
41
  | Draining is a macrotask yield | the node dispatches `message` into a floating async task, so `settled()` yields with `setImmediate` until the frame count stops moving. Counting microtask turns is a number that is right until someone adds an `await` — the first version drained 32 and read `examples/dummy`'s deeper feed read as "the node answered nothing" |
@@ -65,6 +68,7 @@ is its own entry point and not part of the barrel.
65
68
  | Factory seeds | derived from the table name unless given, so two entities never draw the same uuid stream. `reset()` cascades into associated parents — a half-reset row is worse than none |
66
69
  | Shared examples | `behavesLike` calls `describe`, so it goes at declaration scope; bun rejects a `describe` inside a test body |
67
70
  | An island needs a BUILDER, not an import | `buildIslands` is `@ultimat3/cli`'s and both packages are tier 5; the one declared edge is `cli → testing`, so the reverse is a `bun run boundaries` failure. `mountIsland({ build, root, file })` takes the function as a parameter and declares only the two fields it reads — `IslandChunkLike` is `{ file, code }`, so a CSS artifact, a source map or a dev/production flag the bundler grows is invisible here. Moving the bundler down a tier was the alternative and it drags `@babel/core` and `babel-preset-solid` with it, into a package whose whole point is being importable from tier 0 |
71
+ | `mountIsland` AWAITS `mount` | `IslandEntry['mount']` returns `unknown`, not `void`, and the call is awaited — the shipped runtime chains `import(e).then((m) => m.mount(el, props))` (`packages/render/src/hydrate.ts`) and only marks the element mounted when that settles, so an `async` mount is an ordinary island and `like.island.tsx` already is one. Typed `=> void` and called bare, the fixture returned before an island that opens a queue or a socket had rendered anything, so every assertion after it read an empty wrapper — and worse, the mount RESUMED after `restore()` had taken the fake `document` back out, failing with `document is not defined` inside whichever later test happened to be running. Not a breaking change: `IslandEntry` is module-private and an island module is matched structurally off an `unknown` import, so nothing implements it. |
68
72
  | The micro-DOM is the fixture's, once **for islands** | `island-dom.ts`. **"Once" is scoped to this job, and said so only from 2026-08-23**: `packages/ui/src/fake-dom.ts` is a second micro-DOM (201 lines, test-only, off that package's barrel) for a different one — focus, `activeElement`, `contains` and a `:not()` selector grammar for keyboard code, none of which parses a `<template>`. It is not a copy to collapse: `ui` is tier 4 and this package is tier 5, so `ui -> testing` is an upward import the boundary check refuses, and the merge would need the shared half moved down to a tier neither grammar belongs in. `bun test` has no DOM and no DOM library may be added; `generate: 'dom'` builds every element from `_$template("<label …>")`, so a stub without a parsed `<template>` cannot run one line of a compiled island. It lived twice, ~200 lines each, in `packages/cli/src/island-bundle.test.ts` and the reference app's island test |
69
73
  | `style` and `classList` RECORD, `As of 2026-08` | `FakeStyle` is one declaration map behind all four spellings compiled Solid uses on one element: a STATIC entry baked into the template's `style=` attribute, a dynamic one through `setStyleProperty` → `style.setProperty`, a whole-object or string prop through `style()` → `cssText`, and a cleared one through `removeAttribute`. It was `style = {}` until 2026-08-21 — `<Form>`, `<Stack>`, `<Grid>` and `<Container>` each set a CSS custom property, so every one of them died inside `mount` with `e.style.setProperty is not a function`, and `x g resource` emitted a plain `<form>` rather than the design system's. A design-system component kept out of generated code by the limits of a TEST DOUBLE. A no-op `setProperty` would have stopped the crash and left "the component set `--form-gap`" unassertable, which is the same hole one layer down |
70
74
  | `classList` is the class attribute | not a list of its own, so `classList.toggle` — what the compiler emits INLINE for `classList={{ … }}`, with no runtime helper in front of it — and `className` can never answer one element two ways. `add` was the only method the stand-in had and is the one Solid never calls |
package/README.md CHANGED
@@ -136,9 +136,19 @@ there is no flag on the fixture and no code to silence.
136
136
  | `contractTest` | OpenAPI diff vs the committed spec, MCP exposure | `contract` |
137
137
  | `liveTest` | exactly what each subscriber receives | `live` |
138
138
  | `jobTest` | step sequence, retries, idempotency | `job` |
139
- | `e2eTest` | a browser driver incl. offline mode + SW update; with none registered it SKIPS, and the gate's `e2e` step passes over the skip — ask `hasE2eDriver()` rather than reading that as a pass | `e2e` |
139
+ | `e2eTest` | a browser driver; with none registered it SKIPS, and the gate's `e2e` step passes over the skip — ask `hasE2eDriver()` rather than reading that as a pass | `e2e` |
140
140
  | `evalTest` | LLM output scoring against a threshold | `eval` |
141
141
 
142
+ **Registering one, `As of 2026-08-25`.** `@ultimat3/cli`'s `installE2eDriver({ page, baseUrl })` is
143
+ the driver that exists — a `PageLike` over `@ultimat3/scraping`'s browser — and an app's test preload
144
+ is what calls it. It returns the undo, and `resetE2eDriver()` is the seam's inverse for anything that
145
+ installs one by hand: `bun test` is one process, so a driver left registered reaches every later file.
146
+
147
+ It registers `page` and nothing else. `budget`, `signIn` and `deploy` keep refusing with
148
+ `X_TEST_FIXTURE_UNAVAILABLE`, and so do `E2eFixtures`' `offline()` / `online()` / `update()` — the
149
+ browser's own network state and a second build id are not things a page port can answer for, and a
150
+ fixture that silently no-opped would make the assertion after it read as proof.
151
+
142
152
  Each helper prefixes the test name with its type (`job · onboards an org`), which is what
143
153
  `bun test --test-name-pattern "job · "` selects — the six lines of `x verify` come from the tests
144
154
  themselves, not from a directory convention.
@@ -281,6 +291,12 @@ line and makes the direction visible instead of hidden.
281
291
  **`fire` answers whether a handler RAN.** A selector that matches nothing and an island that
282
292
  attached no handler are the same silence; the second is a bug and the first is a typo in the test.
283
293
 
294
+ **An `async` mount is awaited**, as the shipped hydration runtime awaits it — an island that opens
295
+ a queue or a socket before its first render (`like.island.tsx` does) needs no `settle()` helper in
296
+ the test. Until `As of 2026-08-25` the call was bare, so the fixture answered before such an island
297
+ had rendered anything and the mount later resumed against a `document` the teardown had already
298
+ removed.
299
+
284
300
  **A mount installs process-global state**, so `MountedIsland` is `Disposable` — `using`, or
285
301
  `island[Symbol.dispose]()` in an `afterAll`. Left installed it hands a fake `document` to every
286
302
  later FILE in the run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/testing",
3
- "version": "13.0.0",
3
+ "version": "15.0.0",
4
4
  "description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,16 +33,16 @@
33
33
  "test": "bun test"
34
34
  },
35
35
  "dependencies": {
36
- "@ultimat3/cache": "13.0.0",
37
- "@ultimat3/core": "13.0.0",
38
- "@ultimat3/db": "13.0.0",
39
- "@ultimat3/entity": "13.0.0",
40
- "@ultimat3/i18n": "13.0.0",
41
- "@ultimat3/jobs": "13.0.0",
42
- "@ultimat3/mail": "13.0.0",
43
- "@ultimat3/policy": "13.0.0",
44
- "@ultimat3/query": "13.0.0",
45
- "@ultimat3/realtime": "13.0.0",
46
- "@ultimat3/time": "13.0.0"
36
+ "@ultimat3/cache": "15.0.0",
37
+ "@ultimat3/core": "15.0.0",
38
+ "@ultimat3/db": "15.0.0",
39
+ "@ultimat3/entity": "15.0.0",
40
+ "@ultimat3/i18n": "15.0.0",
41
+ "@ultimat3/jobs": "15.0.0",
42
+ "@ultimat3/mail": "15.0.0",
43
+ "@ultimat3/policy": "15.0.0",
44
+ "@ultimat3/query": "15.0.0",
45
+ "@ultimat3/realtime": "15.0.0",
46
+ "@ultimat3/time": "15.0.0"
47
47
  }
48
48
  }
@@ -62,8 +62,16 @@ export interface MountedIsland extends Disposable {
62
62
  ): boolean;
63
63
  }
64
64
 
65
+ /**
66
+ * Module-private, and its `mount` returns `unknown` rather than `void` — the shipped hydration
67
+ * runtime chains `import(e).then((m) => m.mount(el, props))` (`packages/render/src/hydrate.ts`),
68
+ * so a browser AWAITS whatever `mount` answers before it marks the element mounted. An island that
69
+ * opens a queue or a socket first is therefore an ordinary island (`like.island.tsx` is `async`),
70
+ * and a fixture typed `=> void` could only ever call it and walk away. Widening, not narrowing: an
71
+ * island module is matched structurally off an `unknown` import, so nothing implements this type.
72
+ */
65
73
  interface IslandEntry {
66
- readonly mount: (el: unknown, props: unknown) => void;
74
+ readonly mount: (el: unknown, props: unknown) => unknown;
67
75
  }
68
76
 
69
77
  /** `unknown` + a check, not a cast: a chunk whose `mount` was renamed is a real authoring mistake
@@ -180,7 +188,11 @@ export async function mountIsland(options: MountIslandOptions): Promise<MountedI
180
188
  if (options.shell !== undefined) {
181
189
  for (const child of [...parseHtml(options.shell).children]) el.appendChild(child);
182
190
  }
183
- entry.mount(el, options.props);
191
+ // AWAITED, because the runtime this fixture stands in for awaits it. Left unawaited, an async
192
+ // island's `mount` resumed AFTER the `restore()` below had taken the fake `document` back out
193
+ // — so it failed with `document is not defined` inside whichever later test happened to be
194
+ // running, with no thread back here.
195
+ await entry.mount(el, options.props);
184
196
  return {
185
197
  code: chunk.code,
186
198
  el,
package/src/index.ts CHANGED
@@ -252,6 +252,7 @@ export {
252
252
  hasE2eDriver,
253
253
  jobTest,
254
254
  liveTest,
255
+ resetE2eDriver,
255
256
  SEPARATOR,
256
257
  TEST_TYPES,
257
258
  testName,
package/src/test-types.ts CHANGED
@@ -112,6 +112,16 @@ export function useE2eDriver(driver: (name: string, body: E2eBody) => void): voi
112
112
  * instead of reading an all-skipped run as a green one. */
113
113
  export const hasE2eDriver = (): boolean => e2eDriver !== undefined;
114
114
 
115
+ /**
116
+ * Put the seam back. The counterpart `useE2eDriver` shipped without, and the module scope it
117
+ * writes to is process-global: `bun test` shares one process across files, so a file that installs
118
+ * a browser and does not undo it hands every later file an `e2eTest` that opens a page nobody
119
+ * asked for. Same shape and same reason as `@ultimat3/scraping`'s `resetScrapeDriver()`.
120
+ */
121
+ export const resetE2eDriver = (): void => {
122
+ e2eDriver = undefined;
123
+ };
124
+
115
125
  export const e2eTest = (name: string, body: E2eBody): void => {
116
126
  if (e2eDriver === undefined) {
117
127
  test.skip(