@itmetsam/samurai-framework 0.21.4 → 0.21.5

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 (71) hide show
  1. package/dist/api.d.ts +3 -1
  2. package/dist/browser/browser.d.ts +9 -0
  3. package/dist/browser/browser.js +25 -5
  4. package/dist/browser/cdp.d.ts +11 -0
  5. package/dist/browser/cdp.js +28 -0
  6. package/dist/browser/chrome-launch.d.ts +4 -0
  7. package/dist/browser/chrome-launch.js +2 -2
  8. package/dist/browser/index.d.ts +5 -0
  9. package/dist/browser/index.js +4 -0
  10. package/dist/browser/launch-firefox.js +1 -1
  11. package/dist/browser/launch-flags.d.ts +4 -1
  12. package/dist/browser/launch-flags.js +18 -5
  13. package/dist/browser/page-settings.d.ts +1 -1
  14. package/dist/browser/page.d.ts +4 -0
  15. package/dist/browser/page.js +8 -1
  16. package/dist/browser/session-capabilities.d.ts +10 -0
  17. package/dist/browser/session-capabilities.js +11 -0
  18. package/dist/browser/session.d.ts +2 -1
  19. package/dist/browser/session.js +3 -3
  20. package/dist/browser/storage-state.d.ts +6 -0
  21. package/dist/browser/storage-state.js +37 -0
  22. package/dist/browser/use-applier.d.ts +27 -0
  23. package/dist/browser/use-applier.js +94 -0
  24. package/dist/browser/viewport.d.ts +3 -2
  25. package/dist/browser/viewport.js +15 -2
  26. package/dist/config/config.js +2 -0
  27. package/dist/config/use-options.d.ts +16 -0
  28. package/dist/config/use-options.js +128 -0
  29. package/dist/logger/warn-once.d.ts +4 -0
  30. package/dist/logger/warn-once.js +13 -0
  31. package/dist/runner/attachments.d.ts +16 -0
  32. package/dist/runner/attachments.js +60 -0
  33. package/dist/runner/fixture-args.d.ts +15 -0
  34. package/dist/runner/fixture-args.js +95 -0
  35. package/dist/runner/fixture-defs.d.ts +22 -0
  36. package/dist/runner/fixture-defs.js +103 -0
  37. package/dist/runner/fixture-scope.d.ts +33 -0
  38. package/dist/runner/fixture-scope.js +156 -0
  39. package/dist/runner/fixtures.d.ts +34 -0
  40. package/dist/runner/fixtures.js +39 -0
  41. package/dist/runner/hooks.d.ts +6 -1
  42. package/dist/runner/hooks.js +15 -3
  43. package/dist/runner/registry.d.ts +3 -39
  44. package/dist/runner/registry.js +93 -19
  45. package/dist/runner/reporter.d.ts +7 -1
  46. package/dist/runner/reporter.js +3 -0
  47. package/dist/runner/run-test.d.ts +1 -3
  48. package/dist/runner/run-test.js +92 -34
  49. package/dist/runner/steps.d.ts +13 -0
  50. package/dist/runner/steps.js +67 -0
  51. package/dist/runner/suite.d.ts +5 -0
  52. package/dist/runner/suite.js +8 -0
  53. package/dist/runner/test-annotations.d.ts +16 -0
  54. package/dist/runner/test-annotations.js +47 -0
  55. package/dist/runner/test-function.d.ts +103 -0
  56. package/dist/runner/test-function.js +1 -0
  57. package/dist/runner/test-info.d.ts +104 -0
  58. package/dist/runner/test-info.js +169 -0
  59. package/dist/runner/test-runner.js +2 -1
  60. package/dist/runner/test-timeout.d.ts +22 -0
  61. package/dist/runner/test-timeout.js +45 -0
  62. package/dist/runner/use-options.d.ts +7 -0
  63. package/dist/runner/use-options.js +18 -0
  64. package/dist/runner/worker-session.js +2 -1
  65. package/dist/script/element-handle.d.ts +1 -1
  66. package/dist/script/element-handle.js +1 -1
  67. package/dist/types/config.d.ts +3 -2
  68. package/dist/types/extend.d.ts +30 -0
  69. package/dist/types/test.d.ts +120 -0
  70. package/dist/types/use.d.ts +47 -0
  71. package/package.json +1 -1
