@ultimat3/testing 19.3.1 → 19.3.3
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 +1 -0
- package/README.md +11 -0
- package/package.json +12 -12
- package/src/errors.ts +1 -1
- package/src/fixture-island.ts +35 -2
package/CLAUDE.md
CHANGED
|
@@ -71,6 +71,7 @@ is its own entry point and not part of the barrel.
|
|
|
71
71
|
| Shared examples | `behavesLike` calls `describe`, so it goes at declaration scope; bun rejects a `describe` inside a test body |
|
|
72
72
|
| 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 |
|
|
73
73
|
| `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. |
|
|
74
|
+
| Dispose STOPS the island, then restores the globals | `mountIsland` keeps what `mount` resolved to and, when it is a function, calls it in `[Symbol.dispose]` BEFORE `restore()` — the island's disposer clears an interval whose callback reads `document`, or removes a listener from it, and must find the `document` it was made against. Solid's `render` returns a disposer, so `return render(…)` is an island's whole side of it (`X_TEST_ISLAND_NO_MOUNT`'s fix line and `x g island`'s template both say so). Restoring the globals was the whole teardown until 2026-09-07, and an island that polled on an interval kept ticking after the DOM was gone: measured in ai-maxxing as `document is not defined` thrown into whichever later test was running and as one file's fetch stub receiving another file's POSTs. Once — `using` and an `afterAll` that also disposes by hand are both real — and a disposer that throws still hands the globals back in a `finally`; the throw is the test's to see |
|
|
74
75
|
| 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 |
|
|
75
76
|
| `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 |
|
|
76
77
|
| `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
|
@@ -303,6 +303,17 @@ removed.
|
|
|
303
303
|
`island[Symbol.dispose]()` in an `afterAll`. Left installed it hands a fake `document` to every
|
|
304
304
|
later FILE in the run.
|
|
305
305
|
|
|
306
|
+
**Dispose also stops the island** (`As of 2026-09-07`): when `mount` returns a function, that is
|
|
307
|
+
the island's disposer and dispose calls it — before the globals go back, so a disposer that
|
|
308
|
+
removes a listener from `document` or clears an interval whose callback reads it finds the same
|
|
309
|
+
`document` `mount` ran under. Solid's `render` already returns exactly that, so an island's whole
|
|
310
|
+
side of the contract is `return render(() => <Counter {...props} />, el)` — and a disposed root
|
|
311
|
+
runs every `onCleanup` inside it. Until this landed, restoring the globals was the whole of the
|
|
312
|
+
teardown: an island whose `mount` polled on an interval kept ticking after the DOM was gone,
|
|
313
|
+
which surfaced as `document is not defined` thrown into whichever later test happened to be
|
|
314
|
+
running, and as one file's fetch stub receiving POSTs from another file's island. A `mount` that
|
|
315
|
+
returns nothing disposes as it always did.
|
|
316
|
+
|
|
306
317
|
### Selectors
|
|
307
318
|
|
|
308
319
|
`find`, `all`, `text`, `fire`, `resize`, `scroll` and `observing` take one grammar, `As of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/testing",
|
|
3
|
-
"version": "19.3.
|
|
3
|
+
"version": "19.3.3",
|
|
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": "19.3.
|
|
37
|
-
"@ultimat3/core": "19.3.
|
|
38
|
-
"@ultimat3/db": "19.3.
|
|
39
|
-
"@ultimat3/entity": "19.3.
|
|
40
|
-
"@ultimat3/i18n": "19.3.
|
|
41
|
-
"@ultimat3/jobs": "19.3.
|
|
42
|
-
"@ultimat3/mail": "19.3.
|
|
43
|
-
"@ultimat3/policy": "19.3.
|
|
44
|
-
"@ultimat3/query": "19.3.
|
|
45
|
-
"@ultimat3/realtime": "19.3.
|
|
46
|
-
"@ultimat3/time": "19.3.
|
|
36
|
+
"@ultimat3/cache": "19.3.3",
|
|
37
|
+
"@ultimat3/core": "19.3.3",
|
|
38
|
+
"@ultimat3/db": "19.3.3",
|
|
39
|
+
"@ultimat3/entity": "19.3.3",
|
|
40
|
+
"@ultimat3/i18n": "19.3.3",
|
|
41
|
+
"@ultimat3/jobs": "19.3.3",
|
|
42
|
+
"@ultimat3/mail": "19.3.3",
|
|
43
|
+
"@ultimat3/policy": "19.3.3",
|
|
44
|
+
"@ultimat3/query": "19.3.3",
|
|
45
|
+
"@ultimat3/realtime": "19.3.3",
|
|
46
|
+
"@ultimat3/time": "19.3.3"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/errors.ts
CHANGED
|
@@ -466,7 +466,7 @@ export class IslandMountMissingError extends UltimateError {
|
|
|
466
466
|
fix:
|
|
467
467
|
component === undefined
|
|
468
468
|
? `${file} exports nothing a mount could render — x g island <name> --at apps/web/site writes an island whose mount() is already there, and its shape is the one to copy`
|
|
469
|
-
: `in ${file} add: import { render } from 'solid-js/web'; export function mount(el: HTMLElement, props: Parameters<typeof ${component}>[0]): void { el.textContent = ''; render(() => <${component} {...props} />, el); }`,
|
|
469
|
+
: `in ${file} add: import { render } from 'solid-js/web'; export function mount(el: HTMLElement, props: Parameters<typeof ${component}>[0]): () => void { el.textContent = ''; return render(() => <${component} {...props} />, el); }`,
|
|
470
470
|
});
|
|
471
471
|
}
|
|
472
472
|
}
|
package/src/fixture-island.ts
CHANGED
|
@@ -57,6 +57,10 @@ export interface MountIslandOptions {
|
|
|
57
57
|
readonly size?: ResizeInput;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* `Disposable`: dispose calls the disposer the island's `mount` returned (Solid's `render` answers
|
|
62
|
+
* one — `return render(…)`), then hands the process its globals back. Both, in that order.
|
|
63
|
+
*/
|
|
60
64
|
export interface MountedIsland extends Disposable {
|
|
61
65
|
/** The chunk's source, for asserting on what did or did not reach the browser. */
|
|
62
66
|
readonly code: string;
|
|
@@ -101,11 +105,19 @@ export interface MountedIsland extends Disposable {
|
|
|
101
105
|
* opens a queue or a socket first is therefore an ordinary island (`like.island.tsx` is `async`),
|
|
102
106
|
* and a fixture typed `=> void` could only ever call it and walk away. Widening, not narrowing: an
|
|
103
107
|
* island module is matched structurally off an `unknown` import, so nothing implements this type.
|
|
108
|
+
*
|
|
109
|
+
* What `mount` answers, once awaited, is the island's DISPOSER when it is a function — the one
|
|
110
|
+
* way an island hands back what it started, and Solid's `render` already returns exactly that, so
|
|
111
|
+
* `return render(…)` is the whole of an island's side. The fixture calls it on dispose.
|
|
104
112
|
*/
|
|
105
113
|
interface IslandEntry {
|
|
106
114
|
readonly mount: (el: unknown, props: unknown) => unknown;
|
|
107
115
|
}
|
|
108
116
|
|
|
117
|
+
/** The island's disposer, when `mount` answered one — anything else is an island with nothing to stop. */
|
|
118
|
+
const disposerOf = (returned: unknown): (() => void) | undefined =>
|
|
119
|
+
typeof returned === 'function' ? (returned as () => void) : undefined;
|
|
120
|
+
|
|
109
121
|
/** `unknown` + a check, not a cast: a chunk whose `mount` was renamed is a real authoring mistake
|
|
110
122
|
* and `entry.mount is not a function` names neither the file nor the export it wanted. */
|
|
111
123
|
function entryOf(module: unknown, file: string): IslandEntry {
|
|
@@ -198,6 +210,13 @@ function installGlobals(values: Readonly<Record<string, unknown>>): () => void {
|
|
|
198
210
|
* The island fixture. Everything it installs is process-global, so the result is `Disposable` and
|
|
199
211
|
* the idiom is `using mounted = await mountIsland(…)` — a mount left installed hands a fake
|
|
200
212
|
* `document` to every later FILE in the run.
|
|
213
|
+
*
|
|
214
|
+
* Dispose is two things in one order: the island's own disposer first, then the globals back.
|
|
215
|
+
* Restoring the globals was the whole of it until 2026-09-07, so an island whose `mount` started
|
|
216
|
+
* an interval kept ticking after the DOM was gone — measured as `document is not defined` thrown
|
|
217
|
+
* into whichever later test was running, and as one file's fetch stub receiving POSTs from another
|
|
218
|
+
* file's island. The disposer runs while the fake `document` is still installed, because what it
|
|
219
|
+
* clears (a listener on `document`, an interval whose callback reads it) was made against that one.
|
|
201
220
|
*/
|
|
202
221
|
export async function mountIsland(options: MountIslandOptions): Promise<MountedIsland> {
|
|
203
222
|
// `only`, because this fixture has always KNOWN which island it wants and asked for all of them
|
|
@@ -240,7 +259,21 @@ export async function mountIsland(options: MountIslandOptions): Promise<MountedI
|
|
|
240
259
|
// island's `mount` resumed AFTER the `restore()` below had taken the fake `document` back out
|
|
241
260
|
// — so it failed with `document is not defined` inside whichever later test happened to be
|
|
242
261
|
// running, with no thread back here.
|
|
243
|
-
await entry.mount(el, options.props);
|
|
262
|
+
const unmount = disposerOf(await entry.mount(el, options.props));
|
|
263
|
+
let disposed = false;
|
|
264
|
+
const dispose = (): void => {
|
|
265
|
+
// Once: `using` and an `afterAll` that also disposes by hand are both real, and Solid's own
|
|
266
|
+
// disposer is idempotent while an island's `clearInterval` wrapper need not be.
|
|
267
|
+
if (disposed) return;
|
|
268
|
+
disposed = true;
|
|
269
|
+
try {
|
|
270
|
+
unmount?.();
|
|
271
|
+
} finally {
|
|
272
|
+
// Whatever the disposer did — including throw, which is the test's to see — the process
|
|
273
|
+
// gets its globals back, or the fake `document` reaches every later file in the run.
|
|
274
|
+
restore();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
244
277
|
const resolve = (target: string | FakeElement | null | undefined): FakeElement | null =>
|
|
245
278
|
typeof target === 'string' ? el.querySelector(target) : (target ?? null);
|
|
246
279
|
return {
|
|
@@ -271,7 +304,7 @@ export async function mountIsland(options: MountIslandOptions): Promise<MountedI
|
|
|
271
304
|
const node = resolve(target);
|
|
272
305
|
return node !== null && [...resizeObservers].some((each) => each.targets.has(node));
|
|
273
306
|
},
|
|
274
|
-
[Symbol.dispose]:
|
|
307
|
+
[Symbol.dispose]: dispose,
|
|
275
308
|
};
|
|
276
309
|
} catch (error) {
|
|
277
310
|
// A mount that throws restores the process before it rethrows: the alternative leaves every
|