@ultimat3/testing 26.0.0 → 26.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/README.md +1 -0
- package/package.json +16 -15
- package/src/fixture-push.ts +113 -0
- package/src/fixtures.ts +2 -0
- package/src/framework-fixtures.ts +4 -0
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ with `{ runJobs }` — never `describe(testName('unit', …))` around a bare `te
|
|
|
92
92
|
| `clock` | `now()` · `advance('3d')` · `set(instant)` on the frozen clock | the preload |
|
|
93
93
|
| `mail` | `outbox()` · `lastTo(address)` · `failOnce(mail)` over an in-memory transport | the preload |
|
|
94
94
|
| `network` | `offline()` · `drop()` · `online()` · `state()` over the sealed network | the preload |
|
|
95
|
+
| `push` | a Web Push runtime with the push service and the device played in-process: `subscribe(actorId, { locale })` gives that person a browser, `sent()` is what each device would SHOW (decrypted with its own key), `answerOnce(410 \| 429 \| …)` scripts the push service | the preload |
|
|
95
96
|
| `runJobs` | a worker: call it to enqueue+drain, then `enqueue()` `drain()` `due()` `inFlight()` `depth()` — see [below](#a-job-under-runjobs) | the preload |
|
|
96
97
|
| `statements` | every statement the test issued: `all()` `count(fingerprint?)` `shapes()` — and an N+1 throws | the preload |
|
|
97
98
|
| `page` | the browser: `goto` `gotoStreamed` `getByRole` `evaluate` `waitForServiceWorker({ timeoutMs? })` — an uncontrolled first page is reloaded once for its worker (`e2e-service-worker.ts`) | a browser driver |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/testing",
|
|
3
|
-
"version": "26.
|
|
3
|
+
"version": "26.1.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",
|
|
@@ -36,19 +36,20 @@
|
|
|
36
36
|
"test": "bun test"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ultimat3/action": "26.
|
|
40
|
-
"@ultimat3/cache": "26.
|
|
41
|
-
"@ultimat3/core": "26.
|
|
42
|
-
"@ultimat3/db": "26.
|
|
43
|
-
"@ultimat3/entity": "26.
|
|
44
|
-
"@ultimat3/http": "26.
|
|
45
|
-
"@ultimat3/i18n": "26.
|
|
46
|
-
"@ultimat3/jobs": "26.
|
|
47
|
-
"@ultimat3/mail": "26.
|
|
48
|
-
"@ultimat3/policy": "26.
|
|
49
|
-
"@ultimat3/
|
|
50
|
-
"@ultimat3/
|
|
51
|
-
"@ultimat3/
|
|
52
|
-
"@ultimat3/
|
|
39
|
+
"@ultimat3/action": "26.1.0",
|
|
40
|
+
"@ultimat3/cache": "26.1.0",
|
|
41
|
+
"@ultimat3/core": "26.1.0",
|
|
42
|
+
"@ultimat3/db": "26.1.0",
|
|
43
|
+
"@ultimat3/entity": "26.1.0",
|
|
44
|
+
"@ultimat3/http": "26.1.0",
|
|
45
|
+
"@ultimat3/i18n": "26.1.0",
|
|
46
|
+
"@ultimat3/jobs": "26.1.0",
|
|
47
|
+
"@ultimat3/mail": "26.1.0",
|
|
48
|
+
"@ultimat3/policy": "26.1.0",
|
|
49
|
+
"@ultimat3/pwa": "26.1.0",
|
|
50
|
+
"@ultimat3/query": "26.1.0",
|
|
51
|
+
"@ultimat3/realtime": "26.1.0",
|
|
52
|
+
"@ultimat3/render": "26.1.0",
|
|
53
|
+
"@ultimat3/time": "26.1.0"
|
|
53
54
|
}
|
|
54
55
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// The `push` fixture: a Web Push runtime for one test, with the push service AND the device played
|
|
2
|
+
// in-process. `push.subscribe(actorId)` gives that person a browser — a real P-256 key and auth
|
|
3
|
+
// secret, as `PushSubscription.toJSON()` carries — and `push.sent()` is what each device would SHOW:
|
|
4
|
+
// every delivery decrypted with that browser's own key, so a test asserts the notification, not
|
|
5
|
+
// the bytes. Nothing reaches the network.
|
|
6
|
+
|
|
7
|
+
import type { PushReceiverKeys } from '@ultimat3/pwa';
|
|
8
|
+
import { frozenNow } from './determinism';
|
|
9
|
+
|
|
10
|
+
/** One delivery, as the device received it. */
|
|
11
|
+
export interface TestPushMessage {
|
|
12
|
+
readonly actorId: string;
|
|
13
|
+
readonly endpoint: string;
|
|
14
|
+
/** The push service's request headers: `ttl`, `urgency`, `topic`, `authorization`. */
|
|
15
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
16
|
+
/** The notification the worker would show — `serializePushMessage`'s fields, decrypted. */
|
|
17
|
+
readonly notification: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `Disposable`: the fixture installs the process's push runtime and releases it after the test. */
|
|
21
|
+
export interface TestPush extends Disposable {
|
|
22
|
+
/** A browser for `actorId`, subscribed in `locale` (default `en`); answers its endpoint. */
|
|
23
|
+
subscribe(
|
|
24
|
+
actorId: string,
|
|
25
|
+
options?: { readonly locale?: string; readonly timeZone?: string },
|
|
26
|
+
): Promise<string>;
|
|
27
|
+
/** Oldest first. */
|
|
28
|
+
sent(): readonly TestPushMessage[];
|
|
29
|
+
/** The push service answers the next request `status`: 410 deletes it, 429 makes the job retry. */
|
|
30
|
+
answerOnce(status: number): void;
|
|
31
|
+
clear(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const PUSH_SERVICE = 'https://push.test';
|
|
35
|
+
|
|
36
|
+
export async function testPush(): Promise<TestPush> {
|
|
37
|
+
const pwa = await import('@ultimat3/pwa');
|
|
38
|
+
const store = pwa.memoryPushSubscriptionStore();
|
|
39
|
+
const devices = new Map<string, { readonly actorId: string; readonly keys: PushReceiverKeys }>();
|
|
40
|
+
const sent: TestPushMessage[] = [];
|
|
41
|
+
const answers: number[] = [];
|
|
42
|
+
let counter = 0;
|
|
43
|
+
|
|
44
|
+
const pushService = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
45
|
+
const request = new Request(input, init);
|
|
46
|
+
const answer = answers.shift() ?? 201;
|
|
47
|
+
const device = devices.get(request.url);
|
|
48
|
+
if (answer >= 200 && answer < 300 && device !== undefined) {
|
|
49
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
50
|
+
const plain = await pwa.decryptPushMessage(body, device.keys);
|
|
51
|
+
sent.push({
|
|
52
|
+
actorId: device.actorId,
|
|
53
|
+
endpoint: request.url,
|
|
54
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
55
|
+
notification: JSON.parse(new TextDecoder().decode(plain)) as Record<string, unknown>,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return new Response(null, { status: answer });
|
|
59
|
+
}) as typeof fetch;
|
|
60
|
+
|
|
61
|
+
const release = await pwa.installWebPush({
|
|
62
|
+
store,
|
|
63
|
+
keys: pwa.DEV_VAPID_KEYS,
|
|
64
|
+
subject: 'mailto:push@example.test',
|
|
65
|
+
fetch: pushService,
|
|
66
|
+
// The stub push service's host: not a real one, so the app's list names it, as an app would.
|
|
67
|
+
pushHosts: [new URL(PUSH_SERVICE).hostname],
|
|
68
|
+
// The clock the `clock` fixture moves — never the wall clock: a VAPID token's `exp` and a
|
|
69
|
+
// subscription's `expirationTime` are read against it, so `clock.advance('2d')` reaches both.
|
|
70
|
+
clock: { now: frozenNow, monotonic: () => frozenNow().getTime() },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
async subscribe(actorId, options) {
|
|
75
|
+
counter += 1;
|
|
76
|
+
const pair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, [
|
|
77
|
+
'deriveBits',
|
|
78
|
+
]);
|
|
79
|
+
const publicKey = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
|
|
80
|
+
// why: a browser rolls its auth secret; this stands in for one, and pins nothing a test reads.
|
|
81
|
+
const authSecret = crypto.getRandomValues(new Uint8Array(16));
|
|
82
|
+
const endpoint = `${PUSH_SERVICE}/device/${counter}`;
|
|
83
|
+
devices.set(endpoint, {
|
|
84
|
+
actorId,
|
|
85
|
+
keys: { privateKey: pair.privateKey, publicKey, authSecret },
|
|
86
|
+
});
|
|
87
|
+
const base64url = (bytes: Uint8Array): string =>
|
|
88
|
+
btoa(String.fromCharCode(...bytes))
|
|
89
|
+
.replaceAll('+', '-')
|
|
90
|
+
.replaceAll('/', '_')
|
|
91
|
+
.replaceAll('=', '');
|
|
92
|
+
await store.save({
|
|
93
|
+
endpoint,
|
|
94
|
+
keys: { p256dh: base64url(publicKey), auth: base64url(authSecret) },
|
|
95
|
+
locale: options?.locale ?? 'en',
|
|
96
|
+
timeZone: options?.timeZone ?? 'UTC',
|
|
97
|
+
actorId,
|
|
98
|
+
createdAt: 0,
|
|
99
|
+
expirationTime: null,
|
|
100
|
+
});
|
|
101
|
+
return endpoint;
|
|
102
|
+
},
|
|
103
|
+
sent: () => [...sent],
|
|
104
|
+
answerOnce: (status) => {
|
|
105
|
+
answers.push(status);
|
|
106
|
+
},
|
|
107
|
+
clear: () => {
|
|
108
|
+
sent.length = 0;
|
|
109
|
+
answers.length = 0;
|
|
110
|
+
},
|
|
111
|
+
[Symbol.dispose]: release,
|
|
112
|
+
};
|
|
113
|
+
}
|
package/src/fixtures.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { SignIn, Subscribe, TestBudget, TestDeploy } from './fixture-driver
|
|
|
16
16
|
import type { RunJobs } from './fixture-jobs';
|
|
17
17
|
import type { TestMail } from './fixture-mail';
|
|
18
18
|
import type { TestNetwork } from './fixture-network';
|
|
19
|
+
import type { TestPush } from './fixture-push';
|
|
19
20
|
import type { TestStatements } from './fixture-statements';
|
|
20
21
|
import type { PageLike, TestOptions } from './test-types';
|
|
21
22
|
|
|
@@ -45,6 +46,7 @@ export interface Fixtures {
|
|
|
45
46
|
readonly clock: TestClock;
|
|
46
47
|
readonly mail: TestMail;
|
|
47
48
|
readonly network: TestNetwork;
|
|
49
|
+
readonly push: TestPush;
|
|
48
50
|
readonly runJobs: RunJobs;
|
|
49
51
|
readonly statements: TestStatements;
|
|
50
52
|
readonly budget: TestBudget;
|
|
@@ -7,6 +7,7 @@ import { DRIVER_FIXTURE_NAMES, driverFixtures } from './fixture-drivers';
|
|
|
7
7
|
import { testJobs } from './fixture-jobs';
|
|
8
8
|
import { testMail } from './fixture-mail';
|
|
9
9
|
import { testNetwork } from './fixture-network';
|
|
10
|
+
import { testPush } from './fixture-push';
|
|
10
11
|
import { testStatements } from './fixture-statements';
|
|
11
12
|
import { subscribeDriver } from './fixture-subscribe';
|
|
12
13
|
import { defineFixtures } from './fixtures';
|
|
@@ -21,6 +22,8 @@ export const FRAMEWORK_FIXTURE_NAMES = [
|
|
|
21
22
|
'clock',
|
|
22
23
|
'mail',
|
|
23
24
|
'network',
|
|
25
|
+
// 2026-10-08: the Web Push runtime, with the push service and the device played in-process.
|
|
26
|
+
'push',
|
|
24
27
|
'runJobs',
|
|
25
28
|
'statements',
|
|
26
29
|
// Moved here from `DRIVER_FIXTURE_NAMES` on 2026-08-20: the driver it was waiting for is
|
|
@@ -51,6 +54,7 @@ export function registerFrameworkFixtures(): void {
|
|
|
51
54
|
clock: testClock,
|
|
52
55
|
mail: testMail,
|
|
53
56
|
network: testNetwork,
|
|
57
|
+
push: testPush,
|
|
54
58
|
runJobs: testJobs,
|
|
55
59
|
statements: testStatements,
|
|
56
60
|
// After the spread, so it REPLACES the `unavailableFixture('subscribe')` declaration above it —
|
package/src/index.ts
CHANGED
|
@@ -202,6 +202,8 @@ export type { MailRef, TestMail } from './fixture-mail';
|
|
|
202
202
|
export { testMail } from './fixture-mail';
|
|
203
203
|
export type { TestNetwork } from './fixture-network';
|
|
204
204
|
export { testNetwork } from './fixture-network';
|
|
205
|
+
export type { TestPush, TestPushMessage } from './fixture-push';
|
|
206
|
+
export { testPush } from './fixture-push';
|
|
205
207
|
export type { ObservedStatement, StatementShape, TestStatements } from './fixture-statements';
|
|
206
208
|
export { testStatements } from './fixture-statements';
|
|
207
209
|
export type { SubscribeDriver } from './fixture-subscribe';
|