@ultimat3/testing 27.16.1 → 29.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/package.json +17 -17
- package/src/app-root.ts +13 -0
- package/src/preload.ts +16 -3
- package/src/registry-leak-guard.ts +7 -11
- package/src/render-view.ts +48 -2
- package/src/app-jsx-loader.ts +0 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/testing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "29.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",
|
|
@@ -29,27 +29,27 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"engines": {
|
|
32
|
-
"bun": ">=1.4.
|
|
32
|
+
"bun": ">=1.4.4"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
36
36
|
"test": "bun test"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ultimat3/action": "
|
|
40
|
-
"@ultimat3/cache": "
|
|
41
|
-
"@ultimat3/core": "
|
|
42
|
-
"@ultimat3/db": "
|
|
43
|
-
"@ultimat3/entity": "
|
|
44
|
-
"@ultimat3/http": "
|
|
45
|
-
"@ultimat3/i18n": "
|
|
46
|
-
"@ultimat3/jobs": "
|
|
47
|
-
"@ultimat3/mail": "
|
|
48
|
-
"@ultimat3/policy": "
|
|
49
|
-
"@ultimat3/pwa": "
|
|
50
|
-
"@ultimat3/query": "
|
|
51
|
-
"@ultimat3/realtime": "
|
|
52
|
-
"@ultimat3/render": "
|
|
53
|
-
"@ultimat3/time": "
|
|
39
|
+
"@ultimat3/action": "29.0.0",
|
|
40
|
+
"@ultimat3/cache": "29.0.0",
|
|
41
|
+
"@ultimat3/core": "29.0.0",
|
|
42
|
+
"@ultimat3/db": "29.0.0",
|
|
43
|
+
"@ultimat3/entity": "29.0.0",
|
|
44
|
+
"@ultimat3/http": "29.0.0",
|
|
45
|
+
"@ultimat3/i18n": "29.0.0",
|
|
46
|
+
"@ultimat3/jobs": "29.0.0",
|
|
47
|
+
"@ultimat3/mail": "29.0.0",
|
|
48
|
+
"@ultimat3/policy": "29.0.0",
|
|
49
|
+
"@ultimat3/pwa": "29.0.0",
|
|
50
|
+
"@ultimat3/query": "29.0.0",
|
|
51
|
+
"@ultimat3/realtime": "29.0.0",
|
|
52
|
+
"@ultimat3/render": "29.0.0",
|
|
53
|
+
"@ultimat3/time": "29.0.0"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/app-root.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Whether this test run's root is an APP — an `app.config.ts` beside it. The registry leak guard
|
|
2
|
+
// restores an app's lazily declared registries as a union, and the framework's own suites exactly.
|
|
3
|
+
|
|
4
|
+
// why: Bun ships no synchronous file-exists check; Bun.file().exists() is async and a preload
|
|
5
|
+
// decides this before any test file loads.
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
// why: Bun ships no path-join primitive.
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
export const APP_CONFIG = 'app.config.ts';
|
|
11
|
+
|
|
12
|
+
export const isAppRoot = (root: string = process.cwd()): boolean =>
|
|
13
|
+
existsSync(join(root, APP_CONFIG));
|
package/src/preload.ts
CHANGED
|
@@ -10,7 +10,6 @@ import './quiet-logs';
|
|
|
10
10
|
import { installDeterminism } from './determinism';
|
|
11
11
|
import { registerFrameworkFixtures } from './framework-fixtures';
|
|
12
12
|
import './matchers';
|
|
13
|
-
import { installAppJsxLoader } from './app-jsx-loader';
|
|
14
13
|
import { onFileBoundary } from './file-boundary';
|
|
15
14
|
import { ISOLATED_ENV, releasePluginsAfterIsolatedFile } from './isolated-plugins';
|
|
16
15
|
import { installPerTestReset } from './per-test-reset';
|
|
@@ -55,11 +54,18 @@ installPerTestReset();
|
|
|
55
54
|
// actor's bucket left it spent for the next file on the worker, so a "the 31st call is a 429" test
|
|
56
55
|
// was red whenever another file calling that action ran first. Each file starts with full buckets,
|
|
57
56
|
// the store back to one process' memory, as a file run alone would have it.
|
|
57
|
+
//
|
|
58
|
+
// And the LIFECYCLE, which is one process' too: a drain is terminal, so a file that drained the
|
|
59
|
+
// process — or that emitted a real SIGINT at it, as a signal test does — left every later file on
|
|
60
|
+
// the worker answering 503 X_DRAINING for a reason nothing in that file explains. Each file starts
|
|
61
|
+
// from a live lifecycle, as a file run alone would have it.
|
|
58
62
|
if (Bun.env[ISOLATED_ENV] !== '1') {
|
|
59
63
|
const { forgetHandedOutActionPaths } = await import('@ultimat3/action');
|
|
60
64
|
onFileBoundary(forgetHandedOutActionPaths);
|
|
61
65
|
const { resetRateLimitStore } = await import('@ultimat3/http');
|
|
62
66
|
onFileBoundary(resetRateLimitStore);
|
|
67
|
+
const { resetLifecycle } = await import('@ultimat3/core');
|
|
68
|
+
onFileBoundary(resetLifecycle);
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
// Isolated runs only (`x test` says so): Bun keeps every finished file alive while a plugin is
|
|
@@ -72,8 +78,15 @@ if (Bun.env[ISOLATED_ENV] !== '1') {
|
|
|
72
78
|
// 8 files, 1 -> 8 live global objects (this file's own leak fixture).
|
|
73
79
|
releasePluginsAfterIsolatedFile();
|
|
74
80
|
|
|
75
|
-
//
|
|
76
|
-
|
|
81
|
+
// Every `.tsx` compiles with the framework's JSX factory from the first file on, in every test
|
|
82
|
+
// process — an app root or any workspace below it. Bun's own compile of a `.tsx` is wrong on every
|
|
83
|
+
// Bun (`@ultimat3/render`'s `module-loader.ts`): through 1.4.2 a cached `React.createElement`
|
|
84
|
+
// (`React is not defined`, 18 notificado.co tests), from 1.4.3 a link-time `SyntaxError: Export
|
|
85
|
+
// named 'jsxDEV' not found` in any file that merely imports a component (issue #883). It was
|
|
86
|
+
// installed only where `app.config.ts` sat beside the run, so a workspace package's own tests
|
|
87
|
+
// needed a hand-written preload (castlefight.online, ai-maxxing).
|
|
88
|
+
const { installRenderLoader } = await import('@ultimat3/render/server');
|
|
89
|
+
installRenderLoader();
|
|
77
90
|
|
|
78
91
|
// Opt-out exists for one case: a test that deliberately exercises a real integration in a job the
|
|
79
92
|
// team runs on purpose. It is an env var, not an API, so it cannot be set from inside a test file.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { afterAll } from 'bun:test';
|
|
7
7
|
import { knownTags, registeredTiers } from '@ultimat3/cache';
|
|
8
|
-
import { isAppRoot } from './app-
|
|
8
|
+
import { isAppRoot } from './app-root';
|
|
9
9
|
import { RegistryLeakError } from './errors';
|
|
10
10
|
import { runBoundaryHooks, runFileBoundary } from './file-boundary';
|
|
11
11
|
import type { ProcessRegistrySnapshot } from './registry-snapshot';
|
|
@@ -196,16 +196,12 @@ export function installRegistryLeakGuard(): void {
|
|
|
196
196
|
name: 'ultimate-registry-leak-guard',
|
|
197
197
|
setup(build) {
|
|
198
198
|
// `.test.ts` only, never `.test.tsx`. A load handler that answered `loader: 'tsx'` would
|
|
199
|
-
// compile JSX with Bun's
|
|
200
|
-
//
|
|
201
|
-
// registered from the preload, it would shadow
|
|
202
|
-
// `.tsx`
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
// moved to `server.ts:15` in the 9.0.0 split, and this comment claimed otherwise until
|
|
206
|
-
// 2026-08-23. The `server` half is still the one that would have to be imported.) Zero
|
|
207
|
-
// `.test.tsx` files exist and the convention is `<file>.test.ts`, so the narrower filter
|
|
208
|
-
// costs nothing today; a `.test.tsx` added later is unguarded rather than mis-compiled.
|
|
199
|
+
// compile JSX with Bun's own transform — the one `@ultimat3/render`'s loader exists to
|
|
200
|
+
// replace, a link-time `jsxDEV` error from Bun 1.4.3 (issue #883) — and, because the first
|
|
201
|
+
// matching handler wins and this one is registered from the preload, it would shadow
|
|
202
|
+
// render's transform for that file. Zero `.test.tsx` files exist and the convention is
|
|
203
|
+
// `<file>.test.ts`, so the narrower filter costs nothing today; a `.test.tsx` added later is
|
|
204
|
+
// unguarded rather than mis-compiled.
|
|
209
205
|
build.onLoad({ filter: /\.test\.ts$/ }, async (args) => {
|
|
210
206
|
// Before the registry restore: an island disposed here still needs its fake DOM.
|
|
211
207
|
runFileBoundary();
|
package/src/render-view.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Actor } from '@ultimat3/core';
|
|
6
6
|
import { assert, ctxOf, runWithContext } from '@ultimat3/core';
|
|
7
|
-
import type { IslandDirective, RouteConfig, RouteParams } from '@ultimat3/render';
|
|
7
|
+
import type { AssetPath, IslandDirective, RouteConfig, RouteParams } from '@ultimat3/render';
|
|
8
8
|
|
|
9
9
|
/** What a component rendered. */
|
|
10
10
|
export interface RenderedView {
|
|
@@ -24,6 +24,16 @@ export interface RenderedRoute<TData> extends RenderedView {
|
|
|
24
24
|
readonly meta: Awaited<ReturnType<RouteConfig<TData>['meta']>>;
|
|
25
25
|
/** The islands this render emitted, in order. Empty on a page that ships no JavaScript. */
|
|
26
26
|
readonly islands: readonly IslandDirective[];
|
|
27
|
+
/**
|
|
28
|
+
* The app-wide favicon's URL, when one was declared and the app's site asset table answered:
|
|
29
|
+
* the content-hashed URL `/assets/*` serves — the same one the document's
|
|
30
|
+
* `<link rel="icon" type="image/svg+xml">` carries.
|
|
31
|
+
*
|
|
32
|
+
* `undefined` when the app declares no favicon, and `undefined` when the URL cannot be
|
|
33
|
+
* resolved: a unit test that never loaded the app has no site asset table installed, and a
|
|
34
|
+
* route still renders.
|
|
35
|
+
*/
|
|
36
|
+
readonly favicon?: string | undefined;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -42,6 +52,16 @@ export interface RouteRequest {
|
|
|
42
52
|
readonly params?: RouteParams;
|
|
43
53
|
/** Who is asking. `load` and the page run inside a request context holding this actor. */
|
|
44
54
|
readonly actor?: Actor;
|
|
55
|
+
/**
|
|
56
|
+
* The app's favicon — the app-wide declaration in `app.config.ts` (`site.favicon`), the one
|
|
57
|
+
* SVG every document of the app links. It is the APP's fact, not the route's: the route
|
|
58
|
+
* repeats nothing, and the render resolves it through the app's own site asset table so a
|
|
59
|
+
* test can see the URL `/assets/*` will serve.
|
|
60
|
+
*
|
|
61
|
+
* Optional: an app that declares no favicon, and a test that never loaded the app, both ask
|
|
62
|
+
* for a route with no favicon to resolve.
|
|
63
|
+
*/
|
|
64
|
+
readonly favicon?: AssetPath | undefined;
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
const DEFAULT_URL = 'http://localhost/';
|
|
@@ -64,6 +84,29 @@ const visibleText = (html: string): string =>
|
|
|
64
84
|
|
|
65
85
|
const viewOf = (html: string): RenderedView => ({ html, text: visibleText(html) });
|
|
66
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The favicon's URL through the app's own site asset table — `asset()`'s answer, the same URL
|
|
89
|
+
* the document's `<link rel="icon">` carries and `/assets/*` serves.
|
|
90
|
+
*
|
|
91
|
+
* `undefined` when the app declares no favicon, and `undefined` when the URL cannot be
|
|
92
|
+
* resolved: a unit test that never loaded the app has no site asset table installed, and
|
|
93
|
+
* `asset()` refuses by name there. A route render is not a document, so a favicon it cannot
|
|
94
|
+
* resolve is no reason to fail the render — the app's BOOT is where a declared favicon that is
|
|
95
|
+
* not on disk is refused (`X_ASSET_MISSING`), and a test asks about a route, not about the
|
|
96
|
+
* app's icon.
|
|
97
|
+
*/
|
|
98
|
+
const faviconUrl = async (
|
|
99
|
+
render: typeof import('@ultimat3/render'),
|
|
100
|
+
favicon: AssetPath | undefined,
|
|
101
|
+
): Promise<string | undefined> => {
|
|
102
|
+
if (favicon === undefined) return undefined;
|
|
103
|
+
try {
|
|
104
|
+
return render.asset(favicon);
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
67
110
|
/**
|
|
68
111
|
* Render one component with the props a page would hand it. The props are the component's own
|
|
69
112
|
* type, so a renamed prop is a compile error in the test rather than an `undefined` in the markup.
|
|
@@ -102,6 +145,9 @@ export async function renderRoute<TData>(
|
|
|
102
145
|
const url = request.url ?? DEFAULT_URL;
|
|
103
146
|
const ctx = { params: request.params ?? {}, url };
|
|
104
147
|
const islands = render.islandCollector({ file: url, hydrate: config.hydrate });
|
|
148
|
+
// The app's favicon, resolved through the app's own asset table — and skipped when there is
|
|
149
|
+
// no table to ask, so a route renders with no favicon setup at all.
|
|
150
|
+
const favicon = await faviconUrl(render, request.favicon);
|
|
105
151
|
|
|
106
152
|
const run = async (): Promise<RenderedRoute<TData>> => {
|
|
107
153
|
const data = await render.routeDataFor(config, ctx);
|
|
@@ -110,7 +156,7 @@ export async function renderRoute<TData>(
|
|
|
110
156
|
const html = await renderToHtml(component({ data, params: ctx.params, url, query }), {
|
|
111
157
|
islands,
|
|
112
158
|
});
|
|
113
|
-
return { ...viewOf(html), data, meta, islands: islands.directives };
|
|
159
|
+
return { ...viewOf(html), data, meta, islands: islands.directives, favicon };
|
|
114
160
|
};
|
|
115
161
|
|
|
116
162
|
if (request.actor === undefined) return run();
|
package/src/app-jsx-loader.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// An APP's tests compile every `.tsx` with the app's JSX factory from the first file on.
|
|
2
|
-
//
|
|
3
|
-
// Since 22.7 a worker runs many files in one module registry (no `--isolate`), so a `.tsx` module
|
|
4
|
-
// is compiled ONCE per worker and cached. `@ultimat3/render`'s loader is installed by importing
|
|
5
|
-
// `@ultimat3/render/server`; a file that reached a page component before any file had imported it
|
|
6
|
-
// cached the page compiled with Bun's classic factory, and every later file on that worker failed
|
|
7
|
-
// with `React is not defined` (18 notificado.co tests, measured). Installing the loader in the
|
|
8
|
-
// preload makes the first compile the right one.
|
|
9
|
-
//
|
|
10
|
-
// Only in an app (an `app.config.ts` at the run's root): the framework repository's own tests
|
|
11
|
-
// include suites that pin what a process WITHOUT the loader does, and they keep that process.
|
|
12
|
-
|
|
13
|
-
// why: Bun ships no synchronous file-exists check; Bun.file().exists() is async and a preload
|
|
14
|
-
// decides this before any test file loads.
|
|
15
|
-
import { existsSync } from 'node:fs';
|
|
16
|
-
// why: Bun ships no path-join primitive.
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
|
|
19
|
-
export const APP_CONFIG = 'app.config.ts';
|
|
20
|
-
|
|
21
|
-
export const isAppRoot = (root: string = process.cwd()): boolean =>
|
|
22
|
-
existsSync(join(root, APP_CONFIG));
|
|
23
|
-
|
|
24
|
-
/** Install the render loader when this run's root is an app. Returns whether it did. */
|
|
25
|
-
export async function installAppJsxLoader(root: string = process.cwd()): Promise<boolean> {
|
|
26
|
-
if (!isAppRoot(root)) return false;
|
|
27
|
-
const { installRenderLoader } = await import('@ultimat3/render/server');
|
|
28
|
-
installRenderLoader();
|
|
29
|
-
return true;
|
|
30
|
-
}
|