@itmetsam/samurai-framework 0.21.5 → 0.22.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/dist/api.d.ts +8 -3
- package/dist/api.js +1 -0
- package/dist/browser/browser-context.d.ts +18 -6
- package/dist/browser/browser-context.js +25 -7
- package/dist/browser/browser.d.ts +4 -4
- package/dist/browser/browser.js +11 -8
- package/dist/browser/chrome-launch.js +9 -1
- package/dist/browser/context-cookie-store.d.ts +17 -0
- package/dist/browser/context-cookie-store.js +90 -0
- package/dist/browser/index.d.ts +3 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/keyboard.d.ts +35 -0
- package/dist/browser/keyboard.js +83 -0
- package/dist/browser/mouse.d.ts +42 -0
- package/dist/browser/mouse.js +66 -0
- package/dist/browser/page-content.d.ts +4 -0
- package/dist/browser/page-content.js +12 -0
- package/dist/browser/page-events.d.ts +11 -0
- package/dist/browser/page-events.js +23 -0
- package/dist/browser/page-logging.d.ts +24 -0
- package/dist/browser/page-logging.js +43 -0
- package/dist/browser/page-navigation.d.ts +0 -2
- package/dist/browser/page-navigation.js +1 -6
- package/dist/browser/page-settings.d.ts +1 -1
- package/dist/browser/page-viewport.d.ts +21 -0
- package/dist/browser/page-viewport.js +33 -0
- package/dist/browser/page.d.ts +23 -16
- package/dist/browser/page.js +48 -71
- package/dist/browser/screenshot.d.ts +6 -0
- package/dist/browser/screenshot.js +14 -0
- package/dist/browser/storage-state.d.ts +0 -2
- package/dist/browser/storage-state.js +0 -20
- package/dist/browser/use-applier.d.ts +16 -5
- package/dist/browser/use-applier.js +44 -17
- package/dist/browser/use-commands.d.ts +36 -0
- package/dist/browser/use-commands.js +120 -0
- package/dist/cli/args.d.ts +8 -4
- package/dist/cli/args.js +8 -5
- package/dist/cli/console-reporter.js +5 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +4 -1
- package/dist/config/base-url.d.ts +4 -0
- package/dist/config/base-url.js +6 -0
- package/dist/config/config.js +7 -0
- package/dist/config/fixture-use.d.ts +19 -0
- package/dist/config/fixture-use.js +44 -0
- package/dist/config/fully-parallel.d.ts +3 -0
- package/dist/config/fully-parallel.js +6 -0
- package/dist/config/group-use.d.ts +10 -0
- package/dist/config/group-use.js +51 -0
- package/dist/config/resolved-config.d.ts +2 -0
- package/dist/config/resolved-config.js +3 -0
- package/dist/config/storage-state-file.d.ts +3 -0
- package/dist/config/storage-state-file.js +20 -0
- package/dist/config/use-options.d.ts +3 -1
- package/dist/config/use-options.js +74 -14
- package/dist/config/web-server-options.d.ts +7 -0
- package/dist/config/web-server-options.js +111 -0
- package/dist/locator/check-actions.js +1 -1
- package/dist/locator/drag-actions.d.ts +17 -0
- package/dist/locator/drag-actions.js +80 -0
- package/dist/locator/element-actions.d.ts +10 -4
- package/dist/locator/element-actions.js +23 -16
- package/dist/locator/element-state.d.ts +5 -0
- package/dist/locator/element-state.js +2 -1
- package/dist/locator/event-actions.d.ts +10 -0
- package/dist/locator/event-actions.js +81 -0
- package/dist/locator/input-actions.d.ts +2 -11
- package/dist/locator/input-actions.js +3 -56
- package/dist/locator/key-actions.d.ts +11 -0
- package/dist/locator/key-actions.js +52 -0
- package/dist/locator/key-map.d.ts +16 -0
- package/dist/locator/key-map.js +76 -0
- package/dist/locator/locator.d.ts +35 -5
- package/dist/locator/locator.js +70 -16
- package/dist/locator/page-scripts.d.ts +2 -0
- package/dist/locator/page-scripts.js +7 -0
- package/dist/locator/pointer-actions.d.ts +44 -0
- package/dist/locator/pointer-actions.js +117 -0
- package/dist/locator/pointer-element-actions.d.ts +21 -0
- package/dist/locator/pointer-element-actions.js +42 -0
- package/dist/request/api-request-context.d.ts +65 -0
- package/dist/request/api-request-context.js +198 -0
- package/dist/request/api-response.d.ts +34 -0
- package/dist/request/api-response.js +57 -0
- package/dist/request/cookie-jar.d.ts +48 -0
- package/dist/request/cookie-jar.js +164 -0
- package/dist/request/http-transport.d.ts +31 -0
- package/dist/request/http-transport.js +113 -0
- package/dist/request/redirect.d.ts +10 -0
- package/dist/request/redirect.js +21 -0
- package/dist/request/request-body.d.ts +27 -0
- package/dist/request/request-body.js +71 -0
- package/dist/request/request.d.ts +16 -0
- package/dist/request/request.js +20 -0
- package/dist/runner/all-hooks.d.ts +7 -1
- package/dist/runner/all-hooks.js +17 -1
- package/dist/runner/fixture-defs.d.ts +2 -1
- package/dist/runner/fixture-defs.js +52 -18
- package/dist/runner/fixture-meta.d.ts +20 -0
- package/dist/runner/fixture-meta.js +109 -0
- package/dist/runner/fixture-options.d.ts +8 -0
- package/dist/runner/fixture-options.js +35 -0
- package/dist/runner/fixture-outputs.d.ts +11 -0
- package/dist/runner/fixture-outputs.js +28 -0
- package/dist/runner/fixture-registry.d.ts +35 -0
- package/dist/runner/fixture-registry.js +73 -0
- package/dist/runner/fixture-scope.d.ts +39 -9
- package/dist/runner/fixture-scope.js +139 -70
- package/dist/runner/fixture-start.d.ts +27 -0
- package/dist/runner/fixture-start.js +76 -0
- package/dist/runner/fixture-use.d.ts +23 -0
- package/dist/runner/fixture-use.js +64 -0
- package/dist/runner/fixture-worker.d.ts +26 -0
- package/dist/runner/fixture-worker.js +46 -0
- package/dist/runner/fixtures.d.ts +4 -1
- package/dist/runner/fixtures.js +3 -2
- package/dist/runner/global-hooks.d.ts +13 -0
- package/dist/runner/global-hooks.js +66 -0
- package/dist/runner/options.d.ts +13 -2
- package/dist/runner/output-tail.d.ts +14 -0
- package/dist/runner/output-tail.js +30 -0
- package/dist/runner/pool-schedule.d.ts +22 -0
- package/dist/runner/pool-schedule.js +55 -0
- package/dist/runner/pool-units.d.ts +34 -0
- package/dist/runner/pool-units.js +85 -0
- package/dist/runner/pooled-run.d.ts +7 -5
- package/dist/runner/pooled-run.js +19 -9
- package/dist/runner/prepare-run.js +5 -0
- package/dist/runner/registry.js +21 -7
- package/dist/runner/reporter.d.ts +13 -1
- package/dist/runner/reporter.js +13 -2
- package/dist/runner/request-fixture.d.ts +14 -0
- package/dist/runner/request-fixture.js +25 -0
- package/dist/runner/run-hooks.d.ts +16 -0
- package/dist/runner/run-hooks.js +52 -0
- package/dist/runner/run-test.d.ts +3 -0
- package/dist/runner/run-test.js +23 -3
- package/dist/runner/run.d.ts +8 -0
- package/dist/runner/run.js +10 -0
- package/dist/runner/select.d.ts +15 -4
- package/dist/runner/select.js +38 -12
- package/dist/runner/serial-run.d.ts +23 -0
- package/dist/runner/serial-run.js +88 -0
- package/dist/runner/serial.d.ts +34 -0
- package/dist/runner/serial.js +63 -0
- package/dist/runner/shard.d.ts +22 -0
- package/dist/runner/shard.js +73 -0
- package/dist/runner/suite.d.ts +3 -3
- package/dist/runner/suite.js +4 -3
- package/dist/runner/test-function.d.ts +15 -3
- package/dist/runner/test-info.js +2 -1
- package/dist/runner/test-runner.d.ts +1 -0
- package/dist/runner/test-runner.js +67 -18
- package/dist/runner/use-options.d.ts +18 -4
- package/dist/runner/use-options.js +33 -6
- package/dist/runner/wait-for-url.d.ts +20 -0
- package/dist/runner/wait-for-url.js +65 -0
- package/dist/runner/web-server.d.ts +16 -0
- package/dist/runner/web-server.js +165 -0
- package/dist/runner/worker-pool.d.ts +13 -5
- package/dist/runner/worker-pool.js +85 -51
- package/dist/runner/worker-protocol.d.ts +8 -0
- package/dist/runner/worker-session.js +9 -1
- package/dist/runner/worker.d.ts +2 -0
- package/dist/runner/worker.js +7 -1
- package/dist/types/config.d.ts +117 -21
- package/dist/types/extend.d.ts +54 -2
- package/dist/types/test.d.ts +42 -0
- package/dist/types/use.d.ts +59 -7
- package/package.json +1 -1
- package/dist/browser/emulation.d.ts +0 -61
- package/dist/browser/emulation.js +0 -180
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declared outputs of a fixture that the report and the app may show: the non-secret keys of its value.
|
|
3
|
+
* Throws when a declared output is not a string, a number or a boolean (the value must be a flat record).
|
|
4
|
+
*/
|
|
5
|
+
export function outputsOf(def, value) {
|
|
6
|
+
const shown = (def.outputs ?? []).filter((output) => !output.secret);
|
|
7
|
+
if (shown.length === 0)
|
|
8
|
+
return undefined;
|
|
9
|
+
const record = typeof value === "object" && value !== null
|
|
10
|
+
? value
|
|
11
|
+
: {};
|
|
12
|
+
const outputs = {};
|
|
13
|
+
for (const { name } of shown) {
|
|
14
|
+
const output = record[name];
|
|
15
|
+
if (typeof output !== "string" &&
|
|
16
|
+
typeof output !== "number" &&
|
|
17
|
+
typeof output !== "boolean")
|
|
18
|
+
throw new Error(`output "${name}" must be a string, number or boolean`);
|
|
19
|
+
outputs[name] = output;
|
|
20
|
+
}
|
|
21
|
+
return outputs;
|
|
22
|
+
}
|
|
23
|
+
/** JSON with the keys of every object in order, so equal options give equal text */
|
|
24
|
+
export function stableJson(value) {
|
|
25
|
+
return JSON.stringify(value, (_key, item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
26
|
+
? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b)))
|
|
27
|
+
: item);
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FixtureField, FixtureOutput } from "../types/extend.js";
|
|
2
|
+
import { type FixtureLayers } from "./fixture-defs.js";
|
|
3
|
+
/** Remembers the fixtures of an extended `test`, so `config.fixtures` can be read without running a test */
|
|
4
|
+
export declare function rememberLayers(test: object, layers: FixtureLayers): void;
|
|
5
|
+
/** The fixtures of an extended `test`; nothing for the plain one */
|
|
6
|
+
export declare const layersOf: (test: object) => FixtureLayers | undefined;
|
|
7
|
+
/** What the app and the CLI show of one fixture of a project, read without running a test */
|
|
8
|
+
export interface FixtureCatalogEntry {
|
|
9
|
+
name: string;
|
|
10
|
+
/** The `label` option, else the name */
|
|
11
|
+
label: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
scope: "test" | "worker";
|
|
15
|
+
/** Set up for every test */
|
|
16
|
+
auto: boolean;
|
|
17
|
+
/** The name is a built-in fixture's: this one replaces it */
|
|
18
|
+
overridesBuiltIn: boolean;
|
|
19
|
+
/** The fixtures it destructures, built-in ones included */
|
|
20
|
+
dependencies: string[];
|
|
21
|
+
/** The `<name>Options` fixture: its form, and its values when `use` sets none */
|
|
22
|
+
options?: {
|
|
23
|
+
name: string;
|
|
24
|
+
fields: FixtureField[];
|
|
25
|
+
defaults: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
/** The declared keys of its value, without the secret ones */
|
|
28
|
+
outputs?: FixtureOutput[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The catalog of the fixtures in `config.fixtures`, read from the extended tests without running anything. Throws
|
|
32
|
+
* `config.fixtures: ...` for a list entry that is not an extended test, a name that does not match
|
|
33
|
+
* `/^[a-z][A-Za-z0-9]*$/` and a name defined in two places.
|
|
34
|
+
*/
|
|
35
|
+
export declare function fixtureCatalog(tests: readonly unknown[]): FixtureCatalogEntry[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { BUILT_IN_FIXTURES, } from "./fixture-defs.js";
|
|
2
|
+
/** The fixtures each `test.extend()` result carries; the test function itself holds nothing a config could read */
|
|
3
|
+
const layersByTest = new WeakMap();
|
|
4
|
+
/** Remembers the fixtures of an extended `test`, so `config.fixtures` can be read without running a test */
|
|
5
|
+
export function rememberLayers(test, layers) {
|
|
6
|
+
layersByTest.set(test, layers);
|
|
7
|
+
}
|
|
8
|
+
/** The fixtures of an extended `test`; nothing for the plain one */
|
|
9
|
+
export const layersOf = (test) => layersByTest.get(test);
|
|
10
|
+
const NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
11
|
+
const fail = (problem) => {
|
|
12
|
+
throw new Error(`config.fixtures: ${problem}`);
|
|
13
|
+
};
|
|
14
|
+
/** The last definition of every name in the listed tests, with the place of the first test that has it */
|
|
15
|
+
function collect(tests) {
|
|
16
|
+
const found = new Map();
|
|
17
|
+
tests.forEach((test, index) => {
|
|
18
|
+
const layers = (typeof test === "function" ||
|
|
19
|
+
(typeof test === "object" && test !== null)) &&
|
|
20
|
+
layersByTest.has(test)
|
|
21
|
+
? layersByTest.get(test)
|
|
22
|
+
: undefined;
|
|
23
|
+
if (layers === undefined &&
|
|
24
|
+
!(typeof test === "function" && "extend" in test))
|
|
25
|
+
throw new Error(`config.fixtures[${index}] is not a test (use the result of test.extend())`);
|
|
26
|
+
for (const [name, defs] of layers ?? []) {
|
|
27
|
+
const def = defs[defs.length - 1];
|
|
28
|
+
const earlier = found.get(name);
|
|
29
|
+
if (earlier && earlier.def !== def)
|
|
30
|
+
fail(`fixture "${name}" is defined twice (config.fixtures[${earlier.index}] and config.fixtures[${index}])`);
|
|
31
|
+
if (!earlier)
|
|
32
|
+
found.set(name, { def, index });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return new Map([...found].map(([name, { def }]) => [name, def]));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The catalog of the fixtures in `config.fixtures`, read from the extended tests without running anything. Throws
|
|
39
|
+
* `config.fixtures: ...` for a list entry that is not an extended test, a name that does not match
|
|
40
|
+
* `/^[a-z][A-Za-z0-9]*$/` and a name defined in two places.
|
|
41
|
+
*/
|
|
42
|
+
export function fixtureCatalog(tests) {
|
|
43
|
+
if (!Array.isArray(tests))
|
|
44
|
+
throw new Error("config.fixtures must be an array");
|
|
45
|
+
const defs = collect(tests);
|
|
46
|
+
for (const name of defs.keys())
|
|
47
|
+
if (!NAME.test(name))
|
|
48
|
+
fail(`fixture name "${name}" must match ${NAME} (letters and digits, starting with a lower-case letter)`);
|
|
49
|
+
return [...defs.values()]
|
|
50
|
+
.filter((def) => !def.option)
|
|
51
|
+
.map((def) => {
|
|
52
|
+
const option = defs.get(`${def.name}Options`);
|
|
53
|
+
const outputs = def.outputs?.filter((output) => !output.secret);
|
|
54
|
+
return {
|
|
55
|
+
name: def.name,
|
|
56
|
+
label: def.label ?? def.name,
|
|
57
|
+
...(def.icon !== undefined && { icon: def.icon }),
|
|
58
|
+
...(def.description !== undefined && { description: def.description }),
|
|
59
|
+
scope: def.scope,
|
|
60
|
+
auto: def.auto,
|
|
61
|
+
overridesBuiltIn: BUILT_IN_FIXTURES.has(def.name),
|
|
62
|
+
dependencies: def.deps,
|
|
63
|
+
...(option?.option && {
|
|
64
|
+
options: {
|
|
65
|
+
name: option.name,
|
|
66
|
+
fields: option.fields ?? [],
|
|
67
|
+
defaults: { ...option.value },
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
...(outputs && outputs.length > 0 && { outputs }),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -1,33 +1,63 @@
|
|
|
1
|
-
import type { TestFixtures } from "../types/test.js";
|
|
1
|
+
import type { TestFixtureEntry, TestFixtures } from "../types/test.js";
|
|
2
2
|
import { type FixtureLayers } from "./fixture-defs.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { WorkerFixtures } from "./fixture-worker.js";
|
|
4
|
+
/** What `use` and the run's environment say about the fixtures of one test */
|
|
5
|
+
export interface ScopeRun {
|
|
6
|
+
/** Fixtures to set up with the `auto` ones */
|
|
7
|
+
attach?: readonly string[];
|
|
8
|
+
/** The values of the option fixtures from `use`, by option fixture name */
|
|
9
|
+
options?: Readonly<Record<string, Record<string, unknown>>>;
|
|
10
|
+
/** Fixtures the environment turns off, with the reason a test that needs one is skipped for */
|
|
11
|
+
disabled?: ReadonlyMap<string, string>;
|
|
12
|
+
/** The worker's fixtures, where worker-scoped ones live. Without it they live as long as this scope */
|
|
13
|
+
workers?: WorkerFixtures;
|
|
14
|
+
/** The run's environment: a worker fixture is shared only within one environment */
|
|
15
|
+
environment?: string;
|
|
6
16
|
}
|
|
7
17
|
/**
|
|
8
18
|
* The custom fixtures of one test run. Fixtures are set up when a test or hook asks for them (by destructuring
|
|
9
19
|
* them), their dependencies first, and torn down in the opposite order. The hooks and the body of a test share
|
|
10
|
-
* one scope, so they see the same instances.
|
|
20
|
+
* one scope, so they see the same instances. Worker-scoped fixtures come from the worker's `WorkerFixtures`.
|
|
11
21
|
*/
|
|
12
22
|
export declare class FixtureScope {
|
|
13
23
|
private readonly layers;
|
|
14
24
|
private readonly base;
|
|
25
|
+
private readonly run;
|
|
15
26
|
/** The built-in fixtures, with each custom fixture added (replacing a built-in of that name) once it is set up */
|
|
16
27
|
readonly fixtures: TestFixtures;
|
|
17
28
|
private readonly values;
|
|
18
29
|
private readonly pending;
|
|
30
|
+
/** What was set up for this test, last in the order it is torn down */
|
|
19
31
|
private readonly started;
|
|
32
|
+
/** The key of each worker fixture set up here, which the fixtures that use it build theirs from */
|
|
33
|
+
private readonly keys;
|
|
34
|
+
private readonly entries;
|
|
35
|
+
private readonly entryOf;
|
|
36
|
+
private readonly workers;
|
|
37
|
+
private readonly ownsWorkers;
|
|
20
38
|
private closed;
|
|
21
|
-
constructor(layers: FixtureLayers, base: TestFixtures);
|
|
22
|
-
/** Sets up the fixtures marked `auto` */
|
|
39
|
+
constructor(layers: FixtureLayers, base: TestFixtures, run?: ScopeRun);
|
|
40
|
+
/** Sets up the fixtures marked `auto`, then those `use` attaches */
|
|
23
41
|
prepareAuto(): Promise<void>;
|
|
24
42
|
/** Sets up the custom fixtures `fn` destructures from its first parameter */
|
|
25
43
|
prepare(fn: (...args: never[]) => unknown): Promise<void>;
|
|
44
|
+
/** The fixtures this test set up, in the order they were set up, with how each went so far */
|
|
45
|
+
report(): TestFixtureEntry[];
|
|
26
46
|
/**
|
|
27
|
-
* Finishes the fixtures that were set up, last first, each within `limitMs`. Returns the
|
|
28
|
-
* fixture's cleanup threw or ran out of time); the others still run.
|
|
47
|
+
* Finishes the fixtures that were set up, last first, each within its own `timeout` else `limitMs`. Returns the
|
|
48
|
+
* first error (a fixture's cleanup threw or ran out of time); the others still run. The worker fixtures of a
|
|
49
|
+
* scope that has no worker are finished too.
|
|
29
50
|
*/
|
|
30
51
|
teardown(limitMs: number): Promise<Error | undefined>;
|
|
31
52
|
private ensure;
|
|
32
53
|
private setUp;
|
|
54
|
+
/** Runs the setup function of `def` (for this test, or through the worker) and records how it went */
|
|
55
|
+
private instantiate;
|
|
56
|
+
/** Starts the setup of `def` until it calls `use`, and checks the declared outputs of the value */
|
|
57
|
+
private make;
|
|
58
|
+
/**
|
|
59
|
+
* What makes one instance of a worker fixture different from another: the environment, the values of the
|
|
60
|
+
* option fixtures it reads, the instances of the worker fixtures it uses and the built-ins that differ
|
|
61
|
+
*/
|
|
62
|
+
private workerKey;
|
|
33
63
|
}
|
|
@@ -1,65 +1,49 @@
|
|
|
1
|
+
import { toTestError } from "./test-error.js";
|
|
1
2
|
import { requestedBy } from "./fixture-args.js";
|
|
2
3
|
import { BUILT_IN_FIXTURES, dependencyOf, } from "./fixture-defs.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
const messageOf = (err) => err instanceof Error ? err.message : String(err);
|
|
13
|
-
/** Runs `setup` until it calls `use`; resolves with the value and the means to finish it */
|
|
14
|
-
function startSetup(def, args) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
let release = () => { };
|
|
17
|
-
const released = new Promise((r) => (release = r));
|
|
18
|
-
let used = false;
|
|
19
|
-
let running;
|
|
20
|
-
const done = () => running ?? Promise.resolve();
|
|
21
|
-
const use = async (value) => {
|
|
22
|
-
if (used)
|
|
23
|
-
throw new Error("use() was called twice");
|
|
24
|
-
used = true;
|
|
25
|
-
resolve({ value, release, done });
|
|
26
|
-
await released;
|
|
27
|
-
};
|
|
28
|
-
running = (async () => def.setup?.(args, use))();
|
|
29
|
-
running.then(() => {
|
|
30
|
-
if (!used)
|
|
31
|
-
reject(new Error("the setup never called use()"));
|
|
32
|
-
}, (err) => {
|
|
33
|
-
if (!used)
|
|
34
|
-
reject(err);
|
|
35
|
-
});
|
|
36
|
-
// A failure after use() is read by teardown; this keeps it from being reported as unhandled meanwhile
|
|
37
|
-
running.catch(() => { });
|
|
38
|
-
});
|
|
39
|
-
}
|
|
4
|
+
import { resolveOptions } from "./fixture-options.js";
|
|
5
|
+
import { outputsOf, stableJson } from "./fixture-outputs.js";
|
|
6
|
+
import { finishWithin, FixtureSetupError, startSetup, } from "./fixture-start.js";
|
|
7
|
+
import { WorkerFixtures } from "./fixture-worker.js";
|
|
8
|
+
import { SkipSignal } from "./skip.js";
|
|
9
|
+
/** The built-in fixtures whose value tells worker fixtures apart (the rest are the same for every test of a worker) */
|
|
10
|
+
const KEYED_BUILT_INS = ["browserName", "workerIndex", "baseURL"];
|
|
40
11
|
/**
|
|
41
12
|
* The custom fixtures of one test run. Fixtures are set up when a test or hook asks for them (by destructuring
|
|
42
13
|
* them), their dependencies first, and torn down in the opposite order. The hooks and the body of a test share
|
|
43
|
-
* one scope, so they see the same instances.
|
|
14
|
+
* one scope, so they see the same instances. Worker-scoped fixtures come from the worker's `WorkerFixtures`.
|
|
44
15
|
*/
|
|
45
16
|
export class FixtureScope {
|
|
46
|
-
constructor(layers, base) {
|
|
17
|
+
constructor(layers, base, run = {}) {
|
|
47
18
|
this.layers = layers;
|
|
48
19
|
this.base = base;
|
|
20
|
+
this.run = run;
|
|
49
21
|
this.values = new Map();
|
|
50
22
|
this.pending = new Map();
|
|
23
|
+
/** What was set up for this test, last in the order it is torn down */
|
|
51
24
|
this.started = [];
|
|
25
|
+
/** The key of each worker fixture set up here, which the fixtures that use it build theirs from */
|
|
26
|
+
this.keys = new Map();
|
|
27
|
+
this.entries = [];
|
|
28
|
+
this.entryOf = new Map();
|
|
52
29
|
this.closed = false;
|
|
53
30
|
const own = Object.getOwnPropertyDescriptors(base);
|
|
54
31
|
for (const descriptor of Object.values(own))
|
|
55
32
|
descriptor.configurable = true;
|
|
56
33
|
this.fixtures = Object.defineProperties({}, own);
|
|
34
|
+
this.ownsWorkers = run.workers === undefined;
|
|
35
|
+
this.workers = run.workers ?? new WorkerFixtures(60_000);
|
|
57
36
|
}
|
|
58
|
-
/** Sets up the fixtures marked `auto` */
|
|
37
|
+
/** Sets up the fixtures marked `auto`, then those `use` attaches */
|
|
59
38
|
async prepareAuto() {
|
|
60
39
|
for (const [name, defs] of this.layers)
|
|
61
40
|
if (defs[defs.length - 1]?.auto)
|
|
62
41
|
await this.ensure(name, defs.length - 1);
|
|
42
|
+
for (const name of this.run.attach ?? []) {
|
|
43
|
+
const defs = this.layers.get(name);
|
|
44
|
+
if (defs)
|
|
45
|
+
await this.ensure(name, defs.length - 1);
|
|
46
|
+
}
|
|
63
47
|
}
|
|
64
48
|
/** Sets up the custom fixtures `fn` destructures from its first parameter */
|
|
65
49
|
async prepare(fn) {
|
|
@@ -75,36 +59,38 @@ export class FixtureScope {
|
|
|
75
59
|
throw new Error(`extend(): unknown fixture "${name}"`);
|
|
76
60
|
}
|
|
77
61
|
}
|
|
62
|
+
/** The fixtures this test set up, in the order they were set up, with how each went so far */
|
|
63
|
+
report() {
|
|
64
|
+
return this.entries.map((entry) => structuredClone(entry));
|
|
65
|
+
}
|
|
78
66
|
/**
|
|
79
|
-
* Finishes the fixtures that were set up, last first, each within `limitMs`. Returns the
|
|
80
|
-
* fixture's cleanup threw or ran out of time); the others still run.
|
|
67
|
+
* Finishes the fixtures that were set up, last first, each within its own `timeout` else `limitMs`. Returns the
|
|
68
|
+
* first error (a fixture's cleanup threw or ran out of time); the others still run. The worker fixtures of a
|
|
69
|
+
* scope that has no worker are finished too.
|
|
81
70
|
*/
|
|
82
71
|
async teardown(limitMs) {
|
|
83
72
|
this.closed = true;
|
|
84
73
|
let first;
|
|
85
74
|
for (let one = this.started.pop(); one; one = this.started.pop()) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
catch (err) {
|
|
97
|
-
first ??= new Error(`fixture "${one.name}" teardown failed: ${messageOf(err)}`);
|
|
98
|
-
if (err instanceof Error && err.stack)
|
|
99
|
-
first.stack = err.stack;
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
clearTimeout(timer);
|
|
103
|
-
}
|
|
75
|
+
const begin = performance.now();
|
|
76
|
+
const error = await finishWithin(one, limitMs);
|
|
77
|
+
const entry = this.entryOf.get(one);
|
|
78
|
+
if (entry)
|
|
79
|
+
entry.teardown = {
|
|
80
|
+
status: error ? "failed" : "passed",
|
|
81
|
+
durationMs: performance.now() - begin,
|
|
82
|
+
...(error && { error: toTestError(error) }),
|
|
83
|
+
};
|
|
84
|
+
first ??= error;
|
|
104
85
|
}
|
|
86
|
+
if (this.ownsWorkers)
|
|
87
|
+
first ??= (await this.workers.closeAll())[0];
|
|
105
88
|
return first;
|
|
106
89
|
}
|
|
107
90
|
ensure(name, level) {
|
|
91
|
+
const off = this.run.disabled?.get(name);
|
|
92
|
+
if (off !== undefined)
|
|
93
|
+
return Promise.reject(new SkipSignal("skip", off));
|
|
108
94
|
const key = `${name}#${level}`;
|
|
109
95
|
let promise = this.pending.get(key);
|
|
110
96
|
if (!promise) {
|
|
@@ -129,15 +115,10 @@ export class FixtureScope {
|
|
|
129
115
|
}
|
|
130
116
|
}
|
|
131
117
|
let value = def.value;
|
|
132
|
-
if (def.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
throw new Error("the test is already over");
|
|
137
|
-
}
|
|
138
|
-
value = started.value;
|
|
139
|
-
this.started.push({ name, ...started });
|
|
140
|
-
}
|
|
118
|
+
if (def.option)
|
|
119
|
+
value = resolveOptions(def, this.run.options?.[name], this.base);
|
|
120
|
+
if (def.setup)
|
|
121
|
+
value = await this.instantiate(def, level, args);
|
|
141
122
|
this.values.set(`${name}#${level}`, value);
|
|
142
123
|
if (level === (this.layers.get(name)?.length ?? 0) - 1)
|
|
143
124
|
Object.defineProperty(this.fixtures, name, {
|
|
@@ -148,9 +129,97 @@ export class FixtureScope {
|
|
|
148
129
|
});
|
|
149
130
|
}
|
|
150
131
|
catch (err) {
|
|
151
|
-
throw err instanceof FixtureSetupError
|
|
132
|
+
throw err instanceof FixtureSetupError || err instanceof SkipSignal
|
|
152
133
|
? err
|
|
153
134
|
: new FixtureSetupError(name, err);
|
|
154
135
|
}
|
|
155
136
|
}
|
|
137
|
+
/** Runs the setup function of `def` (for this test, or through the worker) and records how it went */
|
|
138
|
+
async instantiate(def, level, args) {
|
|
139
|
+
const entry = {
|
|
140
|
+
name: def.name,
|
|
141
|
+
scope: def.scope,
|
|
142
|
+
setup: { status: "passed", durationMs: 0 },
|
|
143
|
+
};
|
|
144
|
+
const begin = performance.now();
|
|
145
|
+
try {
|
|
146
|
+
let instance;
|
|
147
|
+
if (def.scope === "worker") {
|
|
148
|
+
const key = this.workerKey(def, level);
|
|
149
|
+
this.keys.set(`${def.name}#${level}`, key);
|
|
150
|
+
const got = await this.workers.acquire(def, key, () => this.make(def, args));
|
|
151
|
+
if (got.reused)
|
|
152
|
+
entry.setup.status = "skipped";
|
|
153
|
+
instance = got;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
instance = await this.make(def, args);
|
|
157
|
+
if (this.closed) {
|
|
158
|
+
instance.started.release();
|
|
159
|
+
throw new Error("the test is already over");
|
|
160
|
+
}
|
|
161
|
+
this.started.push(instance.started);
|
|
162
|
+
this.entryOf.set(instance.started, entry);
|
|
163
|
+
}
|
|
164
|
+
if (instance.outputs)
|
|
165
|
+
entry.outputs = instance.outputs;
|
|
166
|
+
return instance.value;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
entry.setup = {
|
|
170
|
+
status: "failed",
|
|
171
|
+
durationMs: 0,
|
|
172
|
+
error: toTestError(new FixtureSetupError(def.name, err)),
|
|
173
|
+
};
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
if (entry.setup.status !== "skipped")
|
|
178
|
+
entry.setup.durationMs = performance.now() - begin;
|
|
179
|
+
this.entries.push(entry);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Starts the setup of `def` until it calls `use`, and checks the declared outputs of the value */
|
|
183
|
+
async make(def, args) {
|
|
184
|
+
const started = await startSetup(def, args, def.timeout);
|
|
185
|
+
const finish = {
|
|
186
|
+
name: def.name,
|
|
187
|
+
release: started.release,
|
|
188
|
+
done: started.done,
|
|
189
|
+
timeout: def.timeout,
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
const outputs = outputsOf(def, started.value);
|
|
193
|
+
return { value: started.value, started: finish, outputs };
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
started.release();
|
|
197
|
+
await started.done().catch(() => undefined);
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* What makes one instance of a worker fixture different from another: the environment, the values of the
|
|
203
|
+
* option fixtures it reads, the instances of the worker fixtures it uses and the built-ins that differ
|
|
204
|
+
*/
|
|
205
|
+
workerKey(def, level) {
|
|
206
|
+
const parts = [this.run.environment ?? ""];
|
|
207
|
+
for (const dep of def.deps) {
|
|
208
|
+
const target = dependencyOf(this.layers, def.name, level, dep);
|
|
209
|
+
if (target === "built-in" || target === undefined) {
|
|
210
|
+
if (KEYED_BUILT_INS.includes(dep))
|
|
211
|
+
parts.push([dep, this.base[dep]]);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const used = this.layers.get(target.name)?.[target.level];
|
|
215
|
+
if (used?.option)
|
|
216
|
+
parts.push([
|
|
217
|
+
dep,
|
|
218
|
+
{ ...used.value, ...this.run.options?.[target.name] },
|
|
219
|
+
]);
|
|
220
|
+
else if (used?.scope === "worker")
|
|
221
|
+
parts.push([dep, this.keys.get(`${target.name}#${target.level}`)]);
|
|
222
|
+
}
|
|
223
|
+
return stableJson(parts);
|
|
224
|
+
}
|
|
156
225
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FixtureDef } from "./fixture-defs.js";
|
|
2
|
+
/** A custom fixture whose setup threw or never called `use` */
|
|
3
|
+
export declare class FixtureSetupError extends Error {
|
|
4
|
+
constructor(name: string, cause: unknown);
|
|
5
|
+
}
|
|
6
|
+
/** A fixture that is set up: `release` lets its setup function finish, `done` is that function's end */
|
|
7
|
+
export interface Started {
|
|
8
|
+
name: string;
|
|
9
|
+
release: () => void;
|
|
10
|
+
done: () => Promise<void>;
|
|
11
|
+
/** The fixture's own `timeout` for its teardown */
|
|
12
|
+
timeout?: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs `setup` until it calls `use`; resolves with the value and the means to finish it. A setup that takes longer
|
|
16
|
+
* than `limitMs` fails; if it calls `use` later anyway, it is let go at once.
|
|
17
|
+
*/
|
|
18
|
+
export declare function startSetup(def: FixtureDef, args: Record<string, unknown>, limitMs?: number): Promise<{
|
|
19
|
+
value: unknown;
|
|
20
|
+
release: () => void;
|
|
21
|
+
done: () => Promise<void>;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Lets a set-up fixture finish its cleanup, within the fixture's own `timeout` else `limitMs`. Returns the error
|
|
25
|
+
* (`fixture "x" teardown failed: ...`) when the cleanup threw or ran out of time.
|
|
26
|
+
*/
|
|
27
|
+
export declare function finishWithin(one: Started, limitMs: number): Promise<Error | undefined>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** A custom fixture whose setup threw or never called `use` */
|
|
2
|
+
export class FixtureSetupError extends Error {
|
|
3
|
+
constructor(name, cause) {
|
|
4
|
+
super(`fixture "${name}" setup failed: ${messageOf(cause)}`);
|
|
5
|
+
this.name = "FixtureSetupError";
|
|
6
|
+
if (cause instanceof Error && cause.stack)
|
|
7
|
+
this.stack = cause.stack;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const messageOf = (err) => err instanceof Error ? err.message : String(err);
|
|
11
|
+
/**
|
|
12
|
+
* Runs `setup` until it calls `use`; resolves with the value and the means to finish it. A setup that takes longer
|
|
13
|
+
* than `limitMs` fails; if it calls `use` later anyway, it is let go at once.
|
|
14
|
+
*/
|
|
15
|
+
export function startSetup(def, args, limitMs) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let release = () => { };
|
|
18
|
+
const released = new Promise((r) => (release = r));
|
|
19
|
+
let used = false;
|
|
20
|
+
let running;
|
|
21
|
+
const done = () => running ?? Promise.resolve();
|
|
22
|
+
const timer = limitMs === undefined
|
|
23
|
+
? undefined
|
|
24
|
+
: setTimeout(() => {
|
|
25
|
+
release();
|
|
26
|
+
reject(new Error(`timed out after ${limitMs / 1000} seconds`));
|
|
27
|
+
}, limitMs);
|
|
28
|
+
const use = async (value) => {
|
|
29
|
+
if (used)
|
|
30
|
+
throw new Error("use() was called twice");
|
|
31
|
+
used = true;
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolve({ value, release, done });
|
|
34
|
+
await released;
|
|
35
|
+
};
|
|
36
|
+
running = (async () => def.setup?.(args, use))();
|
|
37
|
+
running.then(() => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
if (!used)
|
|
40
|
+
reject(new Error("the setup never called use()"));
|
|
41
|
+
}, (err) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
if (!used)
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
// A failure after use() is read by teardown; this keeps it from being reported as unhandled meanwhile
|
|
47
|
+
running.catch(() => { });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Lets a set-up fixture finish its cleanup, within the fixture's own `timeout` else `limitMs`. Returns the error
|
|
52
|
+
* (`fixture "x" teardown failed: ...`) when the cleanup threw or ran out of time.
|
|
53
|
+
*/
|
|
54
|
+
export async function finishWithin(one, limitMs) {
|
|
55
|
+
one.release();
|
|
56
|
+
const limit = one.timeout ?? limitMs;
|
|
57
|
+
let timer;
|
|
58
|
+
try {
|
|
59
|
+
await Promise.race([
|
|
60
|
+
one.done(),
|
|
61
|
+
new Promise((_, reject) => {
|
|
62
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${limit / 1000} seconds`)), limit);
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const failure = new Error(`fixture "${one.name}" teardown failed: ${messageOf(err)}`);
|
|
69
|
+
if (err instanceof Error && err.stack)
|
|
70
|
+
failure.stack = err.stack;
|
|
71
|
+
return failure;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SamuraiTestConfig } from "../types/config.js";
|
|
2
|
+
import type { RegisteredTestCase } from "../types/test.js";
|
|
3
|
+
import type { FixtureLayers } from "./fixture-defs.js";
|
|
4
|
+
import type { FixtureCatalogEntry } from "./fixture-registry.js";
|
|
5
|
+
/** What the fixture keys of the `use` layers come to for one test */
|
|
6
|
+
export interface ResolvedFixtureUse {
|
|
7
|
+
/** The fixtures to set up before `beforeEach`, besides the `auto` ones; one the environment turns off is still listed when a later layer insists, and skips the test */
|
|
8
|
+
attach: string[];
|
|
9
|
+
/** Option fixture name to its values from `use`, over the fixture's own default */
|
|
10
|
+
options: Record<string, Record<string, unknown>>;
|
|
11
|
+
/** Fixtures the run's environment turns off, with the reason a test that needs one is skipped for */
|
|
12
|
+
disabled: Map<string, string>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The fixtures a test gets from `use`: config, then the run's environment, then the group, then `test.use()`, a
|
|
16
|
+
* later layer winning key by key. The environment's `false` is the exception: it wins over a `true` anywhere, so a
|
|
17
|
+
* fixture an environment turns off cannot be switched back on for it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function effectiveFixtureUse(test: RegisteredTestCase): Promise<ResolvedFixtureUse>;
|
|
20
|
+
/** Refuses a config whose `use` (config, environments, groups) names a fixture that is not in `config.fixtures` */
|
|
21
|
+
export declare function checkFixtureUseNames(config: SamuraiTestConfig, catalog: readonly FixtureCatalogEntry[]): void;
|
|
22
|
+
/** Refuses a `test.use()` that names a fixture the test's own `test.extend()` does not define */
|
|
23
|
+
export declare function checkTestFixtureUse(use: object, layers: FixtureLayers | undefined): void;
|