@itmetsam/samurai-framework 0.22.0 → 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.
Files changed (56) hide show
  1. package/dist/api.d.ts +3 -3
  2. package/dist/browser/page-content.d.ts +4 -0
  3. package/dist/browser/page-content.js +12 -0
  4. package/dist/browser/page-events.d.ts +11 -0
  5. package/dist/browser/page-events.js +23 -0
  6. package/dist/browser/page-logging.d.ts +24 -0
  7. package/dist/browser/page-logging.js +43 -0
  8. package/dist/browser/page-settings.d.ts +1 -1
  9. package/dist/browser/page-viewport.d.ts +21 -0
  10. package/dist/browser/page-viewport.js +33 -0
  11. package/dist/browser/page.d.ts +6 -9
  12. package/dist/browser/page.js +31 -63
  13. package/dist/browser/screenshot.d.ts +6 -0
  14. package/dist/browser/screenshot.js +14 -0
  15. package/dist/config/config.js +2 -1
  16. package/dist/config/fixture-use.d.ts +19 -0
  17. package/dist/config/fixture-use.js +44 -0
  18. package/dist/config/group-use.d.ts +4 -2
  19. package/dist/config/group-use.js +30 -20
  20. package/dist/config/use-options.js +5 -1
  21. package/dist/runner/fixture-defs.d.ts +2 -1
  22. package/dist/runner/fixture-defs.js +51 -18
  23. package/dist/runner/fixture-meta.d.ts +20 -0
  24. package/dist/runner/fixture-meta.js +109 -0
  25. package/dist/runner/fixture-options.d.ts +8 -0
  26. package/dist/runner/fixture-options.js +35 -0
  27. package/dist/runner/fixture-outputs.d.ts +11 -0
  28. package/dist/runner/fixture-outputs.js +28 -0
  29. package/dist/runner/fixture-registry.d.ts +35 -0
  30. package/dist/runner/fixture-registry.js +73 -0
  31. package/dist/runner/fixture-scope.d.ts +39 -9
  32. package/dist/runner/fixture-scope.js +139 -70
  33. package/dist/runner/fixture-start.d.ts +27 -0
  34. package/dist/runner/fixture-start.js +76 -0
  35. package/dist/runner/fixture-use.d.ts +23 -0
  36. package/dist/runner/fixture-use.js +64 -0
  37. package/dist/runner/fixture-worker.d.ts +26 -0
  38. package/dist/runner/fixture-worker.js +46 -0
  39. package/dist/runner/prepare-run.js +5 -0
  40. package/dist/runner/registry.js +15 -5
  41. package/dist/runner/reporter.d.ts +3 -1
  42. package/dist/runner/reporter.js +1 -0
  43. package/dist/runner/run-test.d.ts +3 -0
  44. package/dist/runner/run-test.js +16 -1
  45. package/dist/runner/run.d.ts +7 -0
  46. package/dist/runner/run.js +9 -0
  47. package/dist/runner/test-function.d.ts +2 -2
  48. package/dist/runner/test-runner.js +6 -1
  49. package/dist/runner/use-options.d.ts +16 -2
  50. package/dist/runner/use-options.js +28 -8
  51. package/dist/runner/worker-session.js +6 -0
  52. package/dist/types/config.d.ts +35 -22
  53. package/dist/types/extend.d.ts +54 -2
  54. package/dist/types/test.d.ts +24 -0
  55. package/dist/types/use.d.ts +14 -0
  56. package/package.json +1 -1
