@ultimat3/testing 27.12.0 → 27.13.1
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/README.md +7 -2
- package/package.json +16 -16
- package/src/cdp-e2e-session.ts +12 -2
- package/src/cdp-network-probe.ts +80 -0
- package/src/e2e-driver.ts +2 -1
- package/src/e2e-page.ts +43 -6
- package/src/errors.ts +2 -0
- package/src/fixtures.ts +8 -2
- package/src/index.ts +1 -0
- package/src/test-pace.ts +36 -0
- package/src/test-types.ts +11 -3
package/README.md
CHANGED
|
@@ -205,8 +205,13 @@ there is no flag on the fixture and no code to silence.
|
|
|
205
205
|
|
|
206
206
|
Each takes an optional `{ timeoutMs }` after the body — `unitTest(name, body, { timeoutMs: 30_000 })`,
|
|
207
207
|
and `test` takes the same; `evalTest` reads it off its options — handed to `bun:test` as that
|
|
208
|
-
test's own deadline (`TestOptions`). Absent is Bun's 5 s default
|
|
209
|
-
|
|
208
|
+
test's own deadline (`TestOptions`), at this machine's pace. Absent is Bun's 5 s default, which
|
|
209
|
+
`x verify` paces the same way. An `E2eDriver` receives it as its third argument.
|
|
210
|
+
|
|
211
|
+
**`paced(ms)`** is a declared deadline at this machine's pace, for a bare `test(…, paced(90_000))`:
|
|
212
|
+
`ms` times `ULTIMATE_TEST_PACE`, which `x verify` measures and sets on every test run it starts —
|
|
213
|
+
1 on a 4-core CI runner, more on a 2-core or slower box. Absent (a hand-run `bun test`) is 1; a
|
|
214
|
+
value that is not a number at or above 1 is `X_TEST_PACE_INVALID`. `unitTest`, `contractTest`, `liveTest` and `jobTest` hand their body the
|
|
210
215
|
[fixture bag](#fixtures); `e2eTest` hands its own (`E2eFixtures`).
|
|
211
216
|
|
|
212
217
|
**Registering one, `As of 2026-08-25`.** `@ultimat3/cli`'s `installE2eDriver({ page, baseUrl })` is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/testing",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.13.1",
|
|
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",
|
|
@@ -36,20 +36,20 @@
|
|
|
36
36
|
"test": "bun test"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ultimat3/action": "27.
|
|
40
|
-
"@ultimat3/cache": "27.
|
|
41
|
-
"@ultimat3/core": "27.
|
|
42
|
-
"@ultimat3/db": "27.
|
|
43
|
-
"@ultimat3/entity": "27.
|
|
44
|
-
"@ultimat3/http": "27.
|
|
45
|
-
"@ultimat3/i18n": "27.
|
|
46
|
-
"@ultimat3/jobs": "27.
|
|
47
|
-
"@ultimat3/mail": "27.
|
|
48
|
-
"@ultimat3/policy": "27.
|
|
49
|
-
"@ultimat3/pwa": "27.
|
|
50
|
-
"@ultimat3/query": "27.
|
|
51
|
-
"@ultimat3/realtime": "27.
|
|
52
|
-
"@ultimat3/render": "27.
|
|
53
|
-
"@ultimat3/time": "27.
|
|
39
|
+
"@ultimat3/action": "27.13.1",
|
|
40
|
+
"@ultimat3/cache": "27.13.1",
|
|
41
|
+
"@ultimat3/core": "27.13.1",
|
|
42
|
+
"@ultimat3/db": "27.13.1",
|
|
43
|
+
"@ultimat3/entity": "27.13.1",
|
|
44
|
+
"@ultimat3/http": "27.13.1",
|
|
45
|
+
"@ultimat3/i18n": "27.13.1",
|
|
46
|
+
"@ultimat3/jobs": "27.13.1",
|
|
47
|
+
"@ultimat3/mail": "27.13.1",
|
|
48
|
+
"@ultimat3/policy": "27.13.1",
|
|
49
|
+
"@ultimat3/pwa": "27.13.1",
|
|
50
|
+
"@ultimat3/query": "27.13.1",
|
|
51
|
+
"@ultimat3/realtime": "27.13.1",
|
|
52
|
+
"@ultimat3/render": "27.13.1",
|
|
53
|
+
"@ultimat3/time": "27.13.1"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/cdp-e2e-session.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { CdpConnection } from './cdp-connection';
|
|
|
7
7
|
import type { E2eTab } from './cdp-e2e-page';
|
|
8
8
|
import { cdpE2eTab } from './cdp-e2e-page';
|
|
9
9
|
import { CdpCallFailedError, CdpTimeoutError } from './cdp-errors';
|
|
10
|
+
import { isNetworkProbe, networkAgrees } from './cdp-network-probe';
|
|
10
11
|
import { offlineScripts } from './cdp-offline-script';
|
|
11
12
|
|
|
12
13
|
export interface E2eSession {
|
|
@@ -92,6 +93,8 @@ export async function cdpE2eSession(options: CdpE2eSessionOptions): Promise<E2eS
|
|
|
92
93
|
});
|
|
93
94
|
connection.on('Network.requestWillBeSent', (params) => {
|
|
94
95
|
const request = params['request'];
|
|
96
|
+
// The switch's own proof, not the app's traffic: a test that counts requests never sees it.
|
|
97
|
+
if (isNetworkProbe(field(request, 'url') ?? '')) return;
|
|
95
98
|
requests.push(`${field(request, 'method') ?? '?'} ${field(request, 'url') ?? ''}`);
|
|
96
99
|
});
|
|
97
100
|
connection.on('Target.attachedToTarget', (params) => {
|
|
@@ -232,13 +235,20 @@ export async function cdpE2eSession(options: CdpE2eSessionOptions): Promise<E2eS
|
|
|
232
235
|
const confirmed = async (online: boolean): Promise<void> => {
|
|
233
236
|
const deadline = performance.now() + options.loadTimeoutMs;
|
|
234
237
|
const open = [...pageSessions].filter((session) => sessions.has(session));
|
|
238
|
+
// `navigator.onLine` first, then a real request: Chrome tells the page before its network
|
|
239
|
+
// service applies the cut, so the first alone let a `fetch` through (`cdp-network-probe.ts`).
|
|
235
240
|
const answers = await Promise.all(
|
|
236
|
-
open.map(async (session) =>
|
|
241
|
+
open.map(async (session) => {
|
|
242
|
+
const reads =
|
|
243
|
+
(await pageReads(session, online, deadline)) &&
|
|
244
|
+
(await networkAgrees(send, session, online, deadline));
|
|
245
|
+
return [session, reads] as const;
|
|
246
|
+
}),
|
|
237
247
|
);
|
|
238
248
|
const waiting = answers.filter(([, reads]) => !reads).map(([session]) => session);
|
|
239
249
|
if (waiting.length > 0) {
|
|
240
250
|
throw new CdpTimeoutError({
|
|
241
|
-
method: `${ON_LINE} === ${String(online)} in page session ${waiting.join(', ')}`,
|
|
251
|
+
method: `${ON_LINE} === ${String(online)} and a request ${online ? 'reaching' : 'refused'} in page session ${waiting.join(', ')}`,
|
|
242
252
|
timeoutMs: options.loadTimeoutMs,
|
|
243
253
|
});
|
|
244
254
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// One responsibility: proving a page's NETWORK agrees with the offline switch, by sending a request.
|
|
2
|
+
// `navigator.onLine` is not that proof: Chrome tells the renderer before the network service applies
|
|
3
|
+
// the condition (two pipes, no order between them), so a `fetch` on the next line went through under
|
|
4
|
+
// a cut the page already read as offline — `browser-harness.e2e.test.ts`, CI run 39223207154.
|
|
5
|
+
|
|
6
|
+
import type { CdpResult } from './cdp-connection';
|
|
7
|
+
|
|
8
|
+
type Send = (
|
|
9
|
+
method: string,
|
|
10
|
+
params: Record<string, unknown>,
|
|
11
|
+
session: string,
|
|
12
|
+
deadlineMs?: number,
|
|
13
|
+
) => Promise<CdpResult>;
|
|
14
|
+
|
|
15
|
+
/** The probe's path: under `/_x/`, which no app route owns, so the app answers it 404 and does nothing. */
|
|
16
|
+
export const NETWORK_PROBE_PATH = '/_x/network-probe';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `HEAD`, because the framework's service worker answers GET only — a GET could be served from its
|
|
20
|
+
* cache or its offline document, and read as `reached` under a cut forever. `none` on a page that is
|
|
21
|
+
* not on http(s) (`about:blank`): it has no origin to ask, and nothing on it can fetch either.
|
|
22
|
+
*/
|
|
23
|
+
export const NETWORK_PROBE = `(async () => {
|
|
24
|
+
if (location.protocol !== 'http:' && location.protocol !== 'https:') return 'none';
|
|
25
|
+
try {
|
|
26
|
+
await fetch('${NETWORK_PROBE_PATH}', { method: 'HEAD', cache: 'no-store' });
|
|
27
|
+
return 'reached';
|
|
28
|
+
} catch {
|
|
29
|
+
return 'refused';
|
|
30
|
+
}
|
|
31
|
+
})()`;
|
|
32
|
+
|
|
33
|
+
/** Whether a logged request URL is a probe — kept out of the session's request log. */
|
|
34
|
+
export const isNetworkProbe = (url: string): boolean => {
|
|
35
|
+
try {
|
|
36
|
+
return new URL(url).pathname === NETWORK_PROBE_PATH;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const probeValue = (answer: CdpResult): unknown => {
|
|
43
|
+
const result = answer.result?.['result'];
|
|
44
|
+
return typeof result === 'object' && result !== null
|
|
45
|
+
? (result as Record<string, unknown>)['value']
|
|
46
|
+
: undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Probes one page until a request from it is refused (`online` false) or reaches the server
|
|
51
|
+
* (`online` true), and answers `false` at `deadline`. Each probe is itself a round trip to the
|
|
52
|
+
* network, so the loop paces itself — no sleep. A probe refused by a navigation is asked again.
|
|
53
|
+
*/
|
|
54
|
+
export async function networkAgrees(
|
|
55
|
+
send: Send,
|
|
56
|
+
session: string,
|
|
57
|
+
online: boolean,
|
|
58
|
+
deadline: number,
|
|
59
|
+
): Promise<boolean> {
|
|
60
|
+
const wanted = online ? 'reached' : 'refused';
|
|
61
|
+
for (;;) {
|
|
62
|
+
const remaining = deadline - performance.now();
|
|
63
|
+
if (remaining <= 0) return false;
|
|
64
|
+
try {
|
|
65
|
+
const answer = await send(
|
|
66
|
+
'Runtime.evaluate',
|
|
67
|
+
{ expression: NETWORK_PROBE, returnByValue: true, awaitPromise: true },
|
|
68
|
+
session,
|
|
69
|
+
remaining,
|
|
70
|
+
);
|
|
71
|
+
const value = probeValue(answer);
|
|
72
|
+
if (value === wanted || value === 'none') return true;
|
|
73
|
+
} catch {
|
|
74
|
+
// A navigation destroyed the context, or the call ran out of time: the deadline decides.
|
|
75
|
+
}
|
|
76
|
+
// One macrotask between probes: a refusal that settles as a microtask would otherwise starve
|
|
77
|
+
// every timer and socket in the process until the deadline.
|
|
78
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/e2e-driver.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { FixtureUnavailableError } from './errors';
|
|
|
9
9
|
import { withFailureContext } from './failure-context';
|
|
10
10
|
import { unavailableFixture } from './fixture-drivers';
|
|
11
11
|
import { defineFixtures } from './fixtures';
|
|
12
|
+
import { paced } from './test-pace';
|
|
12
13
|
import type { E2eBody, E2eFixtures, PageLike } from './test-types';
|
|
13
14
|
import { resetE2eDriver, useE2eDriver } from './test-types';
|
|
14
15
|
|
|
@@ -107,7 +108,7 @@ export function installE2eDriver(options: E2eDriverOptions): () => void {
|
|
|
107
108
|
throw withFailureContext(error);
|
|
108
109
|
}
|
|
109
110
|
};
|
|
110
|
-
bunTest(name, run, test?.timeoutMs);
|
|
111
|
+
bunTest(name, run, test?.timeoutMs === undefined ? undefined : paced(test.timeoutMs));
|
|
111
112
|
});
|
|
112
113
|
return () => {
|
|
113
114
|
// Both halves, because both were installed. Putting the DECLARATION back — rather than
|
package/src/e2e-page.ts
CHANGED
|
@@ -62,6 +62,21 @@ const readField = (raw: unknown, key: string): unknown => {
|
|
|
62
62
|
|
|
63
63
|
const TITLE = '(() => JSON.stringify({ title: document.title }))()';
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Where the server's SECOND flush begins: every hole a streamed page reveals is written as one
|
|
67
|
+
* chunk opening with this tag (`revealChunk` in `@ultimat3/render`'s `render-stream.ts`, which the
|
|
68
|
+
* barrel does not export). Everything before it is the first flush — the head and the shell.
|
|
69
|
+
*/
|
|
70
|
+
export const REVEAL_MARK = '<template data-x-hole=';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* How long the reader waits for the rest of a flush that arrived in pieces. One server flush is
|
|
74
|
+
* ONE `enqueue`, but the transport may hand it to the page as several chunks — a read of the first
|
|
75
|
+
* network chunk alone cut the shell mid-tag and failed `toContain` on a header it really sent.
|
|
76
|
+
* A pause this long ends the read when no hole has been revealed; a reveal ends it at once.
|
|
77
|
+
*/
|
|
78
|
+
export const FLUSH_IDLE_MS = 250;
|
|
79
|
+
|
|
65
80
|
/**
|
|
66
81
|
* The first flush, read by a `fetch` INSIDE the page after the navigation, because neither
|
|
67
82
|
* `ScrapePage` nor `CdpPageLike` exposes a response body — puppeteer's `page.on('response')` is
|
|
@@ -70,15 +85,37 @@ const TITLE = '(() => JSON.stringify({ title: document.title }))()';
|
|
|
70
85
|
* The cost, stated rather than hidden: this is a SECOND request to the same route, so what it
|
|
71
86
|
* measures is that route's streaming behaviour and not the byte-for-byte first chunk the open
|
|
72
87
|
* document received. It runs in the page, so it carries the page's cookies and its origin — a
|
|
73
|
-
* `fetch` from the test process would carry neither.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
88
|
+
* `fetch` from the test process would carry neither. Chunks are joined until the first reveal
|
|
89
|
+
* (cut there) or a pause of `FLUSH_IDLE_MS`, and the reader is then CANCELLED: a streamed response
|
|
90
|
+
* nobody pulls stays open until its last hole fills, holding one of the page's six connections to
|
|
91
|
+
* its origin for the rest of the test.
|
|
76
92
|
*/
|
|
77
93
|
const firstFlushExpression = (url: string): string =>
|
|
78
94
|
`(() => fetch(${JSON.stringify(url)}, { credentials: 'same-origin' })
|
|
79
|
-
.then((response) => {
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
.then((response) => {
|
|
96
|
+
const reader = response.body.getReader();
|
|
97
|
+
const decoder = new TextDecoder();
|
|
98
|
+
const mark = ${JSON.stringify(REVEAL_MARK)};
|
|
99
|
+
let html = '';
|
|
100
|
+
const finish = () => {
|
|
101
|
+
reader.cancel().catch(() => {});
|
|
102
|
+
const cut = html.indexOf(mark);
|
|
103
|
+
return JSON.stringify({ html: cut === -1 ? html : html.slice(0, cut) });
|
|
104
|
+
};
|
|
105
|
+
const pull = (first) => {
|
|
106
|
+
const read = reader.read();
|
|
107
|
+
const next = first ? read : Promise.race([
|
|
108
|
+
read,
|
|
109
|
+
new Promise((resolve) => setTimeout(() => resolve(null), ${FLUSH_IDLE_MS})),
|
|
110
|
+
]);
|
|
111
|
+
return next.then((chunk) => {
|
|
112
|
+
if (chunk === null || chunk.done) return finish();
|
|
113
|
+
html += decoder.decode(chunk.value, { stream: true });
|
|
114
|
+
return html.includes(mark) ? finish() : pull(false);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
return pull(true);
|
|
118
|
+
}))()`;
|
|
82
119
|
|
|
83
120
|
/**
|
|
84
121
|
* The adapter. Every member re-reads the live page: a `PageLike` handed to a test outlives every
|
package/src/errors.ts
CHANGED
|
@@ -48,6 +48,7 @@ export const TESTING_ERROR_CODES = [
|
|
|
48
48
|
'X_TEST_NUMBER_EXPECTED',
|
|
49
49
|
'X_TEST_ISLAND_STATE_UNKNOWN',
|
|
50
50
|
'X_TEST_APP_NOT_BOOTED',
|
|
51
|
+
'X_TEST_PACE_INVALID',
|
|
51
52
|
] as const;
|
|
52
53
|
|
|
53
54
|
export type TestingErrorCode = (typeof TESTING_ERROR_CODES)[number];
|
|
@@ -88,6 +89,7 @@ export const TESTING_ERROR_TITLES: Readonly<Record<TestingErrorCode, string>> =
|
|
|
88
89
|
X_TEST_NUMBER_EXPECTED: 'a matcher expected a finite number and got something else',
|
|
89
90
|
X_TEST_ISLAND_STATE_UNKNOWN: 'an island state manifest declares no state with that id',
|
|
90
91
|
X_TEST_APP_NOT_BOOTED: 'describeApp handle read before boot',
|
|
92
|
+
X_TEST_PACE_INVALID: 'ULTIMATE_TEST_PACE is not a number at or above 1',
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
// Titles must be registered for `format()` to render the contract's first line. Every code above is
|
package/src/fixtures.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { TestMail } from './fixture-mail';
|
|
|
18
18
|
import type { TestNetwork } from './fixture-network';
|
|
19
19
|
import type { TestPush } from './fixture-push';
|
|
20
20
|
import type { TestStatements } from './fixture-statements';
|
|
21
|
+
import { paced } from './test-pace';
|
|
21
22
|
import type { PageLike, TestOptions } from './test-types';
|
|
22
23
|
|
|
23
24
|
/** Built once per test, on first use. */
|
|
@@ -257,8 +258,13 @@ export async function runWithFixtures(body: FixtureRunBody): Promise<void> {
|
|
|
257
258
|
* must not be the reason the next file inherits a queue.
|
|
258
259
|
*
|
|
259
260
|
* `options.timeoutMs` is the test's own deadline, the same key the six typed helpers take: a
|
|
260
|
-
* fixture that builds something slow (an island, a browser) is the test that needs one.
|
|
261
|
+
* fixture that builds something slow (an island, a browser) is the test that needs one. Read at
|
|
262
|
+
* this machine's pace (`paced`), as the typed helpers read theirs.
|
|
261
263
|
*/
|
|
262
264
|
export function fixtureTest(name: string, body: FixtureBody, options?: TestOptions): void {
|
|
263
|
-
bunTest(
|
|
265
|
+
bunTest(
|
|
266
|
+
name,
|
|
267
|
+
() => runWithFixtures(body),
|
|
268
|
+
options?.timeoutMs === undefined ? undefined : paced(options.timeoutMs),
|
|
269
|
+
);
|
|
264
270
|
}
|
package/src/index.ts
CHANGED
package/src/test-pace.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// How much slower this machine runs than the one a test's declared deadline was measured on.
|
|
2
|
+
// `x verify` measures it (`packages/cli/src/verify-pace.ts`) and hands it to every test run it
|
|
3
|
+
// starts as `ULTIMATE_TEST_PACE`; a test's own timeout is read at that pace, so a 2-core 1.8 GHz
|
|
4
|
+
// box is held to the same bound a CI runner is instead of failing on a number set for the runner.
|
|
5
|
+
|
|
6
|
+
import { UltimateError } from '@ultimat3/core';
|
|
7
|
+
|
|
8
|
+
/** Set by `x verify` on each test run it starts. Absent — a hand-run `bun test` — is pace 1. */
|
|
9
|
+
export const TEST_PACE_ENV = 'ULTIMATE_TEST_PACE';
|
|
10
|
+
|
|
11
|
+
/** A pace that does not parse, or one below 1: a machine is never read as faster than the bound. */
|
|
12
|
+
export class TestPaceInvalidError extends UltimateError {
|
|
13
|
+
constructor(value: string) {
|
|
14
|
+
super({
|
|
15
|
+
code: 'X_TEST_PACE_INVALID',
|
|
16
|
+
cause: `${TEST_PACE_ENV}=${JSON.stringify(value)} is not a number at or above 1`,
|
|
17
|
+
fix: `env -u ${TEST_PACE_ENV} bun test <file> # the gate measures the pace and sets it on each test run itself; unset is pace 1`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The pace this process was started at. */
|
|
23
|
+
export function testPace(env: Readonly<Record<string, string | undefined>> = Bun.env): number {
|
|
24
|
+
const raw = env[TEST_PACE_ENV];
|
|
25
|
+
if (raw === undefined || raw.trim() === '') return 1;
|
|
26
|
+
const pace = Number(raw);
|
|
27
|
+
if (!Number.isFinite(pace) || pace < 1) throw new TestPaceInvalidError(raw);
|
|
28
|
+
return pace;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A declared test deadline at this machine's pace: `test(name, body, paced(90_000))`. The number
|
|
33
|
+
* stays the one measured on a fast machine; only a slower one stretches it.
|
|
34
|
+
*/
|
|
35
|
+
export const paced = (ms: number, env?: Readonly<Record<string, string | undefined>>): number =>
|
|
36
|
+
Math.round(ms * testPace(env));
|
package/src/test-types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { test } from 'bun:test';
|
|
|
6
6
|
import { TestEvalThresholdError } from './errors';
|
|
7
7
|
import type { FixtureBody } from './fixtures';
|
|
8
8
|
import { runWithFixtures } from './fixtures';
|
|
9
|
+
import { paced } from './test-pace';
|
|
9
10
|
|
|
10
11
|
export const TEST_TYPES = ['unit', 'contract', 'live', 'job', 'e2e', 'eval'] as const;
|
|
11
12
|
|
|
@@ -21,7 +22,10 @@ export const testName = (type: TestType, name: string): string => `${type}${SEPA
|
|
|
21
22
|
* outlive Bun's 5 s default, which is the one-way rule broken for a number (#589).
|
|
22
23
|
*/
|
|
23
24
|
export interface TestOptions {
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* This test's own deadline in milliseconds, handed to `bun:test` at this machine's pace
|
|
27
|
+
* (`paced`). Absent is the run's default, which `x verify` paces the same way.
|
|
28
|
+
*/
|
|
25
29
|
readonly timeoutMs?: number;
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -34,7 +38,11 @@ export interface TestOptions {
|
|
|
34
38
|
const typed =
|
|
35
39
|
(type: TestType) =>
|
|
36
40
|
(name: string, body: FixtureBody, options?: TestOptions): void => {
|
|
37
|
-
test(
|
|
41
|
+
test(
|
|
42
|
+
testName(type, name),
|
|
43
|
+
() => runWithFixtures(body),
|
|
44
|
+
options?.timeoutMs === undefined ? undefined : paced(options.timeoutMs),
|
|
45
|
+
);
|
|
38
46
|
};
|
|
39
47
|
|
|
40
48
|
/** Pure logic: no database, no clock, no network. The cheapest test that can fail for real. */
|
|
@@ -192,7 +200,7 @@ export function evalTest<TInput, TOutput>(
|
|
|
192
200
|
throw new TestEvalThresholdError({ name, threshold: options.threshold, detail });
|
|
193
201
|
}
|
|
194
202
|
},
|
|
195
|
-
options.timeoutMs,
|
|
203
|
+
options.timeoutMs === undefined ? undefined : paced(options.timeoutMs),
|
|
196
204
|
);
|
|
197
205
|
}
|
|
198
206
|
|