@@ -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,34 @@
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 { RunnerOptions } from "./options.js";
6
+ import type { PreparedRun } from "./prepare-run.js";
7
+ import type { OutputSink } from "../wait/output-sink.js";
8
+ /** What `beforeAll` and `afterAll` hooks get: no page, only what a run knows */
9
+ export declare function allHookFixtures(run: PreparedRun, browserName: SupportedBrowser, workerIndex?: number): AllHookFixtures;
10
+ /** What one run of a test builds its fixtures from */
11
+ export interface FixtureSources {
12
+ launched: {
13
+ page: Page;
14
+ browser: Browser;
15
+ };
16
+ browserName: SupportedBrowser;
17
+ workerIndex: number;
18
+ run: PreparedRun;
19
+ test: RegisteredTestCase;
20
+ options: RunnerOptions;
21
+ sink: OutputSink;
22
+ info: TestInfo;
23
+ /** What the `use` options came to in this browser */
24
+ device: {
25
+ isMobile: boolean;
26
+ hasTouch: boolean;
27
+ };
28
+ }
29
+ /**
30
+ * The fixtures a test body and its each-hooks receive. `context`, `baseURL` and `testInfo` are getters: nothing
31
+ * is looked up until a test reads one (and `page.context()` throws for a page without one, which only a test
32
+ * that asks for it should see).
33
+ */
34
+ export declare function buildTestFixtures(from: FixtureSources): TestFixtures;
@@ -0,0 +1,39 @@
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` 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, } = 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
+ testInfo: { enumerable: true, get: () => info },
38
+ });
39
+ }
@@ -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>): 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>;
@@ -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)
@@ -1,41 +1,5 @@
1
- import type { AllHook, EachHook, RegisteredTestCase, TestCaseBase, TestDetails, TestOptions } from "../types/test.js";
2
- type Body = TestCaseBase["function"];
3
- /** `test(title, fn)`, `test.skip(title, fn)`, `test.skip(condition, reason)` and the other forms */
4
- export interface TestFunction {
5
- (title: string, fn: Body): void;
6
- (title: string, options: TestOptions, fn: Body): void;
7
- /** Registers a test that is not run */
8
- skip: SkipFunction;
9
- /** Registers a test that is not run because it is known to be broken */
10
- fixme: SkipFunction;
11
- /** Runs only this test (and the other `only` ones) */
12
- only: {
13
- (title: string, fn: Body): void;
14
- (title: string, options: TestOptions, fn: Body): void;
15
- };
16
- }
17
- /**
18
- * With a title and a body it registers a skipped test, `{ reason }` saying why; with a condition it skips the
19
- * running test when the condition is true
20
- */
21
- interface SkipFunction {
22
- (title: string, fn: Body): void;
23
- (title: string, options: TestOptions, fn: Body): void;
24
- (condition: boolean, reason?: string): void;
25
- }
26
- /** `describe.skip` and `describe.fixme`: every test inside gets the modifier, and `{ reason }` says why */
27
- interface DescribeSkipFunction {
28
- (title: string, fn: () => void): void;
29
- (title: string, details: TestDetails, fn: () => void): void;
30
- }
31
- /** `describe(title, fn)` and the same with a modifier on every test inside */
32
- export interface DescribeFunction {
33
- (title: string, fn: () => void): void;
34
- skip: DescribeSkipFunction;
35
- fixme: DescribeSkipFunction;
36
- only(title: string, fn: () => void): void;
37
- }
38
- export declare const test: TestFunction;
1
+ import type { AllHook, EachHook, RegisteredTestCase } from "../types/test.js";
2
+ import type { DescribeFunction, TestFunction } from "./test-function.js";
39
3
  export declare const describe: DescribeFunction;
40
4
  /** Runs before each test of the `describe` block it is written in (of the file, at the top level) */
41
5
  export declare function beforeEach(hook: EachHook): void;
@@ -45,7 +9,7 @@ export declare function afterEach(hook: EachHook): void;
45
9
  export declare function beforeAll(hook: AllHook): void;
46
10
  /** Runs once per worker, when the worker is done, for every `describe` block it ran a `beforeAll` for */
47
11
  export declare function afterAll(hook: AllHook): void;
12
+ export declare const test: TestFunction;
48
13
  export declare function registeredTests(): readonly RegisteredTestCase[];
49
14
  /** Forgets every registered test: a run registers its specs afresh */
50
15
  export declare function clearRegistry(): void;
51
- export {};
@@ -1,8 +1,12 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import logger from "../logger/index.js";
3
- import { SkipSignal } from "./skip.js";
3
+ import { extendLayers } from "./fixture-defs.js";
4
+ import { validateUseOptions } from "../config/use-options.js";
5
+ import { currentStepLog, currentTestInfo, infoFor, skipFromCondition, } from "./test-info.js";
6
+ import { runStep } from "./steps.js";
7
+ import { markTests } from "./test-annotations.js";
4
8
  import { validateTestIO } from "./test-io.js";
5
- import { addHook, currentSuite, enterSuite, leaveSuite, modifierOf, resetSuites, } from "./suite.js";
9
+ import { addHook, addUse, currentSuite, enterSuite, leaveSuite, modifierOf, resetSuites, } from "./suite.js";
6
10
  const registered = [];
7
11
  /** Runs `fn` as the `describe` block `title`, under `modifier` when there is one. `fn` runs immediately and must be synchronous. */
8
12
  function openDescribe(file, title, fn, modifier, reason) {
@@ -18,8 +22,8 @@ function openDescribe(file, title, fn, modifier, reason) {
18
22
  leaveSuite();
19
23
  }
20
24
  }
21
- function add(file, title, fn, modifier, extras = {}) {
22
- const { reason, inputs, outputs } = extras;
25
+ function add({ file, line, column }, title, fn, modifier, extras = {}) {
26
+ const { reason, inputs, outputs, extension, expectedFail } = extras;
23
27
  const suite = currentSuite(file);
24
28
  const titlePath = [...titlesOf(suite), title];
25
29
  const name = titlePath.join(" > ");
@@ -28,10 +32,13 @@ function add(file, title, fn, modifier, extras = {}) {
28
32
  titlePath,
29
33
  function: fn,
30
34
  file,
35
+ ...(line !== undefined && { line, column }),
31
36
  suite,
32
37
  ...modifierOf({ modifier, ...(reason !== undefined && { reason }) }, suite),
33
38
  ...(inputs && { inputs }),
34
39
  ...(outputs && { outputs }),
40
+ ...(extension && { extension }),
41
+ ...(expectedFail && { expectedFail }),
35
42
  });
36
43
  logger.debug("Registered test %s", name);
37
44
  }
@@ -58,28 +65,59 @@ function bodyAndOptions(call, title, second, third) {
58
65
  };
59
66
  }
60
67
  /** `test()` and `test.only()`: a reason only makes sense with skip and fixme */
61
- function registering(call, modifier) {
68
+ function registering(call, modifier, extension) {
62
69
  return ((title, second, third) => {
63
70
  const { fn, reason, ...io } = bodyAndOptions(call, title, second, third);
64
71
  if (reason !== undefined)
65
72
  throw new Error(`${call}(): reason only applies to skip and fixme`);
66
73
  // Frames: Error, callerFile, this function, the spec
67
- add(callerFile(), title, fn, modifier, io);
74
+ add(callerLocation(), title, fn, modifier, {
75
+ ...io,
76
+ ...(extension && { extension }),
77
+ });
68
78
  });
69
79
  }
70
80
  /** `test.skip` and `test.fixme`: a title and a body register a test with the modifier; a condition skips the running test */
71
- function skipping(annotation) {
81
+ function skipping(annotation, extension) {
72
82
  return ((first, second, third) => {
73
83
  if (typeof first === "string") {
74
84
  const { fn, ...extras } = bodyAndOptions(`test.${annotation}`, first, second, third);
75
85
  // Frames: Error, callerFile, this function, the spec
76
- add(callerFile(), first, fn, annotation, extras);
86
+ add(callerLocation(), first, fn, annotation, {
87
+ ...extras,
88
+ ...(extension && { extension }),
89
+ });
77
90
  return;
78
91
  }
79
- if (first)
80
- throw new SkipSignal(annotation, second);
92
+ skipFromCondition(annotation, first, second);
81
93
  });
82
94
  }
95
+ /**
96
+ * `test.fail`: a title and a body register a test that is expected to fail; otherwise the running test (or, in a
97
+ * `describe` block, every test in it) is marked
98
+ */
99
+ function failing(extension) {
100
+ return ((first, second, third) => {
101
+ if (typeof first !== "string") {
102
+ // Frames: Error, callerFile, this function, the spec
103
+ const file = callerFile();
104
+ return markTests("fail", () => currentSuite(file), first, second);
105
+ }
106
+ const { fn, reason, ...io } = bodyAndOptions("test.fail", first, second, third);
107
+ // Frames: Error, callerFile, this function, the spec
108
+ add(callerLocation(), first, fn, undefined, {
109
+ ...io,
110
+ expectedFail: { ...(reason !== undefined && { description: reason }) },
111
+ ...(extension && { extension }),
112
+ });
113
+ });
114
+ }
115
+ /** `test.slow`: triples the timeout of the running test, or of every test of the `describe` block it is written in */
116
+ const slow = ((condition, reason) => {
117
+ // Frames: Error, callerFile, this function, the spec
118
+ const file = callerFile();
119
+ markTests("slow", () => currentSuite(file), condition, reason);
120
+ });
83
121
  /** `describe.skip` and `describe.fixme`, with or without `{ reason }` */
84
122
  function describeSkipping(annotation) {
85
123
  return ((title, second, third) => {
@@ -87,11 +125,6 @@ function describeSkipping(annotation) {
87
125
  openDescribe(callerFile(), title, fn, annotation, reason);
88
126
  });
89
127
  }
90
- export const test = Object.assign(registering("test"), {
91
- skip: skipping("skip"),
92
- fixme: skipping("fixme"),
93
- only: registering("test.only", "only"),
94
- });
95
128
  export const describe = Object.assign((title, fn) => openDescribe(callerFile(), title, fn), {
96
129
  skip: describeSkipping("skip"),
97
130
  fixme: describeSkipping("fixme"),
@@ -113,6 +146,34 @@ export function beforeAll(hook) {
113
146
  export function afterAll(hook) {
114
147
  addHook(currentSuite(callerFile()), "afterAll", hook);
115
148
  }
149
+ /** `test.use(options)`: checked now, applied when each test of the scope runs */
150
+ function use(options) {
151
+ addUse(currentSuite(callerFile()), validateUseOptions(options));
152
+ }
153
+ /**
154
+ * A `test` that registers its tests with the custom fixtures `extension`. The aliases are the same functions as
155
+ * the named exports: they find the spec file from the stack, so they must not be wrapped.
156
+ */
157
+ function makeTest(extension) {
158
+ return Object.assign(registering("test", undefined, extension), {
159
+ skip: skipping("skip", extension),
160
+ fixme: skipping("fixme", extension),
161
+ fail: failing(extension),
162
+ slow,
163
+ setTimeout: (ms) => infoFor("setTimeout()").setTimeout(ms),
164
+ step: (title, body, options) => runStep(currentStepLog(), title, body, options),
165
+ only: registering("test.only", "only", extension),
166
+ info: currentTestInfo,
167
+ use,
168
+ describe,
169
+ beforeEach,
170
+ afterEach,
171
+ beforeAll,
172
+ afterAll,
173
+ extend: (fixtures) => makeTest(extendLayers(extension, fixtures)),
174
+ });
175
+ }
176
+ export const test = makeTest();
116
177
  export function registeredTests() {
117
178
  return registered;
118
179
  }
@@ -121,10 +182,23 @@ export function clearRegistry() {
121
182
  registered.length = 0;
122
183
  resetSuites();
123
184
  }
124
- /** File of the code that called the entry point: stack lines are Error, callerFile, the entry point, the caller */
125
- function callerFile() {
185
+ /** The place of the caller: stack lines are Error, callerLocation, the entry point, the caller */
186
+ function callerLocation() {
126
187
  const caller = new Error().stack?.split("\n")[3] ?? "";
127
- const match = caller.match(/\((.+?):\d+:\d+\)$/) ?? caller.match(/at (.+?):\d+:\d+$/);
188
+ return parseFrame(caller);
189
+ }
190
+ /** File of the code that called the entry point (the same frames as `callerLocation`) */
191
+ function callerFile() {
192
+ return parseFrame(new Error().stack?.split("\n")[3] ?? "").file;
193
+ }
194
+ function parseFrame(frame) {
195
+ const match = frame.match(/\((.+?):(\d+):(\d+)\)$/) ??
196
+ frame.match(/at (.+?):(\d+):(\d+)$/);
128
197
  const location = match?.[1] ?? "";
129
- return location.startsWith("file://") ? fileURLToPath(location) : location;
198
+ const file = location.startsWith("file://")
199
+ ? fileURLToPath(location)
200
+ : location;
201
+ return match
202
+ ? { file, line: Number(match[2]), column: Number(match[3]) }
203
+ : { file };
130
204
  }
@@ -1,4 +1,4 @@
1
- import type { RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
1
+ import type { TestAnnotation, TestStepEntry, TestAttachmentEntry, RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
2
2
  import type { SupportedBrowser } from "../types/browser.js";
3
3
  /** What a run reports while it happens */
4
4
  export type RunEvent = {
@@ -60,6 +60,12 @@ export interface TestEndExtras extends TestRetries {
60
60
  inputs?: Record<string, string>;
61
61
  /** The test's saved outputs, when it declares any */
62
62
  outputs?: Record<string, string>;
63
+ /** Files the test attached with `testInfo.attach`, relative to the result folder */
64
+ attachments?: TestAttachmentEntry[];
65
+ /** The notes the test pushed to `testInfo.annotations` */
66
+ annotations?: TestAnnotation[];
67
+ /** The steps the test ran with `test.step` */
68
+ steps?: TestStepEntry[];
63
69
  }
64
70
  export default class TestReporter implements TestSink {
65
71
  private testResults;
@@ -148,6 +148,9 @@ export default class TestReporter {
148
148
  ...(extras?.screenshots?.length && { screenshots: extras.screenshots }),
149
149
  ...(extras?.inputs && { inputs: extras.inputs }),
150
150
  ...(extras?.outputs && { outputs: extras.outputs }),
151
+ ...(extras?.attachments && { attachments: extras.attachments }),
152
+ ...(extras?.annotations && { annotations: extras.annotations }),
153
+ ...(extras?.steps && { steps: extras.steps }),
151
154
  ...(extras?.retry && { retry: extras.retry }),
152
155
  ...(extras?.flaky && { flaky: true }),
153
156
  ...(extras?.previousAttempts?.length && {
@@ -1,5 +1,5 @@
1
1
  import type { SupportedBrowser } from "../types/browser.js";
2
- import type { AllHookFixtures, RegisteredTestCase } from "../types/test.js";
2
+ import type { RegisteredTestCase } from "../types/test.js";
3
3
  import type { RunnerOptions } from "./options.js";
4
4
  import type { AllHooks } from "./all-hooks.js";
5
5
  import type { PreparedRun } from "./prepare-run.js";
@@ -17,7 +17,5 @@ export interface WorkerResources {
17
17
  /** The `beforeAll` / `afterAll` hooks of this worker: kept across tests, closed when the worker is done */
18
18
  allHooks?: AllHooks;
19
19
  }
20
- /** What `beforeAll` and `afterAll` hooks get: no page, only what a run knows */
21
- export declare function allHookFixtures(run: PreparedRun, browserName: SupportedBrowser, workerIndex?: number): AllHookFixtures;
22
20
  /** Runs one test in one browser and reports it: launch, run the body, judge the page's logs, close the browser */
23
21
  export declare function runOneTest(test: RegisteredTestCase, browserName: SupportedBrowser, run: PreparedRun, options: RunnerOptions, srcDir: string, reporter: TestSink, resources?: WorkerResources): Promise<void>;