@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,3 +1,4 @@
1
+ import { isFixtureUseKey } from "./fixture-use.js";
1
2
  import { USE_OPTION_NAMES, validateUseOptions } from "./use-options.js";
2
3
  const isSize = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
3
4
  /** Rewrites `use(): locale must ...` as `config.groups[1].use.locale: locale must ...` */
@@ -7,34 +8,43 @@ function atGroup(place, err) {
7
8
  const problem = err.message.replace(/^use\(\): /, "");
8
9
  const name = /^(?:invalid |unknown )?(\w+)/.exec(problem)?.[1];
9
10
  const known = name !== undefined &&
10
- (name === "viewport" || USE_OPTION_NAMES.includes(name));
11
+ (name === "viewport" ||
12
+ USE_OPTION_NAMES.includes(name) ||
13
+ isFixtureUseKey(name));
11
14
  throw new TypeError(`${place}${known ? `.${name}` : ""}: ${problem}`);
12
15
  }
16
+ /** Checks one `use` of a group or an environment; throws `<place>.locale: what is wrong` */
17
+ function validateUseAt(place, use) {
18
+ try {
19
+ const isObject = typeof use === "object" && use !== null && !Array.isArray(use);
20
+ const { viewport, ...options } = (isObject ? use : {});
21
+ // Anything but an object is refused here, with the message `test.use()` gives
22
+ validateUseOptions(isObject ? options : use);
23
+ if (viewport !== undefined &&
24
+ viewport !== null &&
25
+ !(isSize(viewport.width) && isSize(viewport.height)))
26
+ throw new TypeError(`viewport must be { width, height } with whole numbers above 0, or null, got ${JSON.stringify(viewport)}`);
27
+ }
28
+ catch (err) {
29
+ atGroup(place, err);
30
+ }
31
+ }
13
32
  /**
14
- * Checks the `use` of every group of the config: the options of config `use` (`test.use()` ones, and `viewport`).
15
- * Throws `config.groups[1].use.locale: what is wrong`.
33
+ * Checks the `use` of every group of the config: the options of config `use` (`test.use()` ones, and `viewport`),
34
+ * and the fixture keys. Throws `config.groups[1].use.locale: what is wrong`.
16
35
  */
17
36
  export function validateGroupUse(config) {
18
37
  config.groups?.forEach((group, index) => {
19
- if (group.use === undefined)
20
- return;
21
- const place = `config.groups[${index}].use`;
22
- try {
23
- const raw = group.use;
24
- const isObject = typeof raw === "object" && raw !== null && !Array.isArray(raw);
25
- const { viewport, ...options } = (isObject ? raw : {});
26
- // Anything but an object is refused here, with the message `test.use()` gives
27
- validateUseOptions(isObject ? options : raw);
28
- if (viewport !== undefined &&
29
- viewport !== null &&
30
- !(isSize(viewport.width) && isSize(viewport.height)))
31
- throw new TypeError(`viewport must be { width, height } with whole numbers above 0, or null, got ${JSON.stringify(viewport)}`);
32
- }
33
- catch (err) {
34
- atGroup(place, err);
35
- }
38
+ if (group.use !== undefined)
39
+ validateUseAt(`config.groups[${index}].use`, group.use);
36
40
  });
37
41
  }
42
+ /** Checks the `use` of every environment the same way: `config.environments.production.use.locale: what is wrong` */
43
+ export function validateEnvironmentUse(config) {
44
+ for (const [name, environment] of Object.entries(config.environments ?? {}))
45
+ if (environment?.use !== undefined)
46
+ validateUseAt(`config.environments.${name}.use`, environment.use);
47
+ }
38
48
  /** The `use` of the group called `name`; nothing when there is no such group or it has none */