@@ -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
- /** 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
- }
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 first error (a
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
- 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
- }
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.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
- }
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;
@@ -0,0 +1,64 @@
1
+ import { FIXTURES_KEY, mergeFixtureUse, pickFixtureUse, } from "../config/fixture-use.js";
2
+ import { peekRunSettings } from "../config/run-settings.js";
3
+ import { useLayers } from "./use-options.js";
4
+ /**
5
+ * The fixtures a test gets from `use`: config, then the run's environment, then the group, then `test.use()`, a
6
+ * later layer winning key by key. The environment's `false` is the exception: it wins over a `true` anywhere, so a
7
+ * fixture an environment turns off cannot be switched back on for it.
8
+ */
9
+ export async function effectiveFixtureUse(test) {
10
+ const layers = await useLayers(test);
11
+ const merged = mergeFixtureUse([
12
+ layers.config,
13
+ layers.environment,
14
+ layers.group,
15
+ ...layers.tests,
16
+ ]);
17
+ const environment = peekRunSettings()?.environment ?? "";
18
+ const disabled = new Map();
19
+ for (const [name, on] of Object.entries(mergeFixtureUse([layers.environment]).fixtures))
20
+ if (!on)
21
+ disabled.set(name, `Fixture ${name} is turned off in ${environment}`);
22
+ return {
23
+ attach: Object.entries(merged.fixtures)
24
+ .filter(([, on]) => on)
25
+ .map(([name]) => name),
26
+ options: merged.options,
27
+ disabled,
28
+ };
29
+ }
30
+ const list = (names, none) => names.length > 0 ? names.join(", ") : none;
31
+ /** Throws when a `use` names a fixture or an option fixture that `catalog` does not have */
32
+ function checkPlace(place, use, catalog) {
33
+ const fixtures = catalog.map((entry) => entry.name);
34
+ const options = catalog.flatMap((entry) => entry.options?.name ?? []);
35
+ for (const [key, value] of Object.entries(pickFixtureUse(use))) {
36
+ if (key === FIXTURES_KEY) {
37
+ for (const name of Object.keys(value))
38
+ if (!fixtures.includes(name))
39
+ throw new Error(`${place}.fixtures: unknown fixture "${name}". ${fixtures.length > 0 ? `Known fixtures: ${list(fixtures, "")}` : "No fixtures are listed in config.fixtures"}`);
40
+ }
41
+ else if (!options.includes(key))
42
+ throw new Error(`${place}.${key}: unknown option fixture "${key}". ${options.length > 0 ? `Known option fixtures: ${list(options, "")}` : "No option fixtures are listed in config.fixtures"}`);
43
+ }
44
+ }
45
+ /** Refuses a config whose `use` (config, environments, groups) names a fixture that is not in `config.fixtures` */
46
+ export function checkFixtureUseNames(config, catalog) {
47
+ checkPlace("config.use", config.use, catalog);
48
+ for (const [name, environment] of Object.entries(config.environments ?? {}))
49
+ checkPlace(`config.environments.${name}.use`, environment.use, catalog);
50
+ config.groups?.forEach((group, index) => checkPlace(`config.groups[${index}].use`, group.use, catalog));
51
+ }
52
+ /** Refuses a `test.use()` that names a fixture the test's own `test.extend()` does not define */
53
+ export function checkTestFixtureUse(use, layers) {
54
+ for (const [key, value] of Object.entries(pickFixtureUse(use))) {
55
+ const known = [...(layers?.keys() ?? [])];
56
+ if (key === FIXTURES_KEY) {
57
+ for (const name of Object.keys(value))
58
+ if (!layers?.has(name))
59
+ throw new TypeError(`use(): unknown fixture "${name}". ${known.length > 0 ? `This test has: ${known.join(", ")}` : "This test has no fixtures; write it with test.extend()"}`);
60
+ }
61
+ else if (!layers?.get(key)?.slice(-1)[0]?.option)
62
+ throw new TypeError(`use(): unknown option fixture "${key}"`);
63
+ }
64
+ }
@@ -0,0 +1,26 @@
1
+ import type { FixtureDef } from "./fixture-defs.js";
2
+ import { type Started } from "./fixture-start.js";
3
+ /** A worker fixture that is set up: its value, how to finish it, and the outputs the report lists */
4
+ export interface WorkerInstance {
5
+ value: unknown;
6
+ started: Started;
7
+ outputs?: Record<string, string | number | boolean> | undefined;
8
+ }
9
+ /**
10
+ * The worker-scoped fixtures of one worker. An instance is shared by the tests that need the same definition with
11
+ * the same key (the environment, the resolved options and the instances it uses), is set up on first use and is
12
+ * torn down when the worker is done, last set up first.
13
+ */
14
+ export declare class WorkerFixtures {
15
+ private readonly limitMs;
16
+ private readonly byDef;
17
+ private readonly live;
18
+ /** `limitMs`: how long a teardown may take when the fixture has no `timeout` of its own */
19
+ constructor(limitMs: number);
20
+ /** The instance of `def` under `key`: the one already set up, else the result of `make`. A failed setup is not kept */
21
+ acquire(def: FixtureDef, key: string, make: () => Promise<WorkerInstance>): Promise<WorkerInstance & {
22
+ reused: boolean;
23
+ }>;
24
+ /** Tears every instance down, last set up first, and returns the errors; a second call has nothing to do */
25
+ closeAll(): Promise<Error[]>;
26
+ }
@@ -0,0 +1,46 @@
1
+ import { finishWithin } from "./fixture-start.js";
2
+ /**
3
+ * The worker-scoped fixtures of one worker. An instance is shared by the tests that need the same definition with
4
+ * the same key (the environment, the resolved options and the instances it uses), is set up on first use and is
5
+ * torn down when the worker is done, last set up first.
6
+ */
7
+ export class WorkerFixtures {
8
+ /** `limitMs`: how long a teardown may take when the fixture has no `timeout` of its own */
9
+ constructor(limitMs) {
10
+ this.limitMs = limitMs;
11
+ this.byDef = new Map();
12
+ this.live = [];
13
+ }
14
+ /** The instance of `def` under `key`: the one already set up, else the result of `make`. A failed setup is not kept */
15
+ async acquire(def, key, make) {
16
+ let keys = this.byDef.get(def);
17
+ if (!keys)
18
+ this.byDef.set(def, (keys = new Map()));
19
+ const existing = keys.get(key);
20
+ if (existing)
21
+ return { ...(await existing), reused: true };
22
+ const created = make().then((instance) => {
23
+ this.live.push(instance);
24
+ return instance;
25
+ });
26
+ keys.set(key, created);
27
+ try {
28
+ return { ...(await created), reused: false };
29
+ }
30
+ catch (err) {
31
+ keys.delete(key);
32
+ throw err;
33
+ }
34
+ }
35
+ /** Tears every instance down, last set up first, and returns the errors; a second call has nothing to do */
36
+ async closeAll() {
37
+ const errors = [];
38
+ this.byDef.clear();
39
+ for (let one = this.live.pop(); one; one = this.live.pop()) {
40
+ const error = await finishWithin(one.started, this.limitMs);
41
+ if (error)
42
+ errors.push(error);
43
+ }
44
+ return errors;
45
+ }
46
+ }
@@ -2,11 +2,16 @@ import { isShortSecret, registerSecret } from "../config/mask.js";
2
2
  import { resolveRunSettings, setRunSettings, } from "../config/run-settings.js";
