@uniflowed/test 0.4.0 → 0.6.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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import type { BrowserTransport, ControlOptions } from "./cdp.js";
|
|
3
|
+
import { runnerFetch } from "./runner-fetch.js";
|
|
3
4
|
|
|
4
5
|
export async function createTransport(options: ControlOptions): Promise<BrowserTransport> {
|
|
5
6
|
if (options.executable != null)
|
|
@@ -12,7 +13,8 @@ export async function createTransport(options: ControlOptions): Promise<BrowserT
|
|
|
12
13
|
method: string,
|
|
13
14
|
args: $ReadOnlyArray<mixed> = [],
|
|
14
15
|
): Promise<$FlowFixMe> {
|
|
15
|
-
|
|
16
|
+
// The platform's `fetch`, not the global a test's request mock replaced.
|
|
17
|
+
const response = await runnerFetch("/uf-test/browser", {
|
|
16
18
|
method: "POST",
|
|
17
19
|
headers: { "content-type": "application/json", "uf-test-browser": token },
|
|
18
20
|
body: JSON.stringify({ id, method, args }),
|
package/internal/browser/page.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import * as output from "../output.js";
|
|
38
38
|
import { run } from "../run.js";
|
|
39
39
|
import { installInSourceTests } from "../../in-source.js";
|
|
40
|
+
import { runnerFetch } from "./runner-fetch.js";
|
|
40
41
|
|
|
41
42
|
/** One file, as the server hands it over. */
|
|
42
43
|
type PageRequest = {
|
|
@@ -74,7 +75,9 @@ function flush(): Promise<void> {
|
|
|
74
75
|
if (already != null) return already;
|
|
75
76
|
if (queue.length === 0) return Promise.resolve();
|
|
76
77
|
const batch = queue.splice(0, queue.length);
|
|
77
|
-
|
|
78
|
+
// Not the global: a file that installed a request mock owns that one, and
|
|
79
|
+
// would answer the runner's report as an unhandled request.
|
|
80
|
+
const sent = runnerFetch("/uf-test/events", {
|
|
78
81
|
method: "POST",
|
|
79
82
|
headers: { "content-type": "application/json" },
|
|
80
83
|
body: JSON.stringify(batch),
|
|
@@ -195,7 +198,7 @@ function page(): $FlowFixMe {
|
|
|
195
198
|
async function serve(): Promise<void> {
|
|
196
199
|
let request: PageRequest;
|
|
197
200
|
try {
|
|
198
|
-
request = await (await
|
|
201
|
+
request = await (await runnerFetch("/uf-test/next")).json();
|
|
199
202
|
} catch {
|
|
200
203
|
// The driver has gone. Nothing to report it to.
|
|
201
204
|
return;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/test`: the `fetch` the browser runner talks to `uf`
|
|
4
|
+
// with.
|
|
5
|
+
//
|
|
6
|
+
// A page reports its results over HTTP — `POST /uf-test/events`, and
|
|
7
|
+
// `POST /uf-test/browser` for `createBrowser` — and the file under test runs
|
|
8
|
+
// in that same page. A file that installs a request mock replaces
|
|
9
|
+
// `globalThis.fetch`, and `@uniflowed/mock`'s default policy rejects every
|
|
10
|
+
// request no handler claimed. Reached through the global, the runner's own
|
|
11
|
+
// requests would be answered by the test's mock: rejected, and the rejection
|
|
12
|
+
// swallowed by the reporter, so the results would never reach `uf test`
|
|
13
|
+
// (ubugeeei-prod/uf#1394).
|
|
14
|
+
//
|
|
15
|
+
// So the harness captures the platform's `fetch` in a classic script that runs
|
|
16
|
+
// before any module — before the runner, and so before any test file could
|
|
17
|
+
// replace it — and the runner's requests go through that capture. A constant
|
|
18
|
+
// captured in this module would depend on when it was first evaluated and on
|
|
19
|
+
// `page.js` and the import map reaching it by the same URL; the harness
|
|
20
|
+
// script depends on neither.
|
|
21
|
+
|
|
22
|
+
/** Where `server.js`'s harness leaves the platform's `fetch`. */
|
|
23
|
+
export const RUNNER_FETCH: symbol = Symbol.for("uf.test.browser.fetch");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `fetch`, as it was before any test file ran.
|
|
27
|
+
*
|
|
28
|
+
* Falls back to the global when nothing was captured, which is a page not
|
|
29
|
+
* served by `uf test --browser` — there is no test file ahead of it to have
|
|
30
|
+
* replaced anything.
|
|
31
|
+
*/
|
|
32
|
+
export function runnerFetch(input: RequestInfo, init?: RequestOptions): Promise<Response> {
|
|
33
|
+
const captured = Reflect.get(globalThis, RUNNER_FETCH);
|
|
34
|
+
if (typeof captured === "function") {
|
|
35
|
+
return captured(input, init);
|
|
36
|
+
}
|
|
37
|
+
return fetch(input, init);
|
|
38
|
+
}
|
|
@@ -639,6 +639,10 @@ function harnessHtml(map: string, browserToken: string): string {
|
|
|
639
639
|
'<meta charset="utf-8">',
|
|
640
640
|
"<title>uf test</title>",
|
|
641
641
|
`<script>globalThis[Symbol.for("uf.test.browser.token")]=${JSON.stringify(browserToken)}</script>`,
|
|
642
|
+
// Before any module, so before any test file can replace it: the runner
|
|
643
|
+
// reports through this `fetch`, never through a mocked global. See
|
|
644
|
+
// `./runner-fetch.js`.
|
|
645
|
+
'<script>globalThis[Symbol.for("uf.test.browser.fetch")]=globalThis.fetch.bind(globalThis)</script>',
|
|
642
646
|
`<script type="importmap">${map}</script>`,
|
|
643
647
|
`<script type="module" src="${page}"></script>`,
|
|
644
648
|
"",
|
package/internal/frames.js
CHANGED
|
@@ -252,6 +252,14 @@ function sourceMapsForCallSites(): FindSourceMap | false {
|
|
|
252
252
|
nodeSourceMaps = false;
|
|
253
253
|
return false;
|
|
254
254
|
}
|
|
255
|
+
// A `uf test` worker whose loader maps lazily holds the maps Node would
|
|
256
|
+
// have, and answers for them the way `findSourceMap` would; see
|
|
257
|
+
// `@uniflowed/host`'s `internal/lazy-source-maps.js`.
|
|
258
|
+
const lazy = host[Symbol.for("@uniflowed/host/source-maps")];
|
|
259
|
+
if (typeof lazy?.findSourceMap === "function") {
|
|
260
|
+
nodeSourceMaps = (file) => lazy.findSourceMap(file);
|
|
261
|
+
return nodeSourceMaps;
|
|
262
|
+
}
|
|
255
263
|
const findSourceMap = process.getBuiltinModule("node:module")?.findSourceMap;
|
|
256
264
|
nodeSourceMaps =
|
|
257
265
|
typeof findSourceMap === "function"
|
package/internal/run.js
CHANGED
|
@@ -75,6 +75,19 @@ export type RunOptions = {|
|
|
|
75
75
|
* round. `uf test --bench` sets it.
|
|
76
76
|
*/
|
|
77
77
|
readonly bench?: boolean,
|
|
78
|
+
/**
|
|
79
|
+
* Run only share `index` of `count` of the file's cases, and report nothing
|
|
80
|
+
* about the rest.
|
|
81
|
+
*
|
|
82
|
+
* `uf test` splits a file long enough to hold up a run across several
|
|
83
|
+
* workers, and each is handed one share: the cases are numbered in the
|
|
84
|
+
* order the file registered them, and share `index` is the contiguous run
|
|
85
|
+
* from `index * n / count` to `(index + 1) * n / count`. Every worker
|
|
86
|
+
* imports the whole file and walks the same tree, so the shares between
|
|
87
|
+
* them report every case once. A case outside the share is not reported at
|
|
88
|
+
* all — not as skipped — because another worker is reporting it.
|
|
89
|
+
*/
|
|
90
|
+
readonly part?: {| readonly index: number, readonly count: number |} | null,
|
|
78
91
|
|};
|
|
79
92
|
|
|
80
93
|
/** Default budget for one case, matching what most runners use. */
|
|
@@ -381,7 +394,7 @@ async function runSuite(
|
|
|
381
394
|
options: RunOptions,
|
|
382
395
|
onlyMode: boolean,
|
|
383
396
|
emit: (result: Result) => void,
|
|
384
|
-
state: {| bail: boolean |},
|
|
397
|
+
state: {| bail: boolean, readonly owned: Set<Case> | null |},
|
|
385
398
|
setUpAncestors: () => Promise<void>,
|
|
386
399
|
): Promise<boolean> {
|
|
387
400
|
const skipped = context.skipped || node.modifier === "skip" || node.modifier === "todo";
|
|
@@ -430,6 +443,10 @@ async function runSuite(
|
|
|
430
443
|
break;
|
|
431
444
|
}
|
|
432
445
|
if (child.kind !== "suite") {
|
|
446
|
+
// Another worker's share: it reports this case, and sets up for it.
|
|
447
|
+
if (state.owned != null && !state.owned.has(child)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
433
450
|
const willRun =
|
|
434
451
|
!inner.skipped &&
|
|
435
452
|
child.modifier !== "skip" &&
|
|
@@ -490,6 +507,7 @@ async function runSuite(
|
|
|
490
507
|
export async function run(options: RunOptions, emit: (result: Result) => void): Promise<void> {
|
|
491
508
|
const root = collected();
|
|
492
509
|
const onlyMode = hasOnly(root, false);
|
|
510
|
+
const owned = share(root, options.part);
|
|
493
511
|
const context: Context = {
|
|
494
512
|
path: [],
|
|
495
513
|
beforeEach: [],
|
|
@@ -497,5 +515,36 @@ export async function run(options: RunOptions, emit: (result: Result) => void):
|
|
|
497
515
|
skipped: false,
|
|
498
516
|
onlyPath: !onlyMode,
|
|
499
517
|
};
|
|
500
|
-
await runSuite(root, context, options, onlyMode, emit, { bail: false }, async () => {});
|
|
518
|
+
await runSuite(root, context, options, onlyMode, emit, { bail: false, owned }, async () => {});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The cases of `part`, or `null` for a run of every case.
|
|
523
|
+
*
|
|
524
|
+
* Numbered in the order a walk of the tree meets them, which is the order the
|
|
525
|
+
* file registered them and the order `runSuite` runs them in — the same in
|
|
526
|
+
* every worker that imported the file, which is what lets each work out its
|
|
527
|
+
* own share without being told the others'.
|
|
528
|
+
*/
|
|
529
|
+
function share(
|
|
530
|
+
root: Suite,
|
|
531
|
+
part: ?{| readonly index: number, readonly count: number |},
|
|
532
|
+
): Set<Case> | null {
|
|
533
|
+
if (part == null || part.count < 2) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
const cases: Array<Case> = [];
|
|
537
|
+
const walk = (node: Suite) => {
|
|
538
|
+
for (const child of node.children) {
|
|
539
|
+
if (child.kind === "suite") {
|
|
540
|
+
walk(child);
|
|
541
|
+
} else {
|
|
542
|
+
cases.push(child);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
walk(root);
|
|
547
|
+
const from = Math.floor((part.index * cases.length) / part.count);
|
|
548
|
+
const to = Math.floor(((part.index + 1) * cases.length) / part.count);
|
|
549
|
+
return new Set(cases.slice(from, to));
|
|
501
550
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The test API and worker for `uf test`: describe/it, a full matcher set, and the process uf fans test files out to.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"!*.test.js"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@uniflowed/host": "0.
|
|
41
|
+
"@uniflowed/host": "0.6.0",
|
|
42
42
|
"pixelmatch": "^7.1.0",
|
|
43
43
|
"pngjs": "^7.0.0"
|
|
44
44
|
},
|
package/worker.js
CHANGED
|
@@ -86,6 +86,8 @@ type Request = {|
|
|
|
86
86
|
*/
|
|
87
87
|
readonly invalidate?: $ReadOnlyArray<string>,
|
|
88
88
|
readonly filter?: string | null,
|
|
89
|
+
/** Run only this share of the file's cases; see `RunOptions.part`. */
|
|
90
|
+
readonly part?: {| readonly index: number, readonly count: number |} | null,
|
|
89
91
|
readonly timeoutMs?: number,
|
|
90
92
|
/**
|
|
91
93
|
* Which request this is, counting from one within this worker.
|
|
@@ -259,6 +261,7 @@ async function runImportedFile(
|
|
|
259
261
|
await run(
|
|
260
262
|
{
|
|
261
263
|
filter: request.filter ?? null,
|
|
264
|
+
part: request.part ?? null,
|
|
262
265
|
timeoutMs: request.timeoutMs,
|
|
263
266
|
file: absolute,
|
|
264
267
|
bench: benching(),
|