@itmetsam/samurai-framework 0.21.4 → 0.22.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/dist/api.d.ts +8 -1
- 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 +11 -2
- package/dist/browser/browser.js +31 -8
- package/dist/browser/cdp.d.ts +11 -0
- package/dist/browser/cdp.js +28 -0
- package/dist/browser/chrome-launch.d.ts +4 -0
- package/dist/browser/chrome-launch.js +11 -3
- 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 +8 -1
- package/dist/browser/index.js +5 -1
- package/dist/browser/keyboard.d.ts +35 -0
- package/dist/browser/keyboard.js +83 -0
- package/dist/browser/launch-firefox.js +1 -1
- package/dist/browser/launch-flags.d.ts +4 -1
- package/dist/browser/launch-flags.js +18 -5
- package/dist/browser/mouse.d.ts +42 -0
- package/dist/browser/mouse.js +66 -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.d.ts +20 -6
- package/dist/browser/page.js +24 -8
- package/dist/browser/session-capabilities.d.ts +10 -0
- package/dist/browser/session-capabilities.js +11 -0
- package/dist/browser/session.d.ts +2 -1
- package/dist/browser/session.js +3 -3
- package/dist/browser/storage-state.d.ts +4 -0
- package/dist/browser/storage-state.js +17 -0
- package/dist/browser/use-applier.d.ts +38 -0
- package/dist/browser/use-applier.js +121 -0
- package/dist/browser/use-commands.d.ts +36 -0
- package/dist/browser/use-commands.js +120 -0
- package/dist/browser/viewport.d.ts +3 -2
- package/dist/browser/viewport.js +15 -2
- 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 +8 -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 +8 -0
- package/dist/config/group-use.js +41 -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 +18 -0
- package/dist/config/use-options.js +184 -0
- 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/logger/warn-once.d.ts +4 -0
- package/dist/logger/warn-once.js +13 -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/attachments.d.ts +16 -0
- package/dist/runner/attachments.js +60 -0
- package/dist/runner/fixture-args.d.ts +15 -0
- package/dist/runner/fixture-args.js +95 -0
- package/dist/runner/fixture-defs.d.ts +22 -0
- package/dist/runner/fixture-defs.js +104 -0
- package/dist/runner/fixture-scope.d.ts +33 -0
- package/dist/runner/fixture-scope.js +156 -0
- package/dist/runner/fixtures.d.ts +37 -0
- package/dist/runner/fixtures.js +40 -0
- package/dist/runner/global-hooks.d.ts +13 -0
- package/dist/runner/global-hooks.js +66 -0
- package/dist/runner/hooks.d.ts +6 -1
- package/dist/runner/hooks.js +15 -3
- 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/registry.d.ts +3 -39
- package/dist/runner/registry.js +99 -21
- package/dist/runner/reporter.d.ts +17 -1
- package/dist/runner/reporter.js +15 -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 +1 -3
- package/dist/runner/run-test.js +98 -35
- package/dist/runner/run.d.ts +1 -0
- package/dist/runner/run.js +1 -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/steps.d.ts +13 -0
- package/dist/runner/steps.js +67 -0
- package/dist/runner/suite.d.ts +8 -3
- package/dist/runner/suite.js +12 -3
- package/dist/runner/test-annotations.d.ts +16 -0
- package/dist/runner/test-annotations.js +47 -0
- package/dist/runner/test-function.d.ts +115 -0
- package/dist/runner/test-function.js +1 -0
- package/dist/runner/test-info.d.ts +104 -0
- package/dist/runner/test-info.js +170 -0
- package/dist/runner/test-runner.d.ts +1 -0
- package/dist/runner/test-runner.js +64 -19
- package/dist/runner/test-timeout.d.ts +22 -0
- package/dist/runner/test-timeout.js +45 -0
- package/dist/runner/use-options.d.ts +7 -0
- package/dist/runner/use-options.js +25 -0
- 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 +5 -2
- package/dist/runner/worker.d.ts +2 -0
- package/dist/runner/worker.js +7 -1
- package/dist/script/element-handle.d.ts +1 -1
- package/dist/script/element-handle.js +1 -1
- package/dist/types/config.d.ts +86 -2
- package/dist/types/extend.d.ts +30 -0
- package/dist/types/test.d.ts +138 -0
- package/dist/types/use.d.ts +85 -0
- package/package.json +1 -1
- package/dist/browser/emulation.d.ts +0 -61
- package/dist/browser/emulation.js +0 -180
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { requestedBy } from "./fixture-args.js";
|
|
2
|
+
/** The fixtures the runner builds for every test: a custom fixture of the same name overrides it */
|
|
3
|
+
export const BUILT_IN_FIXTURES = new Set([
|
|
4
|
+
"page",
|
|
5
|
+
"browser",
|
|
6
|
+
"browserName",
|
|
7
|
+
"workerIndex",
|
|
8
|
+
"env",
|
|
9
|
+
"secrets",
|
|
10
|
+
"inputs",
|
|
11
|
+
"outputs",
|
|
12
|
+
"context",
|
|
13
|
+
"baseURL",
|
|
14
|
+
"request",
|
|
15
|
+
"testInfo",
|
|
16
|
+
"isMobile",
|
|
17
|
+
"hasTouch",
|
|
18
|
+
]);
|
|
19
|
+
const OPTION_KEYS = new Set(["auto", "scope"]);
|
|
20
|
+
function isPlainObject(value) {
|
|
21
|
+
if (typeof value !== "object" || value === null)
|
|
22
|
+
return false;
|
|
23
|
+
const proto = Object.getPrototypeOf(value);
|
|
24
|
+
return proto === Object.prototype || proto === null;
|
|
25
|
+
}
|
|
26
|
+
/** `[fixture, options]` is always a pair with options; an array value goes in a function */
|
|
27
|
+
function checkedOptions(name, options) {
|
|
28
|
+
for (const key of Object.keys(options))
|
|
29
|
+
if (!OPTION_KEYS.has(key))
|
|
30
|
+
throw new Error(`extend(): fixture "${name}" has an unknown option "${key}"`);
|
|
31
|
+
if (options.auto !== undefined && typeof options.auto !== "boolean")
|
|
32
|
+
throw new Error(`extend(): fixture "${name}": auto must be true or false`);
|
|
33
|
+
if (options.scope === "worker")
|
|
34
|
+
throw new Error(`extend(): fixture "${name}" has scope "worker", which is not supported; fixtures live as long as one test ({ scope: "test" })`);
|
|
35
|
+
if (options.scope !== undefined && options.scope !== "test")
|
|
36
|
+
throw new Error(`extend(): fixture "${name}": scope must be "test"`);
|
|
37
|
+
return { auto: options.auto === true };
|
|
38
|
+
}
|
|
39
|
+
function definitionOf(name, raw) {
|
|
40
|
+
const [fixture, options] = Array.isArray(raw) && raw.length === 2 && isPlainObject(raw[1])
|
|
41
|
+
? [raw[0], checkedOptions(name, raw[1])]
|
|
42
|
+
: [raw, { auto: false }];
|
|
43
|
+
if (typeof fixture !== "function")
|
|
44
|
+
return { name, auto: options.auto, deps: [], value: fixture };
|
|
45
|
+
const requested = requestedBy(fixture);
|
|
46
|
+
if (requested.kind === "opaque" || requested.rest)
|
|
47
|
+
throw new Error(`extend(): fixture "${name}" must destructure the fixtures it uses in its first argument, as ({ page }, use) or ({}, use)`);
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
auto: options.auto,
|
|
51
|
+
deps: requested.names,
|
|
52
|
+
setup: fixture,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Where a dependency of `name` (at definition `level`) comes from: another definition, or the built-in fixture */
|
|
56
|
+
export function dependencyOf(layers, name, level, dep) {
|
|
57
|
+
if (dep === name)
|
|
58
|
+
return level > 0
|
|
59
|
+
? { name, level: level - 1 }
|
|
60
|
+
: BUILT_IN_FIXTURES.has(dep)
|
|
61
|
+
? "built-in"
|
|
62
|
+
: undefined;
|
|
63
|
+
const defs = layers.get(dep);
|
|
64
|
+
if (defs)
|
|
65
|
+
return { name: dep, level: defs.length - 1 };
|
|
66
|
+
return BUILT_IN_FIXTURES.has(dep) ? "built-in" : undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Throws for a dependency nobody defines and for a cycle (`a` needs `b` needs `a`) */
|
|
69
|
+
function validate(layers) {
|
|
70
|
+
const done = new Set();
|
|
71
|
+
const walk = (name, level, path) => {
|
|
72
|
+
const key = `${name}#${level}`;
|
|
73
|
+
if (done.has(key))
|
|
74
|
+
return;
|
|
75
|
+
if (path.includes(key)) {
|
|
76
|
+
const loop = [...path.slice(path.indexOf(key)), key];
|
|
77
|
+
throw new Error(`extend(): fixtures depend on each other in a circle: ${loop.map((k) => k.split("#")[0]).join(" -> ")}`);
|
|
78
|
+
}
|
|
79
|
+
const def = layers.get(name)?.[level];
|
|
80
|
+
if (!def)
|
|
81
|
+
return;
|
|
82
|
+
for (const dep of def.deps) {
|
|
83
|
+
const target = dependencyOf(layers, name, level, dep);
|
|
84
|
+
if (target === undefined)
|
|
85
|
+
throw new Error(`extend(): fixture "${name}" depends on unknown fixture "${dep}"`);
|
|
86
|
+
if (target !== "built-in")
|
|
87
|
+
walk(target.name, target.level, [...path, key]);
|
|
88
|
+
}
|
|
89
|
+
done.add(key);
|
|
90
|
+
};
|
|
91
|
+
for (const [name, defs] of layers)
|
|
92
|
+
for (let level = 0; level < defs.length; level++)
|
|
93
|
+
walk(name, level, []);
|
|
94
|
+
}
|
|
95
|
+
/** The fixtures after `test.extend(definitions)` on a test that has `base`; throws `extend(): ...` for a definition that cannot work */
|
|
96
|
+
export function extendLayers(base, definitions) {
|
|
97
|
+
if (!isPlainObject(definitions))
|
|
98
|
+
throw new Error("extend(): needs an object of fixtures");
|
|
99
|
+
const layers = new Map(base);
|
|
100
|
+
for (const [name, raw] of Object.entries(definitions))
|
|
101
|
+
layers.set(name, [...(layers.get(name) ?? []), definitionOf(name, raw)]);
|
|
102
|
+
validate(layers);
|
|
103
|
+
return layers;
|
|
104
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TestFixtures } from "../types/test.js";
|
|
2
|
+
import { type FixtureLayers } from "./fixture-defs.js";
|
|
3
|
+
/** A custom fixture whose setup threw or never called `use` */
|
|
4
|
+
export declare class FixtureSetupError extends Error {
|
|
5
|
+
constructor(name: string, cause: unknown);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The custom fixtures of one test run. Fixtures are set up when a test or hook asks for them (by destructuring
|
|
9
|
+
* 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.
|
|
11
|
+
*/
|
|
12
|
+
export declare class FixtureScope {
|
|
13
|
+
private readonly layers;
|
|
14
|
+
private readonly base;
|
|
15
|
+
/** The built-in fixtures, with each custom fixture added (replacing a built-in of that name) once it is set up */
|
|
16
|
+
readonly fixtures: TestFixtures;
|
|
17
|
+
private readonly values;
|
|
18
|
+
private readonly pending;
|
|
19
|
+
private readonly started;
|
|
20
|
+
private closed;
|
|
21
|
+
constructor(layers: FixtureLayers, base: TestFixtures);
|
|
22
|
+
/** Sets up the fixtures marked `auto` */
|
|
23
|
+
prepareAuto(): Promise<void>;
|
|
24
|
+
/** Sets up the custom fixtures `fn` destructures from its first parameter */
|
|
25
|
+
prepare(fn: (...args: never[]) => unknown): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Finishes the fixtures that were set up, last first, each within `limitMs`. Returns the first error (a
|
|
28
|
+
* fixture's cleanup threw or ran out of time); the others still run.
|
|
29
|
+
*/
|
|
30
|
+
teardown(limitMs: number): Promise<Error | undefined>;
|
|
31
|
+
private ensure;
|
|
32
|
+
private setUp;
|
|
33
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { requestedBy } from "./fixture-args.js";
|
|
2
|
+
import { BUILT_IN_FIXTURES, dependencyOf, } from "./fixture-defs.js";
|
|
3
|
+
/** A custom fixture whose setup threw or never called `use` */
|
|
4
|
+
export class FixtureSetupError extends Error {
|
|
5
|
+
constructor(name, cause) {
|
|
6
|
+
super(`fixture "${name}" setup failed: ${messageOf(cause)}`);
|
|
7
|
+
this.name = "FixtureSetupError";
|
|
8
|
+
if (cause instanceof Error && cause.stack)
|
|
9
|
+
this.stack = cause.stack;
|
|
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
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The custom fixtures of one test run. Fixtures are set up when a test or hook asks for them (by destructuring
|
|
42
|
+
* 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.
|
|
44
|
+
*/
|
|
45
|
+
export class FixtureScope {
|
|
46
|
+
constructor(layers, base) {
|
|
47
|
+
this.layers = layers;
|
|
48
|
+
this.base = base;
|
|
49
|
+
this.values = new Map();
|
|
50
|
+
this.pending = new Map();
|
|
51
|
+
this.started = [];
|
|
52
|
+
this.closed = false;
|
|
53
|
+
const own = Object.getOwnPropertyDescriptors(base);
|
|
54
|
+
for (const descriptor of Object.values(own))
|
|
55
|
+
descriptor.configurable = true;
|
|
56
|
+
this.fixtures = Object.defineProperties({}, own);
|
|
57
|
+
}
|
|
58
|
+
/** Sets up the fixtures marked `auto` */
|
|
59
|
+
async prepareAuto() {
|
|
60
|
+
for (const [name, defs] of this.layers)
|
|
61
|
+
if (defs[defs.length - 1]?.auto)
|
|
62
|
+
await this.ensure(name, defs.length - 1);
|
|
63
|
+
}
|
|
64
|
+
/** Sets up the custom fixtures `fn` destructures from its first parameter */
|
|
65
|
+
async prepare(fn) {
|
|
66
|
+
const requested = requestedBy(fn);
|
|
67
|
+
if (requested.kind === "opaque")
|
|
68
|
+
return;
|
|
69
|
+
const names = requested.rest ? [...this.layers.keys()] : requested.names;
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
const defs = this.layers.get(name);
|
|
72
|
+
if (defs)
|
|
73
|
+
await this.ensure(name, defs.length - 1);
|
|
74
|
+
else if (!BUILT_IN_FIXTURES.has(name))
|
|
75
|
+
throw new Error(`extend(): unknown fixture "${name}"`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Finishes the fixtures that were set up, last first, each within `limitMs`. Returns the first error (a
|
|
80
|
+
* fixture's cleanup threw or ran out of time); the others still run.
|
|
81
|
+
*/
|
|
82
|
+
async teardown(limitMs) {
|
|
83
|
+
this.closed = true;
|
|
84
|
+
let first;
|
|
85
|
+
for (let one = this.started.pop(); one; one = this.started.pop()) {
|
|
86
|
+
one.release();
|
|
87
|
+
let timer;
|
|
88
|
+
try {
|
|
89
|
+
await Promise.race([
|
|
90
|
+
one.done(),
|
|
91
|
+
new Promise((_, reject) => {
|
|
92
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${limitMs / 1000} seconds`)), limitMs);
|
|
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
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return first;
|
|
106
|
+
}
|
|
107
|
+
ensure(name, level) {
|
|
108
|
+
const key = `${name}#${level}`;
|
|
109
|
+
let promise = this.pending.get(key);
|
|
110
|
+
if (!promise) {
|
|
111
|
+
promise = this.setUp(name, level);
|
|
112
|
+
this.pending.set(key, promise);
|
|
113
|
+
}
|
|
114
|
+
return promise;
|
|
115
|
+
}
|
|
116
|
+
async setUp(name, level) {
|
|
117
|
+
const def = this.layers.get(name)?.[level];
|
|
118
|
+
if (!def)
|
|
119
|
+
throw new Error(`extend(): unknown fixture "${name}"`);
|
|
120
|
+
try {
|
|
121
|
+
const args = {};
|
|
122
|
+
for (const dep of def.deps) {
|
|
123
|
+
const target = dependencyOf(this.layers, name, level, dep);
|
|
124
|
+
if (target === "built-in" || target === undefined)
|
|
125
|
+
args[dep] = this.base[dep];
|
|
126
|
+
else {
|
|
127
|
+
await this.ensure(target.name, target.level);
|
|
128
|
+
args[dep] = this.values.get(`${target.name}#${target.level}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let value = def.value;
|
|
132
|
+
if (def.setup) {
|
|
133
|
+
const started = await startSetup(def, args);
|
|
134
|
+
if (this.closed) {
|
|
135
|
+
started.release();
|
|
136
|
+
throw new Error("the test is already over");
|
|
137
|
+
}
|
|
138
|
+
value = started.value;
|
|
139
|
+
this.started.push({ name, ...started });
|
|
140
|
+
}
|
|
141
|
+
this.values.set(`${name}#${level}`, value);
|
|
142
|
+
if (level === (this.layers.get(name)?.length ?? 0) - 1)
|
|
143
|
+
Object.defineProperty(this.fixtures, name, {
|
|
144
|
+
value,
|
|
145
|
+
enumerable: true,
|
|
146
|
+
configurable: true,
|
|
147
|
+
writable: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
throw err instanceof FixtureSetupError
|
|
152
|
+
? err
|
|
153
|
+
: new FixtureSetupError(name, err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Browser } from "../browser/browser.js";
|
|
2
|
+
import type Page from "../browser/page.js";
|
|
3
|
+
import type { SupportedBrowser } from "../types/browser.js";
|
|
4
|
+
import type { AllHookFixtures, RegisteredTestCase, TestFixtures, TestInfo } from "../types/test.js";
|
|
5
|
+
import type { RequestFixture } from "./request-fixture.js";
|
|
6
|
+
import type { RunnerOptions } from "./options.js";
|
|
7
|
+
import type { PreparedRun } from "./prepare-run.js";
|
|
8
|
+
import type { OutputSink } from "../wait/output-sink.js";
|
|
9
|
+
/** What `beforeAll` and `afterAll` hooks get: no page, only what a run knows */
|
|
10
|
+
export declare function allHookFixtures(run: PreparedRun, browserName: SupportedBrowser, workerIndex?: number): AllHookFixtures;
|
|
11
|
+
/** What one run of a test builds its fixtures from */
|
|
12
|
+
export interface FixtureSources {
|
|
13
|
+
launched: {
|
|
14
|
+
page: Page;
|
|
15
|
+
browser: Browser;
|
|
16
|
+
};
|
|
17
|
+
browserName: SupportedBrowser;
|
|
18
|
+
workerIndex: number;
|
|
19
|
+
run: PreparedRun;
|
|
20
|
+
test: RegisteredTestCase;
|
|
21
|
+
options: RunnerOptions;
|
|
22
|
+
sink: OutputSink;
|
|
23
|
+
info: TestInfo;
|
|
24
|
+
/** The test's `request` fixture, made on first use */
|
|
25
|
+
request: RequestFixture;
|
|
26
|
+
/** What the `use` options came to in this browser */
|
|
27
|
+
device: {
|
|
28
|
+
isMobile: boolean;
|
|
29
|
+
hasTouch: boolean;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The fixtures a test body and its each-hooks receive. `context`, `baseURL`, `request` and `testInfo` are getters: nothing
|
|
34
|
+
* is looked up until a test reads one (and `page.context()` throws for a page without one, which only a test
|
|
35
|
+
* that asks for it should see).
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildTestFixtures(from: FixtureSources): TestFixtures;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createSecretsFixture } from "../config/secrets.js";
|
|
2
|
+
import { createEnvFixture } from "../config/variables.js";
|
|
3
|
+
import { createInputsFixture, createOutputsFixture } from "./test-io.js";
|
|
4
|
+
/** What `beforeAll` and `afterAll` hooks get: no page, only what a run knows */
|
|
5
|
+
export function allHookFixtures(run, browserName, workerIndex = 0) {
|
|
6
|
+
const { settings, secrets } = run;
|
|
7
|
+
return {
|
|
8
|
+
browserName,
|
|
9
|
+
workerIndex,
|
|
10
|
+
env: createEnvFixture(settings.environment, settings.variables),
|
|
11
|
+
secrets: createSecretsFixture(settings.environment, secrets),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The fixtures a test body and its each-hooks receive. `context`, `baseURL`, `request` and `testInfo` are getters: nothing
|
|
16
|
+
* is looked up until a test reads one (and `page.context()` throws for a page without one, which only a test
|
|
17
|
+
* that asks for it should see).
|
|
18
|
+
*/
|
|
19
|
+
export function buildTestFixtures(from) {
|
|
20
|
+
const { launched, browserName, workerIndex, run, test, options, sink, info, device, request, } = from;
|
|
21
|
+
const { settings, secrets } = run;
|
|
22
|
+
const fixtures = {
|
|
23
|
+
page: launched.page,
|
|
24
|
+
browser: launched.browser,
|
|
25
|
+
browserName,
|
|
26
|
+
workerIndex,
|
|
27
|
+
env: createEnvFixture(settings.environment, settings.variables),
|
|
28
|
+
secrets: createSecretsFixture(settings.environment, secrets),
|
|
29
|
+
inputs: createInputsFixture(test.inputs, options.inputs),
|
|
30
|
+
outputs: createOutputsFixture(sink),
|
|
31
|
+
isMobile: device.isMobile,
|
|
32
|
+
hasTouch: device.hasTouch,
|
|
33
|
+
};
|
|
34
|
+
return Object.defineProperties(fixtures, {
|
|
35
|
+
context: { enumerable: true, get: () => launched.page.context() },
|
|
36
|
+
baseURL: { enumerable: true, get: () => settings.baseURL },
|
|
37
|
+
request: { enumerable: true, get: () => request.get() },
|
|
38
|
+
testInfo: { enumerable: true, get: () => info },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GlobalHookConfig, SamuraiTestConfig } from "../types/config.js";
|
|
2
|
+
/** The run-level hooks of a config: `setup` before the tests, `teardown` after them */
|
|
3
|
+
export interface GlobalHooks {
|
|
4
|
+
/** Runs `globalSetup`. A throw is `globalSetup(): message` */
|
|
5
|
+
setup(): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Runs what the setup returned, then `globalTeardown`; both even when the setup failed half way. Resolves with the
|
|
8
|
+
* first error as `globalTeardown(): message`, else undefined
|
|
9
|
+
*/
|
|
10
|
+
teardown(): Promise<Error | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/** Binds the config's `globalSetup` / `globalTeardown` to the config they receive and the project folder they load from */
|
|
13
|
+
export declare function globalHooks(config: SamuraiTestConfig, hookConfig: GlobalHookConfig, projectDir: string): GlobalHooks;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
4
|
+
/** The function a hook names: given directly, or the default export of the module at `projectDir`/`hook` */
|
|
5
|
+
async function loadHook(name, hook, projectDir) {
|
|
6
|
+
if (typeof hook === "function")
|
|
7
|
+
return hook;
|
|
8
|
+
const file = path.resolve(projectDir, hook);
|
|
9
|
+
let module;
|
|
10
|
+
try {
|
|
11
|
+
module = (await import(pathToFileURL(file).href));
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
throw new Error(`${name}(): cannot load ${hook}: ${messageOf(error)}`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof module.default !== "function")
|
|
17
|
+
throw new Error(`${name}(): ${hook} must export a function as default`);
|
|
18
|
+
return module.default;
|
|
19
|
+
}
|
|
20
|
+
/** Binds the config's `globalSetup` / `globalTeardown` to the config they receive and the project folder they load from */
|
|
21
|
+
export function globalHooks(config, hookConfig, projectDir) {
|
|
22
|
+
const returned = [];
|
|
23
|
+
return {
|
|
24
|
+
async setup() {
|
|
25
|
+
if (config.globalSetup === undefined)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
const setup = (await loadHook("globalSetup", config.globalSetup, projectDir));
|
|
29
|
+
const teardown = await setup(hookConfig);
|
|
30
|
+
if (typeof teardown === "function")
|
|
31
|
+
returned.push(teardown);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = messageOf(error);
|
|
35
|
+
throw new Error(message.startsWith("globalSetup(): ")
|
|
36
|
+
? message
|
|
37
|
+
: `globalSetup(): ${message}`);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async teardown() {
|
|
41
|
+
let first;
|
|
42
|
+
const record = (error) => {
|
|
43
|
+
const message = messageOf(error);
|
|
44
|
+
first ??= new Error(message.startsWith("globalTeardown(): ")
|
|
45
|
+
? message
|
|
46
|
+
: `globalTeardown(): ${message}`);
|
|
47
|
+
};
|
|
48
|
+
for (const teardown of returned.splice(0).reverse())
|
|
49
|
+
try {
|
|
50
|
+
await teardown();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
record(error);
|
|
54
|
+
}
|
|
55
|
+
if (config.globalTeardown !== undefined)
|
|
56
|
+
try {
|
|
57
|
+
const teardown = (await loadHook("globalTeardown", config.globalTeardown, projectDir));
|
|
58
|
+
await teardown(hookConfig);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
record(error);
|
|
62
|
+
}
|
|
63
|
+
return first;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/runner/hooks.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Suite, TestFixtures } from "../types/test.js";
|
|
2
|
+
import type { FixtureScope } from "./fixture-scope.js";
|
|
2
3
|
import { type HookKind } from "./suite.js";
|
|
3
4
|
/** A hook threw or ran out of time: the message names the hook (`beforeEach hook failed: <message>`) */
|
|
4
5
|
export declare class HookError extends Error {
|
|
@@ -21,4 +22,8 @@ export declare function runHook<F>(kind: HookKind, hook: (fixtures: F) => Promis
|
|
|
21
22
|
* - What it throws: the `beforeEach` failure, else the body's error, else the first `afterEach` failure. A skip is
|
|
22
23
|
* overruled by a failing `afterEach`.
|
|
23
24
|
*/
|
|
24
|
-
export declare function runWithEachHooks(suite: Suite, fixtures: TestFixtures, signal: AbortSignal, body: () => Promise<void
|
|
25
|
+
export declare function runWithEachHooks(suite: Suite, fixtures: TestFixtures, signal: AbortSignal, body: () => Promise<void>,
|
|
26
|
+
/** Called once the body (or a `beforeEach` hook) is over, before the `afterEach` hooks, with what it threw */
|
|
27
|
+
onBodyDone?: (error: unknown) => void,
|
|
28
|
+
/** The custom fixtures: the `auto` ones are set up first, and a hook gets the ones it destructures */
|
|
29
|
+
custom?: FixtureScope): Promise<void>;
|
package/dist/runner/hooks.js
CHANGED
|
@@ -46,24 +46,36 @@ export async function runHook(kind, hook, fixtures, limitMs) {
|
|
|
46
46
|
* - What it throws: the `beforeEach` failure, else the body's error, else the first `afterEach` failure. A skip is
|
|
47
47
|
* overruled by a failing `afterEach`.
|
|
48
48
|
*/
|
|
49
|
-
export async function runWithEachHooks(suite, fixtures, signal, body
|
|
49
|
+
export async function runWithEachHooks(suite, fixtures, signal, body,
|
|
50
|
+
/** Called once the body (or a `beforeEach` hook) is over, before the `afterEach` hooks, with what it threw */
|
|
51
|
+
onBodyDone,
|
|
52
|
+
/** The custom fixtures: the `auto` ones are set up first, and a hook gets the ones it destructures */
|
|
53
|
+
custom) {
|
|
54
|
+
const withFixtures = (hook) => custom
|
|
55
|
+
? async (given) => {
|
|
56
|
+
await custom.prepare(hook);
|
|
57
|
+
return hook(given);
|
|
58
|
+
}
|
|
59
|
+
: hook;
|
|
50
60
|
let primary;
|
|
51
61
|
let failed = false;
|
|
52
62
|
try {
|
|
63
|
+
await custom?.prepareAuto();
|
|
53
64
|
for (const hook of beforeEachOf(suite))
|
|
54
|
-
await runHook("beforeEach", hook, fixtures);
|
|
65
|
+
await runHook("beforeEach", withFixtures(hook), fixtures);
|
|
55
66
|
await body();
|
|
56
67
|
}
|
|
57
68
|
catch (error) {
|
|
58
69
|
primary = error;
|
|
59
70
|
failed = true;
|
|
60
71
|
}
|
|
72
|
+
onBodyDone?.(failed ? primary : undefined);
|
|
61
73
|
let afterError;
|
|
62
74
|
let afterFailed = false;
|
|
63
75
|
if (!signal.aborted) {
|
|
64
76
|
for (const hook of afterEachOf(suite)) {
|
|
65
77
|
try {
|
|
66
|
-
await runHook("afterEach", hook, fixtures);
|
|
78
|
+
await runHook("afterEach", withFixtures(hook), fixtures);
|
|
67
79
|
}
|
|
68
80
|
catch (error) {
|
|
69
81
|
if (!afterFailed)
|
package/dist/runner/options.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SupportedBrowser } from "../types/browser.js";
|
|
2
2
|
import type { RunEvent } from "./reporter.js";
|
|
3
|
+
import type { Shard } from "./shard.js";
|
|
3
4
|
/** What decides which tests of the found files run, and how the browser starts */
|
|
4
5
|
export interface RunnerOptions {
|
|
5
6
|
/** Spec files to run, absolute or relative to the project folder. Default: every `*.spec.ts` under `srcDir` */
|
|
@@ -8,8 +9,12 @@ export interface RunnerOptions {
|
|
|
8
9
|
grep?: string | RegExp;
|
|
9
10
|
/** Only tests with exactly one of these full names (`describe` titles joined with " > ") */
|
|
10
11
|
testNames?: string[];
|
|
11
|
-
/**
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Only the tests of this group of the config's `groups`, or of any of these (unknown name: an error). Combines with
|
|
14
|
+
* the other filters. The run applies the group's `use` and `browsers`; a test in two selected groups runs once per
|
|
15
|
+
* group, and its results name the group.
|
|
16
|
+
*/
|
|
17
|
+
group?: string | string[];
|
|
13
18
|
/** The browsers to run in, instead of the group's or the project's `browsers` (the command line's `--browser`) */
|
|
14
19
|
browsers?: SupportedBrowser[];
|
|
15
20
|
/**
|
|
@@ -24,6 +29,12 @@ export interface RunnerOptions {
|
|
|
24
29
|
* @default the config's `retries`, else 0
|
|
25
30
|
*/
|
|
26
31
|
retries?: number | string;
|
|
32
|
+
/**
|
|
33
|
+
* Runs only one slice of the selected tests, to split a run over several machines (the command line's
|
|
34
|
+
* `--shard=current/total`). Whole files, and serial blocks, go to one shard; the shards are disjoint and together
|
|
35
|
+
* hold every test. The report names the shard. A shard without tests succeeds.
|
|
36
|
+
*/
|
|
37
|
+
shard?: Shard;
|
|
27
38
|
/**
|
|
28
39
|
* Values for the inputs the tests declare, by name: every test of the run reads the names it declares from
|
|
29
40
|
* here, and its own default for a name missing here
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The last few lines a child process printed, kept for the error message when it fails */
|
|
2
|
+
export declare class OutputTail {
|
|
3
|
+
private readonly max;
|
|
4
|
+
/** Values to hide besides the registered secrets (the child's own `env` values) */
|
|
5
|
+
private readonly hidden;
|
|
6
|
+
private lines;
|
|
7
|
+
private partial;
|
|
8
|
+
constructor(max?: number,
|
|
9
|
+
/** Values to hide besides the registered secrets (the child's own `env` values) */
|
|
10
|
+
hidden?: readonly string[]);
|
|
11
|
+
add(chunk: Buffer | string): void;
|
|
12
|
+
/** The kept lines, masked, one per line; empty when the child printed nothing */
|
|
13
|
+
text(): string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MASK, maskText } from "../config/mask.js";
|
|
2
|
+
/** The last few lines a child process printed, kept for the error message when it fails */
|
|
3
|
+
export class OutputTail {
|
|
4
|
+
constructor(max = 20,
|
|
5
|
+
/** Values to hide besides the registered secrets (the child's own `env` values) */
|
|
6
|
+
hidden = []) {
|
|
7
|
+
this.max = max;
|
|
8
|
+
this.hidden = hidden;
|
|
9
|
+
this.lines = [];
|
|
10
|
+
this.partial = "";
|
|
11
|
+
}
|
|
12
|
+
add(chunk) {
|
|
13
|
+
const text = this.partial + chunk.toString();
|
|
14
|
+
const parts = text.split(/\r?\n/);
|
|
15
|
+
this.partial = parts.pop() ?? "";
|
|
16
|
+
this.lines.push(...parts);
|
|
17
|
+
if (this.lines.length > this.max)
|
|
18
|
+
this.lines.splice(0, this.lines.length - this.max);
|
|
19
|
+
}
|
|
20
|
+
/** The kept lines, masked, one per line; empty when the child printed nothing */
|
|
21
|
+
text() {
|
|
22
|
+
let text = [...this.lines, this.partial]
|
|
23
|
+
.filter((line) => line !== "")
|
|
24
|
+
.join("\n");
|
|
25
|
+
for (const value of this.hidden)
|
|
26
|
+
if (value.length >= 4)
|
|
27
|
+
text = text.split(value).join(MASK);
|
|
28
|
+
return maskText(text);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { WorkItem } from "./worker-protocol.js";
|
|
2
|
+
/** What scheduling needs to know of a queued item and of a worker */
|
|
3
|
+
interface Queued {
|
|
4
|
+
item: WorkItem;
|
|
5
|
+
}
|
|
6
|
+
export interface Worker {
|
|
7
|
+
/** The unit this worker is running; it keeps the unit's items to itself until none is left */
|
|
8
|
+
unit?: string | undefined;
|
|
9
|
+
/** The serial key of the unit, held while the worker keeps the unit */
|
|
10
|
+
unitKey?: string | undefined;
|
|
11
|
+
busy?: {
|
|
12
|
+
entry: Queued;
|
|
13
|
+
} | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The queue position of the next item for `worker`: one of the unit it is running, else the first whose serial
|
|
17
|
+
* key is not running and whose unit is not held by a worker. -1 when there is none.
|
|
18
|
+
*/
|
|
19
|
+
export declare function nextEligible(queue: Queued[], workers: Worker[], worker: Worker): number;
|
|
20
|
+
/** How many queued items could run at the same time right now: the items of a unit count one */
|
|
21
|
+
export declare function eligibleCount(queue: Queued[], workers: Worker[]): number;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** The serial keys that are running, or kept by a worker between the items of its unit */
|
|
2
|
+
function busyKeys(workers) {
|
|
3
|
+
const keys = new Set();
|
|
4
|
+
for (const { busy, unitKey } of workers) {
|
|
5
|
+
const key = busy?.entry.item.serialKey ?? unitKey;
|
|
6
|
+
if (key)
|
|
7
|
+
keys.add(key);
|
|
8
|
+
}
|
|
9
|
+
return keys;
|
|
10
|
+
}
|
|
11
|
+
/** The units workers are running: no other worker may take their items */
|
|
12
|
+
function heldUnits(workers) {
|
|
13
|
+
const units = new Set();
|
|
14
|
+
for (const { unit } of workers)
|
|
15
|
+
if (unit)
|
|
16
|
+
units.add(unit);
|
|
17
|
+
return units;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The queue position of the next item for `worker`: one of the unit it is running, else the first whose serial
|
|
21
|
+
* key is not running and whose unit is not held by a worker. -1 when there is none.
|
|
22
|
+
*/
|
|
23
|
+
export function nextEligible(queue, workers, worker) {
|
|
24
|
+
if (worker.unit) {
|
|
25
|
+
const unit = worker.unit;
|
|
26
|
+
const at = queue.findIndex(({ item }) => item.unit === unit);
|
|
27
|
+
if (at >= 0)
|
|
28
|
+
return at;
|
|
29
|
+
worker.unit = worker.unitKey = undefined;
|
|
30
|
+
}
|
|
31
|
+
const busy = busyKeys(workers);
|
|
32
|
+
const units = heldUnits(workers);
|
|
33
|
+
return queue.findIndex(({ item }) => (!item.serialKey || !busy.has(item.serialKey)) &&
|
|
34
|
+
(!item.unit || !units.has(item.unit)));
|
|
35
|
+
}
|
|
36
|
+
/** How many queued items could run at the same time right now: the items of a unit count one */
|
|
37
|
+
export function eligibleCount(queue, workers) {
|
|
38
|
+
const taken = busyKeys(workers);
|
|
39
|
+
const units = heldUnits(workers);
|
|
40
|
+
let count = 0;
|
|
41
|
+
for (const { item } of queue) {
|
|
42
|
+
if (item.unit) {
|
|
43
|
+
if (units.has(item.unit))
|
|
44
|
+
continue;
|
|
45
|
+
units.add(item.unit);
|
|
46
|
+
}
|
|
47
|
+
if (item.serialKey) {
|
|
48
|
+
if (taken.has(item.serialKey))
|
|
49
|
+
continue;
|
|
50
|
+
taken.add(item.serialKey);
|
|
51
|
+
}
|
|
52
|
+
count++;
|
|
53
|
+
}
|
|
54
|
+
return count;
|
|
55
|
+
}
|