39
49
  export function groupUseOf(groups, name) {
40
50
  return groups?.find((group) => group.name === name)?.use ?? {};
@@ -1,3 +1,4 @@
1
+ import { checkFixtureUse, isFixtureUseKey } from "./fixture-use.js";
1
2
  /** The options of `use`: config, group, `test.use()` and (those that can change at run time) `page.use()` */
2
3
  export const USE_OPTION_NAMES = [
3
4
  "locale",
@@ -47,9 +48,12 @@ export function validateUseOptions(options, allowOthers = false) {
47
48
  const renamed = RENAMED[key];
48
49
  if (renamed !== undefined)
49
50
  fail(`unknown option "${key}"; use "${renamed}"`);
50
- if (!allowOthers && !USE_OPTION_NAMES.includes(key))
51
+ if (!allowOthers &&
52
+ !USE_OPTION_NAMES.includes(key) &&
53
+ !isFixtureUseKey(key))
51
54
  fail(`unknown option "${key}"`);
52
55
  }
56
+ checkFixtureUse(options);
53
57
  const use = options;
54
58
  for (const name of BOOLEANS)
55
59
  if (use[name] !== undefined && typeof use[name] !== "boolean")
@@ -1,7 +1,8 @@
1
+ import { type FixtureMeta } from "./fixture-meta.js";
1
2
  /** The function of a fixture: sets the value up, calls `use(value)`, cleans up after `use` returns */
2
3
  type FixtureSetup = (args: Record<string, unknown>, use: (value: unknown) => Promise<void>) => Promise<void>;
3
4
  /** One definition of a fixture: the value or the setup function, what it depends on, whether every test gets it */
4
- export interface FixtureDef {
5
+ export interface FixtureDef extends FixtureMeta {
5
6
  name: string;
6
7
  auto: boolean;
7
8
  deps: string[];
@@ -1,4 +1,5 @@
1
1
  import { requestedBy } from "./fixture-args.js";
2
+ import { checkedOptions, OPTIONS_SUFFIX, } from "./fixture-meta.js";
2
3
  /** The fixtures the runner builds for every test: a custom fixture of the same name overrides it */
3
4
  export const BUILT_IN_FIXTURES = new Set([
4
5
  "page",
@@ -16,42 +17,50 @@ export const BUILT_IN_FIXTURES = new Set([
16
17
  "isMobile",
17
18
  "hasTouch",
18
19
  ]);
19
- const OPTION_KEYS = new Set(["auto", "scope"]);
20
20
  function isPlainObject(value) {
21
21
  if (typeof value !== "object" || value === null)
22
22
  return false;
23
23
  const proto = Object.getPrototypeOf(value);
24
24
  return proto === Object.prototype || proto === null;
25
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
26
  function definitionOf(name, raw) {
40
27
  const [fixture, options] = Array.isArray(raw) && raw.length === 2 && isPlainObject(raw[1])
41
28
  ? [raw[0], checkedOptions(name, raw[1])]
42
- : [raw, { auto: false }];
29
+ : [raw, checkedOptions(name, {})];
30
+ const { auto, meta } = options;
31
+ checkOptionFixture(name, fixture, meta);
43
32
  if (typeof fixture !== "function")
44
- return { name, auto: options.auto, deps: [], value: fixture };
33
+ return { name, auto, deps: [], value: fixture, ...meta };
45
34
  const requested = requestedBy(fixture);
46
35
  if (requested.kind === "opaque" || requested.rest)
47
36
  throw new Error(`extend(): fixture "${name}" must destructure the fixtures it uses in its first argument, as ({ page }, use) or ({}, use)`);
48
37
  return {
49
38
  name,
50
- auto: options.auto,
39
+ auto,
51
40
  deps: requested.names,
52
41
  setup: fixture,
42
+ ...meta,
53
43
  };
54
44
  }
45
+ /** `option: true` and `fields` go together with a name ending in `Options` and a plain object as the value */
46
+ function checkOptionFixture(name, fixture, meta) {
47
+ const suffixed = name.endsWith(OPTIONS_SUFFIX) && name !== OPTIONS_SUFFIX;
48
+ if (!meta.option) {
49
+ if (meta.fields)
50
+ throw new Error(suffixed
51
+ ? `extend(): fixture "${name}" has fields but option is not true`
52
+ : `extend(): fixture "${name}" has fields but is not an option fixture; put them on "${name}${OPTIONS_SUFFIX}" (option: true)`);
53
+ return;
54
+ }
55
+ if (!suffixed)
56
+ throw new Error(`extend(): fixture "${name}" is an option fixture, so its name must end in "${OPTIONS_SUFFIX}"`);
57
+ if (typeof fixture === "function")
58
+ throw new Error(`extend(): option fixture "${name}" must be a plain object, not a function`);
59
+ if (!isPlainObject(fixture))
60
+ throw new Error(`extend(): option fixture "${name}" must be a plain object`);
61
+ }
62
+ /** The name of the fixture an option fixture belongs to */
63
+ const fixtureOf = (optionName) => optionName.slice(0, -OPTIONS_SUFFIX.length);
55
64
  /** Where a dependency of `name` (at definition `level`) comes from: another definition, or the built-in fixture */
56
65
  export function dependencyOf(layers, name, level, dep) {
57
66
  if (dep === name)
@@ -65,6 +74,25 @@ export function dependencyOf(layers, name, level, dep) {
65
74
  return { name: dep, level: defs.length - 1 };
66
75
  return BUILT_IN_FIXTURES.has(dep) ? "built-in" : undefined;
67
76
  }
77
+ /** The built-in fixtures a worker fixture may use: those that do not belong to one test or one page */
78
+ const WORKER_BUILT_INS = new Set([
79
+ "browserName",
80
+ "workerIndex",
81
+ "env",
82
+ "secrets",
83
+ "baseURL",
84
+ ]);
85
+ /** A worker fixture outlives every test: it may use the run's values, other worker fixtures and plain values */
86
+ function checkWorkerDependency(layers, def, dep, target) {
87
+ if (target === "built-in") {
88
+ if (!WORKER_BUILT_INS.has(dep))
89
+ throw new Error(`extend(): worker fixture "${def.name}" cannot use "${dep}"; it lives longer than one test (it may use ${[...WORKER_BUILT_INS].join(", ")}, other worker fixtures and values)`);
90
+ return;
91
+ }
92
+ const used = target && layers.get(target.name)?.[target.level];
93
+ if (used && used.scope !== "worker" && used.setup)
94
+ throw new Error(`extend(): worker fixture "${def.name}" cannot use the test-scoped fixture "${dep}"`);
95
+ }
68
96
  /** Throws for a dependency nobody defines and for a cycle (`a` needs `b` needs `a`) */
69
97
  function validate(layers) {
70
98
  const done = new Set();
@@ -83,14 +111,19 @@ function validate(layers) {
83
111
  const target = dependencyOf(layers, name, level, dep);
84
112
  if (target === undefined)
85
113
  throw new Error(`extend(): fixture "${name}" depends on unknown fixture "${dep}"`);
114
+ if (def.scope === "worker")
115
+ checkWorkerDependency(layers, def, dep, target);
86
116
  if (target !== "built-in")
87
117
  walk(target.name, target.level, [...path, key]);
88
118
  }
89
119
  done.add(key);
90
120
  };
91
- for (const [name, defs] of layers)
121
+ for (const [name, defs] of layers) {
122
+ if (defs[defs.length - 1]?.option && !layers.has(fixtureOf(name)))
123
+ throw new Error(`extend(): option fixture "${name}" has no fixture "${fixtureOf(name)}"`);
92
124
  for (let level = 0; level < defs.length; level++)
93
125
  walk(name, level, []);
126
+ }
94
127
  }
95
128
  /** The fixtures after `test.extend(definitions)` on a test that has `base`; throws `extend(): ...` for a definition that cannot work */
96
129
  export function extendLayers(base, definitions) {
@@ -0,0 +1,20 @@
1
+ import type { FixtureField, FixtureOutput } from "../types/extend.js";
2
+ /** What a fixture's options say besides `auto`: its scope, and the metadata the app's catalog and the report use */
3
+ export interface FixtureMeta {
4
+ scope: "test" | "worker";
5
+ label?: string;
6
+ icon?: string;
7
+ description?: string;
8
+ outputs?: FixtureOutput[];
9
+ timeout?: number;
10
+ /** The fixture is the options of the fixture named without the `Options` suffix */
11
+ option: boolean;
12
+ fields?: FixtureField[];
13
+ }
14
+ /** The suffix that pairs an option fixture with its fixture: `order` reads `orderOptions` */
15
+ export declare const OPTIONS_SUFFIX = "Options";
16
+ /** Checks the options of `[fixture, options]` and returns what the definition keeps; throws `extend(): ...` */
17
+ export declare function checkedOptions(name: string, options: Record<string, unknown>): {
18
+ auto: boolean;
19
+ meta: FixtureMeta;
20
+ };
@@ -0,0 +1,109 @@
1
+ /** The suffix that pairs an option fixture with its fixture: `order` reads `orderOptions` */
2
+ export const OPTIONS_SUFFIX = "Options";
3
+ const OPTION_KEYS = new Set([
4
+ "auto",
5
+ "scope",
6
+ "label",
7
+ "icon",
8
+ "description",
9
+ "outputs",
10
+ "timeout",
11
+ "option",
12
+ "fields",
13
+ ]);
14
+ const FIELD_TYPES = [
15
+ "text",
16
+ "code",
17
+ "expression",
18
+ "number",
19
+ "select",
20
+ "segmented",
21
+ "switch",
22
+ "secret",
23
+ "variable",
24
+ ];
25
+ const fail = (name, problem) => {
26
+ throw new Error(`extend(): fixture "${name}": ${problem}`);
27
+ };
28
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29
+ function checkOutputs(name, outputs) {
30
+ if (!Array.isArray(outputs))
31
+ return fail(name, "outputs must be an array");
32
+ const seen = new Set();
33
+ for (const output of outputs) {
34
+ if (!isObject(output) ||
35
+ typeof output.name !== "string" ||
36
+ output.name === "")
37
+ fail(name, "every output needs a name");
38
+ const { name: key, label, secret } = output;
39
+ if (seen.has(key))
40
+ fail(name, `output "${key}" is listed twice`);
41
+ seen.add(key);
42
+ if (label !== undefined && typeof label !== "string")
43
+ fail(name, `output "${key}": label must be a string`);
44
+ if (secret !== undefined && typeof secret !== "boolean")
45
+ fail(name, `output "${key}": secret must be true or false`);
46
+ }
47
+ return outputs;
48
+ }
49
+ function checkFields(name, fields) {
50
+ if (!Array.isArray(fields))
51
+ return fail(name, "fields must be an array");
52
+ const seen = new Set();
53
+ for (const field of fields) {
54
+ if (!isObject(field) || typeof field.path !== "string" || field.path === "")
55
+ fail(name, "every field needs a path");
56
+ const { path, type, options } = field;
57
+ if (!FIELD_TYPES.includes(type))
58
+ fail(name, `field "${path}" has an unknown type ${JSON.stringify(type)}`);
59
+ if (seen.has(path))
60
+ fail(name, `field "${path}" is listed twice`);
61
+ seen.add(path);
62
+ if ((type === "select" || type === "segmented") && !Array.isArray(options))
63
+ fail(name, `field "${path}" needs options`);
64
+ }
65
+ return fields;
66
+ }
67
+ /** Checks the options of `[fixture, options]` and returns what the definition keeps; throws `extend(): ...` */
68
+ export function checkedOptions(name, options) {
69
+ for (const key of Object.keys(options))
70
+ if (!OPTION_KEYS.has(key))
71
+ throw new Error(`extend(): fixture "${name}" has an unknown option "${key}"`);
72
+ if (options.auto !== undefined && typeof options.auto !== "boolean")
73
+ fail(name, "auto must be true or false");
74
+ if (options.scope !== undefined &&
75
+ !["test", "worker"].includes(options.scope))
76
+ fail(name, 'scope must be "test" or "worker"');
77
+ if (options.label !== undefined && typeof options.label !== "string")
78
+ fail(name, "label must be a string");
79
+ if (options.icon !== undefined &&
80
+ (typeof options.icon !== "string" || options.icon === ""))
81
+ fail(name, "icon must be a non-empty string");
82
+ if (options.description !== undefined &&
83
+ typeof options.description !== "string")
84
+ fail(name, "description must be a string");
85
+ if (options.timeout !== undefined &&
86
+ !(typeof options.timeout === "number" && options.timeout > 0))
87
+ fail(name, "timeout must be a number of milliseconds above 0");
88
+ if (options.option !== undefined && typeof options.option !== "boolean")
89
+ fail(name, "option must be true or false");
90
+ const meta = {
91
+ scope: options.scope === "worker" ? "worker" : "test",
92
+ option: options.option === true,
93
+ ...(options.label !== undefined && { label: options.label }),
94
+ ...(options.icon !== undefined && { icon: options.icon }),
95
+ ...(options.description !== undefined && {
96
+ description: options.description,
97
+ }),
98
+ ...(options.timeout !== undefined && {
99
+ timeout: options.timeout,
100
+ }),
101
+ ...(options.outputs !== undefined && {
102
+ outputs: checkOutputs(name, options.outputs),
103
+ }),
104
+ ...(options.fields !== undefined && {
105
+ fields: checkFields(name, options.fields),
106
+ }),
107
+ };
108
+ return { auto: options.auto === true, meta };
109
+ }
@@ -0,0 +1,8 @@
1
+ import type { TestFixtures } from "../types/test.js";
2
+ import type { FixtureDef } from "./fixture-defs.js";
3
+ /**
4
+ * The value an option fixture gives: its default with the values from `use` over it, key by key. A field of type
5
+ * `secret` holds the name of a secret and one of type `variable` the name of a variable; they are replaced by the
6
+ * secret's or the variable's value here, so the fixture never sees the name.
7
+ */
8
+ export declare function resolveOptions(def: FixtureDef, fromUse: Readonly<Record<string, unknown>> | undefined, base: Pick<TestFixtures, "env" | "secrets">): Record<string, unknown>;
@@ -0,0 +1,35 @@
1
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
+ /** The object at `path` (dots go into objects) and the last key, or nothing when the path leads nowhere */
3
+ function parentOf(value, path) {
4
+ const keys = path.split(".");
5
+ let parent = value;
6
+ for (const key of keys.slice(0, -1)) {
7
+ const next = parent[key];
8
+ if (!isObject(next))
9
+ return undefined;
10
+ parent = next;
11
+ }
12
+ return { parent, key: keys[keys.length - 1] };
13
+ }
14
+ /**
15
+ * The value an option fixture gives: its default with the values from `use` over it, key by key. A field of type
16
+ * `secret` holds the name of a secret and one of type `variable` the name of a variable; they are replaced by the
17
+ * secret's or the variable's value here, so the fixture never sees the name.
18
+ */
19
+ export function resolveOptions(def, fromUse, base) {
20
+ const merged = structuredClone({
21
+ ...def.value,
22
+ ...fromUse,
23
+ });
24
+ for (const field of def.fields ?? []) {
25
+ if (field.type !== "secret" && field.type !== "variable")
26
+ continue;
27
+ const place = parentOf(merged, field.path);
28
+ const name = place?.parent[place.key];
29
+ if (!place || typeof name !== "string" || name === "")
30
+ continue;
31
+ place.parent[place.key] =
32
+ field.type === "secret" ? base.secrets[name] : base.env[name];
33
+ }
34
+ return merged;
35
+ }
@@ -0,0 +1,11 @@
1
+ import type { FixtureDef } from "./fixture-defs.js";
2
+ /** A value as the report lists it */
3
+ type OutputValue = string | number | boolean;
4
+ /**
5
+ * The declared outputs of a fixture that the report and the app may show: the non-secret keys of its value.
6
+ * Throws when a declared output is not a string, a number or a boolean (the value must be a flat record).
7
+ */
8
+ export declare function outputsOf(def: FixtureDef, value: unknown): Record<string, OutputValue> | undefined;
9
+ /** JSON with the keys of every object in order, so equal options give equal text */
10
+ export declare function stableJson(value: unknown): string;
11
+ export {};
@@ -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
- /** A custom fixture whose setup threw or never called `use` */
4
- export declare class FixtureSetupError extends Error {
5
- constructor(name: string, cause: unknown);
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 first error (a
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
  }