3
3
  import { loadSecrets } from "../config/secrets.js";
4
4
  import logger from "../logger/index.js";
5
+ import { fixtureCatalog } from "./fixture-registry.js";
6
+ import { checkFixtureUseNames } from "./fixture-use.js";
5
7
  /**
6
8
  * Starts a run: picks the environment, loads its secrets and registers every value for masking
7
9
  * (all of them, not only the ones a test reads), then makes the settings active.
8
10
  */
9
11
  export function prepareRun(config, overrides, options = {}) {
12
+ // A fixture name defined twice refuses the run before any test starts
13
+ const catalog = fixtureCatalog(config.fixtures ?? []);
14
+ checkFixtureUseNames(config, catalog);
10
15
  const settings = resolveRunSettings(config, overrides);
11
16
  const secrets = loadSecrets(settings.environment, options);
12
17
  for (const [name, value] of secrets) {
@@ -1,6 +1,8 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import logger from "../logger/index.js";
3
3
  import { extendLayers } from "./fixture-defs.js";
4
+ import { rememberLayers } from "./fixture-registry.js";
5
+ import { checkTestFixtureUse } from "./fixture-use.js";
4
6
  import { validateUseOptions } from "../config/use-options.js";
5
7
  import { currentStepLog, currentTestInfo, infoFor, skipFromCondition, } from "./test-info.js";
6
8
  import { runStep } from "./steps.js";
@@ -150,16 +152,21 @@ export function beforeAll(hook) {
150
152
  export function afterAll(hook) {
151
153
  addHook(currentSuite(callerFile()), "afterAll", hook);
152
154
  }
153
- /** `test.use(options)`: checked now, applied when each test of the scope runs */
154
- function use(options) {
155
- addUse(currentSuite(callerFile()), validateUseOptions(options));
155
+ /** `test.use(options)` of a test with the fixtures `extension`: checked now, applied when each test of the scope runs */
156
+ function usingFor(extension) {
157
+ return (options) => {
158
+ const checked = validateUseOptions(options);
159
+ // `fixtures` and option fixtures may only name what this test's `test.extend()` defines
160
+ checkTestFixtureUse(checked, extension);
161
+ addUse(currentSuite(callerFile()), checked);
162
+ };
156
163
  }
157
164
  /**
158
165
  * A `test` that registers its tests with the custom fixtures `extension`. The aliases are the same functions as
159
166
  * the named exports: they find the spec file from the stack, so they must not be wrapped.
160
167
  */
161
168
  function makeTest(extension) {
162
- return Object.assign(registering("test", undefined, extension), {
169
+ const made = Object.assign(registering("test", undefined, extension), {
163
170
  skip: skipping("skip", extension),
164
171
  fixme: skipping("fixme", extension),
165
172
  fail: failing(extension),
@@ -168,7 +175,7 @@ function makeTest(extension) {
168
175
  step: (title, body, options) => runStep(currentStepLog(), title, body, options),
169
176
  only: registering("test.only", "only", extension),
170
177
  info: currentTestInfo,
171
- use,
178
+ use: usingFor(extension),
172
179
  describe,
173
180
  beforeEach,
174
181
  afterEach,
@@ -176,6 +183,9 @@ function makeTest(extension) {
176
183
  afterAll,
177
184
  extend: (fixtures) => makeTest(extendLayers(extension, fixtures)),
178
185
  });
186
+ if (extension)
187
+ rememberLayers(made, extension);
188
+ return made;
179
189
  }
180
190
  export const test = makeTest();
181
191
  export function registeredTests() {
@@ -1,4 +1,4 @@
1
- import type { TestAnnotation, TestStepEntry, TestAttachmentEntry, RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
1
+ import type { TestAnnotation, TestFixtureEntry, TestStepEntry, TestAttachmentEntry, RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
2
2
  import type { SupportedBrowser } from "../types/browser.js";
3
3
  import type { Shard } from "./shard.js";
4
4
  /** What a run reports while it happens */
@@ -75,6 +75,8 @@ export interface TestEndExtras extends TestRetries {
75
75
  annotations?: TestAnnotation[];
76
76
  /** The steps the test ran with `test.step` */
77
77
  steps?: TestStepEntry[];
78
+ /** The fixtures the test set up, when it set any up */
79
+ fixtures?: TestFixtureEntry[];
78
80
  }
79
81
  export default class TestReporter implements TestSink {
80
82
  private testResults;
@@ -160,6 +160,7 @@ export default class TestReporter {
160
160
  ...(extras?.attachments && { attachments: extras.attachments }),
161
161
  ...(extras?.annotations && { annotations: extras.annotations }),
162
162
  ...(extras?.steps && { steps: extras.steps }),
163
+ ...(extras?.fixtures?.length && { fixtures: extras.fixtures }),
163
164
  ...(extras?.retry && { retry: extras.retry }),
164
165
  ...(extras?.flaky && { flaky: true }),
165
166
  ...(extras?.previousAttempts?.length && {
@@ -4,6 +4,7 @@ import type { RunnerOptions } from "./options.js";
4
4
  import type { AllHooks } from "./all-hooks.js";
5
5
  import type { PreparedRun } from "./prepare-run.js";
6
6
  import type { TestSink } from "./reporter.js";
7
+ import type { WorkerFixtures } from "./fixture-worker.js";
7
8
  /** What a worker gives its tests that the main process does not: its own port and profile, and its number */
8
9
  export interface WorkerResources {
9
10
  /** The browser's debugging port. @default `options.port`, else DEFAULT_PORT */
@@ -16,6 +17,8 @@ export interface WorkerResources {
16
17
  attempt?: number;
17
18
  /** The `beforeAll` / `afterAll` hooks of this worker: kept across tests, closed when the worker is done */
18
19
  allHooks?: AllHooks;
20
+ /** The worker-scoped fixtures of this worker: kept across tests, closed when the worker is done */
21
+ workerFixtures?: WorkerFixtures;
19
22
  }
20
23
  /** Runs one test in one browser and reports it: launch, run the body, judge the page's logs, close the browser */
21
24
  export declare function runOneTest(test: RegisteredTestCase, browserName: SupportedBrowser, run: PreparedRun, options: RunnerOptions, srcDir: string, reporter: TestSink, resources?: WorkerResources): Promise<void>;