@tonalisk-labs/kit 1.0.0 → 1.0.6
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 -80
- package/dist/testing/src/config-coverage.d.ts.map +1 -1
- package/dist/testing/src/config-coverage.js +3 -2
- package/dist/testing/src/config-coverage.js.map +1 -1
- package/dist/testing/src/fixtures.d.ts +9 -3
- package/dist/testing/src/fixtures.d.ts.map +1 -1
- package/dist/testing/src/fixtures.js +98 -42
- package/dist/testing/src/fixtures.js.map +1 -1
- package/dist/testing/src/floors/node.js +11 -3
- package/dist/testing/src/floors/node.js.map +1 -1
- package/dist/testing/src/stack/browsers.d.ts +13 -3
- package/dist/testing/src/stack/browsers.d.ts.map +1 -1
- package/dist/testing/src/stack/browsers.js +117 -14
- package/dist/testing/src/stack/browsers.js.map +1 -1
- package/dist/testing/src/stack/containers.d.ts +6 -2
- package/dist/testing/src/stack/containers.d.ts.map +1 -1
- package/dist/testing/src/stack/containers.js +8 -1
- package/dist/testing/src/stack/containers.js.map +1 -1
- package/dist/testing/src/stack/control.d.ts +18 -3
- package/dist/testing/src/stack/control.d.ts.map +1 -1
- package/dist/testing/src/stack/control.js +50 -10
- package/dist/testing/src/stack/control.js.map +1 -1
- package/dist/testing/src/stack/gate-member.d.ts +10 -0
- package/dist/testing/src/stack/gate-member.d.ts.map +1 -1
- package/dist/testing/src/stack/gate-member.js +68 -7
- package/dist/testing/src/stack/gate-member.js.map +1 -1
- package/dist/testing/src/stack/gate-server.d.ts +37 -4
- package/dist/testing/src/stack/gate-server.d.ts.map +1 -1
- package/dist/testing/src/stack/gate-server.js +124 -12
- package/dist/testing/src/stack/gate-server.js.map +1 -1
- package/dist/testing/src/stack/hold.d.ts +24 -3
- package/dist/testing/src/stack/hold.d.ts.map +1 -1
- package/dist/testing/src/stack/hold.js +136 -11
- package/dist/testing/src/stack/hold.js.map +1 -1
- package/dist/testing/src/stack/host.d.ts.map +1 -1
- package/dist/testing/src/stack/host.js +26 -6
- package/dist/testing/src/stack/host.js.map +1 -1
- package/dist/testing/src/stack/lock.d.ts +15 -4
- package/dist/testing/src/stack/lock.d.ts.map +1 -1
- package/dist/testing/src/stack/lock.js +47 -18
- package/dist/testing/src/stack/lock.js.map +1 -1
- package/dist/testing/src/stack/preview-child.js +14 -3
- package/dist/testing/src/stack/preview-child.js.map +1 -1
- package/dist/testing/src/stack/runtime-apple.d.ts.map +1 -1
- package/dist/testing/src/stack/runtime-apple.js +16 -0
- package/dist/testing/src/stack/runtime-apple.js.map +1 -1
- package/dist/testing/src/stack/runtime-docker.d.ts +2 -1
- package/dist/testing/src/stack/runtime-docker.d.ts.map +1 -1
- package/dist/testing/src/stack/runtime-docker.js +29 -3
- package/dist/testing/src/stack/runtime-docker.js.map +1 -1
- package/dist/testing/src/stack/runtime.d.ts +10 -0
- package/dist/testing/src/stack/runtime.d.ts.map +1 -1
- package/dist/testing/src/stack/runtime.js +1 -0
- package/dist/testing/src/stack/runtime.js.map +1 -1
- package/dist/testing/src/stack/socks.d.ts +25 -0
- package/dist/testing/src/stack/socks.d.ts.map +1 -0
- package/dist/testing/src/stack/socks.js +195 -0
- package/dist/testing/src/stack/socks.js.map +1 -0
- package/dist/testing/src/stack/stall.d.ts +36 -0
- package/dist/testing/src/stack/stall.d.ts.map +1 -0
- package/dist/testing/src/stack/stall.js +93 -0
- package/dist/testing/src/stack/stall.js.map +1 -0
- package/package.json +4 -4
- package/testing/src/config-coverage.ts +3 -2
- package/testing/src/fixtures.ts +277 -197
- package/testing/src/floors/node.ts +19 -3
- package/testing/src/stack/browsers.ts +129 -15
- package/testing/src/stack/containers.ts +14 -2
- package/testing/src/stack/control.ts +62 -11
- package/testing/src/stack/gate-member.ts +90 -8
- package/testing/src/stack/gate-server.ts +166 -21
- package/testing/src/stack/hold.ts +163 -11
- package/testing/src/stack/host.ts +32 -9
- package/testing/src/stack/lock.ts +61 -21
- package/testing/src/stack/preview-child.ts +21 -4
- package/testing/src/stack/runtime-apple.ts +20 -1
- package/testing/src/stack/runtime-docker.ts +47 -3
- package/testing/src/stack/runtime.ts +11 -0
- package/testing/src/stack/socks.ts +233 -0
- package/testing/src/stack/stall.ts +145 -0
package/testing/src/fixtures.ts
CHANGED
|
@@ -21,16 +21,20 @@ import { CPU_SLOWDOWN } from './network.ts';
|
|
|
21
21
|
import { type Meter, type Reconnect, startMeter } from './perf/measure.ts';
|
|
22
22
|
import { FAILURE_ATTACHMENT } from './perf/steps.ts';
|
|
23
23
|
import { installVitals } from './perf/vitals.ts';
|
|
24
|
-
import { deviceBudget } from './stack/browsers.ts';
|
|
24
|
+
import { deviceBudget, endpointOf } from './stack/browsers.ts';
|
|
25
25
|
import {
|
|
26
26
|
holdBrowserSeat,
|
|
27
27
|
holdStack,
|
|
28
|
+
deadlineFrom,
|
|
28
29
|
holdTestTurn,
|
|
30
|
+
testBegins,
|
|
31
|
+
waitedFor,
|
|
29
32
|
HOST_ENV,
|
|
30
33
|
LINUX_ENV,
|
|
31
34
|
MEASURE_ENV,
|
|
32
35
|
STACK_ENV,
|
|
33
36
|
} from './stack/control.ts';
|
|
37
|
+
import { announceDeadline } from './stack/hold.ts';
|
|
34
38
|
import { claimPort } from './stack/ports.ts';
|
|
35
39
|
import type { Coordinates } from './stack/stack.ts';
|
|
36
40
|
import { browserTrust, trustCa } from './stack/tls.ts';
|
|
@@ -43,7 +47,8 @@ export interface Options {
|
|
|
43
47
|
/** A visual project's: Chromium in the Linux image, holding screens to their baselines. */
|
|
44
48
|
readonly visual: boolean;
|
|
45
49
|
/** The most browser contexts one of the worker's tests has open at once, its own included: a
|
|
46
|
-
* seat each in the Linux image, which a test of several devices declares
|
|
50
|
+
* seat each in the Linux image while the test runs, which a test of several devices declares
|
|
51
|
+
* with `test.use`. */
|
|
47
52
|
readonly browserContexts: number;
|
|
48
53
|
}
|
|
49
54
|
|
|
@@ -56,15 +61,48 @@ const API = '@api';
|
|
|
56
61
|
interface StackWorkerFixtures {
|
|
57
62
|
/** The target's running stack, from its setup project. */
|
|
58
63
|
readonly stack: Coordinates;
|
|
59
|
-
/** Whether the worker's browser holds a seat in the Linux image (stack/browsers.ts). */
|
|
60
|
-
readonly browserSeat: boolean;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
interface StackFixtures {
|
|
64
67
|
/** Whether this test owns the stack alone (`@exclusive`), or shares it with every other. */
|
|
65
68
|
readonly stackHold: boolean;
|
|
69
|
+
/** The test's deadline following its timeout to the servers of its holds. */
|
|
70
|
+
readonly deadlines: boolean;
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
/** The last move of a deadline on its way, so the next follows it and the servers hear them in
|
|
74
|
+
* the order the test made them. */
|
|
75
|
+
const announcing = { last: Promise.resolve(true) };
|
|
76
|
+
|
|
77
|
+
/** Tells the servers of the running test's holds its deadline, as its timeout now stands; a server
|
|
78
|
+
* that can't be told keeps the deadline it had. */
|
|
79
|
+
const announceTimeout = (timeout: number): void => {
|
|
80
|
+
const deadline = deadlineFrom(timeout);
|
|
81
|
+
announcing.last = announcing.last.then(async () =>
|
|
82
|
+
announceDeadline(deadline).then(
|
|
83
|
+
() => true,
|
|
84
|
+
() => false,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** `type.slow()` moves the running test's deadline with its timeout, as `testInfo.slow()` does: it
|
|
90
|
+
* triples the timeout without `setTimeout`, so a hold's deadline would stay a third of it. */
|
|
91
|
+
const followingSlow = <Type extends object>(type: Type): Type => {
|
|
92
|
+
const slow: unknown = Reflect.get(type, 'slow');
|
|
93
|
+
if (typeof slow === 'function') {
|
|
94
|
+
Reflect.set(type, 'slow', (...args: unknown[]): void => {
|
|
95
|
+
Reflect.apply(slow, type, args);
|
|
96
|
+
try {
|
|
97
|
+
announceTimeout(base.info().timeout);
|
|
98
|
+
} catch {
|
|
99
|
+
// Called while the file loads, for every test in it: no test runs, so none holds anything.
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return type;
|
|
104
|
+
};
|
|
105
|
+
|
|
68
106
|
interface WorkerFixtures {
|
|
69
107
|
/** Chromium's DevTools port, which Lighthouse connects to; no other engine has one. */
|
|
70
108
|
readonly cdpPort: number | undefined;
|
|
@@ -76,6 +114,11 @@ export interface Devices {
|
|
|
76
114
|
}
|
|
77
115
|
|
|
78
116
|
interface TestFixtures {
|
|
117
|
+
/** Whether the test holds seats in the Linux image (stack/browsers.ts), one per context it may
|
|
118
|
+
* have open. */
|
|
119
|
+
readonly browserSeat: boolean;
|
|
120
|
+
/** The test's CPU turn, held for its own work. */
|
|
121
|
+
readonly cpuTurn: boolean;
|
|
79
122
|
readonly invariants: Watch;
|
|
80
123
|
readonly devices: Devices;
|
|
81
124
|
/** Live sockets' channels by path, for a metered page's frame counts. */
|
|
@@ -122,60 +165,67 @@ type BaseWorker = PlaywrightWorkerArgs & PlaywrightWorkerOptions;
|
|
|
122
165
|
export const apiTest: TestType<
|
|
123
166
|
BaseTest & StackFixtures,
|
|
124
167
|
BaseWorker & Options & StackWorkerFixtures
|
|
125
|
-
> =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
> = followingSlow(
|
|
169
|
+
base.extend<StackFixtures, Options & StackWorkerFixtures>({
|
|
170
|
+
/** A test sets its timeout as it starts, past the holds its fixtures took, or makes itself slow:
|
|
171
|
+
* each change moves the deadline its holds' servers bound their waits by (stack/stall.ts). */
|
|
172
|
+
deadlines: [
|
|
173
|
+
async ({ site: _site }, provide, testInfo): Promise<void> => {
|
|
174
|
+
testBegins();
|
|
175
|
+
const set = testInfo.setTimeout.bind(testInfo);
|
|
176
|
+
const slow = testInfo.slow.bind(testInfo);
|
|
177
|
+
Object.assign(testInfo, {
|
|
178
|
+
setTimeout: (timeout: number): void => {
|
|
179
|
+
set(timeout);
|
|
180
|
+
announceTimeout(testInfo.timeout);
|
|
181
|
+
},
|
|
182
|
+
slow: (...args: Parameters<typeof slow>): void => {
|
|
183
|
+
slow(...args);
|
|
184
|
+
announceTimeout(testInfo.timeout);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
139
187
|
await provide(true);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
{ scope: 'worker'
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
},
|
|
189
|
+
{ auto: true },
|
|
190
|
+
],
|
|
191
|
+
site: ['', { option: true, scope: 'worker' }],
|
|
192
|
+
browserContexts: [1, { option: true, scope: 'worker' }],
|
|
193
|
+
row: [undefined, { option: true, scope: 'worker' }],
|
|
194
|
+
visual: [false, { option: true, scope: 'worker' }],
|
|
195
|
+
stack: [
|
|
196
|
+
async ({ site }, provide): Promise<void> => {
|
|
197
|
+
const handoff = process.env[STACK_ENV];
|
|
198
|
+
const parsed: unknown = handoff === undefined ? undefined : JSON.parse(handoff);
|
|
199
|
+
if (!isCoordinates(parsed)) {
|
|
200
|
+
throw new Error(`${site} has no running stack: its setup project starts one`);
|
|
201
|
+
}
|
|
202
|
+
// Node's own clients (fetch, WebSocket) reach the edge over TLS too.
|
|
203
|
+
trustCa(parsed.trust.ca);
|
|
204
|
+
await provide(parsed);
|
|
205
|
+
},
|
|
206
|
+
{ scope: 'worker' },
|
|
207
|
+
],
|
|
208
|
+
stackHold: [
|
|
209
|
+
async ({ site }, provide, testInfo): Promise<void> => {
|
|
210
|
+
const owns = testInfo.tags.includes(EXCLUSIVE);
|
|
211
|
+
if (site === '' || process.env[HOST_ENV] === undefined) {
|
|
212
|
+
await provide(owns);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const release = await waitedFor(async () => holdStack(owns ? 'own' : 'share', site));
|
|
216
|
+
announceTimeout(testInfo.timeout);
|
|
217
|
+
try {
|
|
218
|
+
await provide(owns);
|
|
219
|
+
} finally {
|
|
220
|
+
release();
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
// Unbounded, and out of the test's own time: the grant always comes, as each holder is a test
|
|
224
|
+
// under its own timeout, whose end gives the hold back.
|
|
225
|
+
{ auto: true, timeout: 0 },
|
|
226
|
+
],
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
179
229
|
|
|
180
230
|
/** A browser the Linux image's server runs, where the stack host started one. */
|
|
181
231
|
const servedByImage = (browserName: string, visual: boolean): boolean =>
|
|
@@ -185,152 +235,182 @@ const servedByImage = (browserName: string, visual: boolean): boolean =>
|
|
|
185
235
|
export const test: TestType<
|
|
186
236
|
BaseTest & StackFixtures & TestFixtures,
|
|
187
237
|
BaseWorker & Options & StackWorkerFixtures & WorkerFixtures
|
|
188
|
-
> =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
238
|
+
> = followingSlow(
|
|
239
|
+
apiTest.extend<TestFixtures, WorkerFixtures>({
|
|
240
|
+
cdpPort: [
|
|
241
|
+
async ({ browserName, visual }, provide): Promise<void> => {
|
|
242
|
+
// The visual projects' Chromium runs in the container, its DevTools port out of reach.
|
|
243
|
+
const claim = browserName === 'chromium' && !visual ? await claimPort() : undefined;
|
|
244
|
+
await provide(claim === undefined ? undefined : claim.port);
|
|
245
|
+
if (claim !== undefined) {
|
|
246
|
+
await claim.stop();
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{ scope: 'worker' },
|
|
250
|
+
],
|
|
251
|
+
/**
|
|
252
|
+
* WebKit on macOS, and every visual project's browser, is Playwright's own Linux build in the
|
|
253
|
+
* container the stack host starts; baselines match only the architecture that drew them.
|
|
254
|
+
*/
|
|
255
|
+
connectOptions: [
|
|
256
|
+
async ({ browserName, visual }, provide): Promise<void> => {
|
|
257
|
+
if (visual && arch() !== BASELINE_ARCH) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`The baselines were drawn on ${BASELINE_ARCH} and this machine is ${arch()}: its Linux image renders differently, so the visual projects run on ${BASELINE_ARCH} alone`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const remote = visual || browserName === 'webkit';
|
|
263
|
+
const wsEndpoint = remote ? endpointOf(browserName, process.env[LINUX_ENV]) : undefined;
|
|
264
|
+
if (remote && (visual || platform() === 'darwin') && wsEndpoint === undefined) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
'WebKit on macOS and the visual projects run only in the Linux image: its setup project starts it',
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
// The image's browser reaches the host's loopback through the host's proxy (stack/socks.ts).
|
|
270
|
+
await provide(wsEndpoint === undefined ? undefined : { wsEndpoint });
|
|
271
|
+
},
|
|
272
|
+
{ scope: 'worker' },
|
|
273
|
+
],
|
|
274
|
+
/** The browser trusts the edge as every other harness browser does (browserTrust). */
|
|
275
|
+
launchOptions: [
|
|
276
|
+
async ({ launchOptions, cdpPort, browserName, stack }, provide): Promise<void> => {
|
|
277
|
+
const debugging =
|
|
278
|
+
cdpPort === undefined ? [] : [`--remote-debugging-port=${String(cdpPort)}`];
|
|
279
|
+
const trusting = browserTrust(stack.trust, browserName).args;
|
|
280
|
+
const args = [...(launchOptions.args ?? []), ...debugging, ...trusting];
|
|
281
|
+
await provide(Object.assign(structuredClone(launchOptions), { args }));
|
|
282
|
+
},
|
|
283
|
+
{ scope: 'worker' },
|
|
284
|
+
],
|
|
285
|
+
ignoreHTTPSErrors: async ({ browserName, connectOptions, stack }, provide): Promise<void> => {
|
|
286
|
+
const contained = connectOptions !== undefined;
|
|
287
|
+
await provide(browserTrust(stack.trust, browserName, contained).ignoreHTTPSErrors);
|
|
197
288
|
},
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* WebKit on macOS, and every visual project's browser, is Playwright's own Linux build in the
|
|
202
|
-
* container the stack host starts; baselines match only the architecture that drew them.
|
|
203
|
-
*/
|
|
204
|
-
connectOptions: [
|
|
205
|
-
async ({ browserName, visual, browserSeat: _seated }, provide): Promise<void> => {
|
|
206
|
-
if (visual && arch() !== BASELINE_ARCH) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`The baselines were drawn on ${BASELINE_ARCH} and this machine is ${arch()}: its Linux image renders differently, so the visual projects run on ${BASELINE_ARCH} alone`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
const remote = visual || browserName === 'webkit';
|
|
212
|
-
const wsEndpoint = remote ? process.env[LINUX_ENV] : undefined;
|
|
213
|
-
if (remote && (visual || platform() === 'darwin') && wsEndpoint === undefined) {
|
|
214
|
-
throw new Error(
|
|
215
|
-
'WebKit on macOS and the visual projects run only in the Linux image: its setup project starts it',
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
await provide(
|
|
219
|
-
wsEndpoint === undefined ? undefined : { wsEndpoint, exposeNetwork: '<loopback>' },
|
|
220
|
-
);
|
|
289
|
+
baseURL: async ({ stack }, provide): Promise<void> => {
|
|
290
|
+
await provide(stack.baseURL);
|
|
221
291
|
},
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const args = [...(launchOptions.args ?? []), ...debugging, ...trusting];
|
|
230
|
-
await provide(Object.assign(structuredClone(launchOptions), { args }));
|
|
292
|
+
/** The test's context, made once its stack hold, seats and CPU turn are held; named to the edge. */
|
|
293
|
+
context: async ({ cpuTurn: _turn, context, row, stack }, provide, testInfo): Promise<void> => {
|
|
294
|
+
if (testInfo.tags.includes(API)) {
|
|
295
|
+
throw new Error(`an ${API} test makes no page: it is written with apiTest`);
|
|
296
|
+
}
|
|
297
|
+
await joinEdge(context, stack.baseURL, row === undefined ? 'wired' : row.network);
|
|
298
|
+
await provide(context);
|
|
231
299
|
},
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? ignoreHTTPSErrors,
|
|
262
|
-
},
|
|
263
|
-
visual && !('reducedMotion' in options) ? { reducedMotion: VISUAL_MOTION } : {},
|
|
264
|
-
);
|
|
265
|
-
const device = await browser.newContext(settings).catch((error: unknown) => {
|
|
266
|
-
giveBack();
|
|
267
|
-
throw error;
|
|
268
|
-
});
|
|
269
|
-
opened.push(device);
|
|
270
|
-
device.once('close', giveBack);
|
|
271
|
-
await joinEdge(device, stack.baseURL, row === undefined ? 'wired' : row.network);
|
|
272
|
-
return device.newPage();
|
|
273
|
-
};
|
|
274
|
-
try {
|
|
275
|
-
await provide({ newDevice });
|
|
276
|
-
} finally {
|
|
277
|
-
await Promise.all(opened.map(async (device) => device.close()));
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
/** Measures every document, and slows the CPU on Chromium's low tier (DevTools protocol). */
|
|
281
|
-
page: async ({ page, row, browserName, stack }, provide): Promise<void> => {
|
|
282
|
-
await installVitals(page, stack.apiPrefix);
|
|
283
|
-
const rate = slowdownOf(row, browserName);
|
|
284
|
-
if (rate > 1) {
|
|
285
|
-
const session = await page.context().newCDPSession(page);
|
|
286
|
-
await session.send('Emulation.setCPUThrottlingRate', { rate });
|
|
287
|
-
}
|
|
288
|
-
await provide(page);
|
|
289
|
-
},
|
|
290
|
-
channels: [{}, { option: true }],
|
|
291
|
-
reconnect: [undefined, { option: true }],
|
|
292
|
-
keyboardOnly: [false, { option: true }],
|
|
293
|
-
/** A CPU turn, taken after the stack hold as every turn is, spans the test's own work; the
|
|
294
|
-
* override keeps the hold's own timeout, so none of the wait is charged to the test's. */
|
|
295
|
-
stackHold: async ({ stackHold }, provide): Promise<void> => {
|
|
296
|
-
const release = await holdTestTurn();
|
|
297
|
-
try {
|
|
298
|
-
await provide(stackHold);
|
|
299
|
-
} finally {
|
|
300
|
-
release();
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
meter: [
|
|
304
|
-
async ({ page, stack, channels, reconnect }, provide): Promise<void> => {
|
|
305
|
-
const options = { apiPrefix: stack.apiPrefix, channels, vitals: false, reconnect };
|
|
306
|
-
const meter = process.env[MEASURE_ENV] === '1' ? await startMeter(page, options) : undefined;
|
|
307
|
-
await provide(meter);
|
|
308
|
-
if (meter !== undefined) {
|
|
309
|
-
meter.traffic.stop();
|
|
300
|
+
devices: async (
|
|
301
|
+
{ context: _context, browser, browserContexts, ignoreHTTPSErrors, row, stack, visual },
|
|
302
|
+
provide,
|
|
303
|
+
): Promise<void> => {
|
|
304
|
+
const opened: BrowserContext[] = [];
|
|
305
|
+
const reserve = deviceBudget(browserContexts);
|
|
306
|
+
const newDevice = async (options: BrowserContextOptions = {}): Promise<Page> => {
|
|
307
|
+
const giveBack = reserve();
|
|
308
|
+
const settings = Object.assign(
|
|
309
|
+
structuredClone(options),
|
|
310
|
+
{
|
|
311
|
+
baseURL: options.baseURL ?? stack.baseURL,
|
|
312
|
+
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? ignoreHTTPSErrors,
|
|
313
|
+
},
|
|
314
|
+
visual && !('reducedMotion' in options) ? { reducedMotion: VISUAL_MOTION } : {},
|
|
315
|
+
);
|
|
316
|
+
const device = await browser.newContext(settings).catch((error: unknown) => {
|
|
317
|
+
giveBack();
|
|
318
|
+
throw error;
|
|
319
|
+
});
|
|
320
|
+
opened.push(device);
|
|
321
|
+
device.once('close', giveBack);
|
|
322
|
+
await joinEdge(device, stack.baseURL, row === undefined ? 'wired' : row.network);
|
|
323
|
+
return device.newPage();
|
|
324
|
+
};
|
|
325
|
+
try {
|
|
326
|
+
await provide({ newDevice });
|
|
327
|
+
} finally {
|
|
328
|
+
await Promise.all(opened.map(async (device) => device.close()));
|
|
310
329
|
}
|
|
311
330
|
},
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const { origin } = new URL(stack.baseURL);
|
|
320
|
-
const { apiPrefix, notices, cspExceptions } = stack;
|
|
321
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
322
|
-
const exceptions = { list: cspExceptions ?? [], engine: browserName, today };
|
|
323
|
-
const watched = await watch(page, { origin, apiPrefix, notices, keyboardOnly, exceptions });
|
|
324
|
-
await provide(watched);
|
|
325
|
-
const problems = await watched.take();
|
|
326
|
-
if (testInfo.status !== testInfo.expectedStatus) {
|
|
327
|
-
const body = failureReport(page, problems, watched.recentApi());
|
|
328
|
-
await testInfo.attach(FAILURE_ATTACHMENT, { body, contentType: 'text/plain' });
|
|
331
|
+
/** Measures every document, and slows the CPU on Chromium's low tier (DevTools protocol). */
|
|
332
|
+
page: async ({ page, row, browserName, stack }, provide): Promise<void> => {
|
|
333
|
+
await installVitals(page, stack.apiPrefix);
|
|
334
|
+
const rate = slowdownOf(row, browserName);
|
|
335
|
+
if (rate > 1) {
|
|
336
|
+
const session = await page.context().newCDPSession(page);
|
|
337
|
+
await session.send('Emulation.setCPUThrottlingRate', { rate });
|
|
329
338
|
}
|
|
330
|
-
|
|
339
|
+
await provide(page);
|
|
331
340
|
},
|
|
332
|
-
{
|
|
333
|
-
|
|
334
|
-
}
|
|
341
|
+
channels: [{}, { option: true }],
|
|
342
|
+
reconnect: [undefined, { option: true }],
|
|
343
|
+
keyboardOnly: [false, { option: true }],
|
|
344
|
+
/** Taken after the stack hold, so every holder is a test with its stack, and before the CPU
|
|
345
|
+
* turn, which a holder may wait on: a test waiting for seats holds no turn. The wait is not the
|
|
346
|
+
* test's, and fails should the seats stand still (stack/lock.ts). */
|
|
347
|
+
browserSeat: [
|
|
348
|
+
async (
|
|
349
|
+
{ stackHold: _held, browserName, browserContexts, visual },
|
|
350
|
+
provide,
|
|
351
|
+
testInfo,
|
|
352
|
+
): Promise<void> => {
|
|
353
|
+
if (!servedByImage(browserName, visual) || process.env[HOST_ENV] === undefined) {
|
|
354
|
+
await provide(false);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const release = await waitedFor(async () => holdBrowserSeat(browserContexts));
|
|
358
|
+
announceTimeout(testInfo.timeout);
|
|
359
|
+
try {
|
|
360
|
+
await provide(true);
|
|
361
|
+
} finally {
|
|
362
|
+
release();
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
{ timeout: 0 },
|
|
366
|
+
],
|
|
367
|
+
/** A CPU turn, taken last as every turn is, spans the test's own work; its wait is not the
|
|
368
|
+
* test's. */
|
|
369
|
+
cpuTurn: [
|
|
370
|
+
async ({ browserSeat: _seated }, provide, testInfo): Promise<void> => {
|
|
371
|
+
const release = await waitedFor(holdTestTurn);
|
|
372
|
+
announceTimeout(testInfo.timeout);
|
|
373
|
+
try {
|
|
374
|
+
await provide(true);
|
|
375
|
+
} finally {
|
|
376
|
+
release();
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{ auto: true, timeout: 0 },
|
|
380
|
+
],
|
|
381
|
+
meter: [
|
|
382
|
+
async ({ page, stack, channels, reconnect }, provide): Promise<void> => {
|
|
383
|
+
const options = { apiPrefix: stack.apiPrefix, channels, vitals: false, reconnect };
|
|
384
|
+
const meter =
|
|
385
|
+
process.env[MEASURE_ENV] === '1' ? await startMeter(page, options) : undefined;
|
|
386
|
+
await provide(meter);
|
|
387
|
+
if (meter !== undefined) {
|
|
388
|
+
meter.traffic.stop();
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{ auto: true },
|
|
392
|
+
],
|
|
393
|
+
/** The report is waited for by an event on its folder, within a bound of its own, outside the
|
|
394
|
+
* test's time: macOS writes it some seconds after the crash. */
|
|
395
|
+
browserCrash: [browserWatch(), { auto: true, timeout: REPORT_WAIT_MS + 30_000 }],
|
|
396
|
+
invariants: [
|
|
397
|
+
async ({ page, stack, keyboardOnly, browserName }, provide, testInfo): Promise<void> => {
|
|
398
|
+
const { origin } = new URL(stack.baseURL);
|
|
399
|
+
const { apiPrefix, notices, cspExceptions } = stack;
|
|
400
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
401
|
+
const exceptions = { list: cspExceptions ?? [], engine: browserName, today };
|
|
402
|
+
const watched = await watch(page, { origin, apiPrefix, notices, keyboardOnly, exceptions });
|
|
403
|
+
await provide(watched);
|
|
404
|
+
const problems = await watched.take();
|
|
405
|
+
if (testInfo.status !== testInfo.expectedStatus) {
|
|
406
|
+
const body = failureReport(page, problems, watched.recentApi());
|
|
407
|
+
await testInfo.attach(FAILURE_ATTACHMENT, { body, contentType: 'text/plain' });
|
|
408
|
+
}
|
|
409
|
+
expect(problems, 'the page held every invariant').toEqual([]);
|
|
410
|
+
},
|
|
411
|
+
{ auto: true },
|
|
412
|
+
],
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
335
415
|
|
|
336
416
|
export { expect } from '@playwright/test';
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// A workspace's tests as `node --test` runs them (its arguments: globs and `--import <module>`),
|
|
3
|
-
// those timing code then alone, one at a time
|
|
4
|
-
// KIT_TEST_PHASE=untimed or timed takes one phase, so the
|
|
3
|
+
// those timing code then alone, one at a time, holding the machine's CPU gate whole; the rest
|
|
4
|
+
// under coverage, held to its floors. KIT_TEST_PHASE=untimed or timed takes one phase, so the
|
|
5
|
+
// timed run can have the machine alone.
|
|
5
6
|
import { finished, pipeline } from 'node:stream/promises';
|
|
6
7
|
import { run, type RunOptions } from 'node:test';
|
|
7
8
|
import { spec as SpecReporter } from 'node:test/reporters';
|
|
8
9
|
import { parseArgs } from 'node:util';
|
|
9
10
|
|
|
11
|
+
import { inWholeTurn } from '../stack/gate-member.ts';
|
|
12
|
+
import { GATE_PORT } from '../stack/gate-server.ts';
|
|
10
13
|
import { holdFloors, type Measured } from './hold.ts';
|
|
11
14
|
import { measuredOnce } from './instances.ts';
|
|
12
15
|
import { PHASE_ENV, phaseOf, testFiles } from './timed.ts';
|
|
@@ -60,6 +63,19 @@ const measure = async (): Promise<Measured> => {
|
|
|
60
63
|
};
|
|
61
64
|
|
|
62
65
|
const name = process.env['npm_package_name'] ?? cwd;
|
|
66
|
+
|
|
67
|
+
/** The CPU gate's port: the machine's own, or the one a test of this runner serves. */
|
|
68
|
+
const GATE_PORT_ENV = 'KIT_CPU_GATE_PORT';
|
|
69
|
+
|
|
70
|
+
/** Runs the timed files one at a time, holding every slot of the machine's CPU gate: the tests of
|
|
71
|
+
* other runs, which take their turns there, wait, so none shares the cores they're timed on. */
|
|
72
|
+
const timedPass = async (files: string[]): Promise<boolean> =>
|
|
73
|
+
inWholeTurn(
|
|
74
|
+
async () => passes(files, false),
|
|
75
|
+
`the timed tests of ${name} (pid ${String(process.pid)})`,
|
|
76
|
+
Number(process.env[GATE_PORT_ENV] ?? GATE_PORT),
|
|
77
|
+
);
|
|
78
|
+
|
|
63
79
|
const phase = phaseOf(process.env[PHASE_ENV]);
|
|
64
80
|
const untimed = phase !== 'timed';
|
|
65
81
|
const timedToo = phase !== 'untimed';
|
|
@@ -75,5 +91,5 @@ if (!untimed) {
|
|
|
75
91
|
// A timed test shares no CPU with the others: it runs once they are done, and on its own.
|
|
76
92
|
const passed =
|
|
77
93
|
(!untimed || (await passes(parallel, true))) &&
|
|
78
|
-
(!timedToo || timed.length === 0 || (await
|
|
94
|
+
(!timedToo || timed.length === 0 || (await timedPass(timed)));
|
|
79
95
|
process.exitCode = passed && (!untimed || holdFloors(name, cwd, await measure())) ? 0 : 1;
|