@itmetsam/samurai-framework 0.21.4 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/api.d.ts +8 -1
  2. package/dist/api.js +1 -0
  3. package/dist/browser/browser-context.d.ts +18 -6
  4. package/dist/browser/browser-context.js +25 -7
  5. package/dist/browser/browser.d.ts +11 -2
  6. package/dist/browser/browser.js +31 -8
  7. package/dist/browser/cdp.d.ts +11 -0
  8. package/dist/browser/cdp.js +28 -0
  9. package/dist/browser/chrome-launch.d.ts +4 -0
  10. package/dist/browser/chrome-launch.js +11 -3
  11. package/dist/browser/context-cookie-store.d.ts +17 -0
  12. package/dist/browser/context-cookie-store.js +90 -0
  13. package/dist/browser/index.d.ts +8 -1
  14. package/dist/browser/index.js +5 -1
  15. package/dist/browser/keyboard.d.ts +35 -0
  16. package/dist/browser/keyboard.js +83 -0
  17. package/dist/browser/launch-firefox.js +1 -1
  18. package/dist/browser/launch-flags.d.ts +4 -1
  19. package/dist/browser/launch-flags.js +18 -5
  20. package/dist/browser/mouse.d.ts +42 -0
  21. package/dist/browser/mouse.js +66 -0
  22. package/dist/browser/page-navigation.d.ts +0 -2
  23. package/dist/browser/page-navigation.js +1 -6
  24. package/dist/browser/page-settings.d.ts +1 -1
  25. package/dist/browser/page.d.ts +20 -6
  26. package/dist/browser/page.js +24 -8
  27. package/dist/browser/session-capabilities.d.ts +10 -0
  28. package/dist/browser/session-capabilities.js +11 -0
  29. package/dist/browser/session.d.ts +2 -1
  30. package/dist/browser/session.js +3 -3
  31. package/dist/browser/storage-state.d.ts +4 -0
  32. package/dist/browser/storage-state.js +17 -0
  33. package/dist/browser/use-applier.d.ts +38 -0
  34. package/dist/browser/use-applier.js +121 -0
  35. package/dist/browser/use-commands.d.ts +36 -0
  36. package/dist/browser/use-commands.js +120 -0
  37. package/dist/browser/viewport.d.ts +3 -2
  38. package/dist/browser/viewport.js +15 -2
  39. package/dist/cli/args.d.ts +8 -4
  40. package/dist/cli/args.js +8 -5
  41. package/dist/cli/console-reporter.js +5 -1
  42. package/dist/cli/main.js +1 -0
  43. package/dist/cli/usage.d.ts +1 -1
  44. package/dist/cli/usage.js +4 -1
  45. package/dist/config/base-url.d.ts +4 -0
  46. package/dist/config/base-url.js +6 -0
  47. package/dist/config/config.js +8 -0
  48. package/dist/config/fully-parallel.d.ts +3 -0
  49. package/dist/config/fully-parallel.js +6 -0
  50. package/dist/config/group-use.d.ts +8 -0
  51. package/dist/config/group-use.js +41 -0
  52. package/dist/config/resolved-config.d.ts +2 -0
  53. package/dist/config/resolved-config.js +3 -0
  54. package/dist/config/storage-state-file.d.ts +3 -0
  55. package/dist/config/storage-state-file.js +20 -0
  56. package/dist/config/use-options.d.ts +18 -0
  57. package/dist/config/use-options.js +184 -0
  58. package/dist/config/web-server-options.d.ts +7 -0
  59. package/dist/config/web-server-options.js +111 -0
  60. package/dist/locator/check-actions.js +1 -1
  61. package/dist/locator/drag-actions.d.ts +17 -0
  62. package/dist/locator/drag-actions.js +80 -0
  63. package/dist/locator/element-actions.d.ts +10 -4
  64. package/dist/locator/element-actions.js +23 -16
  65. package/dist/locator/element-state.d.ts +5 -0
  66. package/dist/locator/element-state.js +2 -1
  67. package/dist/locator/event-actions.d.ts +10 -0
  68. package/dist/locator/event-actions.js +81 -0
  69. package/dist/locator/input-actions.d.ts +2 -11
  70. package/dist/locator/input-actions.js +3 -56
  71. package/dist/locator/key-actions.d.ts +11 -0
  72. package/dist/locator/key-actions.js +52 -0
  73. package/dist/locator/key-map.d.ts +16 -0
  74. package/dist/locator/key-map.js +76 -0
  75. package/dist/locator/locator.d.ts +35 -5
  76. package/dist/locator/locator.js +70 -16
  77. package/dist/locator/page-scripts.d.ts +2 -0
  78. package/dist/locator/page-scripts.js +7 -0
  79. package/dist/locator/pointer-actions.d.ts +44 -0
  80. package/dist/locator/pointer-actions.js +117 -0
  81. package/dist/locator/pointer-element-actions.d.ts +21 -0
  82. package/dist/locator/pointer-element-actions.js +42 -0
  83. package/dist/logger/warn-once.d.ts +4 -0
  84. package/dist/logger/warn-once.js +13 -0
  85. package/dist/request/api-request-context.d.ts +65 -0
  86. package/dist/request/api-request-context.js +198 -0
  87. package/dist/request/api-response.d.ts +34 -0
  88. package/dist/request/api-response.js +57 -0
  89. package/dist/request/cookie-jar.d.ts +48 -0
  90. package/dist/request/cookie-jar.js +164 -0
  91. package/dist/request/http-transport.d.ts +31 -0
  92. package/dist/request/http-transport.js +113 -0
  93. package/dist/request/redirect.d.ts +10 -0
  94. package/dist/request/redirect.js +21 -0
  95. package/dist/request/request-body.d.ts +27 -0
  96. package/dist/request/request-body.js +71 -0
  97. package/dist/request/request.d.ts +16 -0
  98. package/dist/request/request.js +20 -0
  99. package/dist/runner/all-hooks.d.ts +7 -1
  100. package/dist/runner/all-hooks.js +17 -1
  101. package/dist/runner/attachments.d.ts +16 -0
  102. package/dist/runner/attachments.js +60 -0
  103. package/dist/runner/fixture-args.d.ts +15 -0
  104. package/dist/runner/fixture-args.js +95 -0
  105. package/dist/runner/fixture-defs.d.ts +22 -0
  106. package/dist/runner/fixture-defs.js +104 -0
  107. package/dist/runner/fixture-scope.d.ts +33 -0
  108. package/dist/runner/fixture-scope.js +156 -0
  109. package/dist/runner/fixtures.d.ts +37 -0
  110. package/dist/runner/fixtures.js +40 -0
  111. package/dist/runner/global-hooks.d.ts +13 -0
  112. package/dist/runner/global-hooks.js +66 -0
  113. package/dist/runner/hooks.d.ts +6 -1
  114. package/dist/runner/hooks.js +15 -3
  115. package/dist/runner/options.d.ts +13 -2
  116. package/dist/runner/output-tail.d.ts +14 -0
  117. package/dist/runner/output-tail.js +30 -0
  118. package/dist/runner/pool-schedule.d.ts +22 -0
  119. package/dist/runner/pool-schedule.js +55 -0
  120. package/dist/runner/pool-units.d.ts +34 -0
  121. package/dist/runner/pool-units.js +85 -0
  122. package/dist/runner/pooled-run.d.ts +7 -5
  123. package/dist/runner/pooled-run.js +19 -9
  124. package/dist/runner/registry.d.ts +3 -39
  125. package/dist/runner/registry.js +99 -21
  126. package/dist/runner/reporter.d.ts +17 -1
  127. package/dist/runner/reporter.js +15 -2
  128. package/dist/runner/request-fixture.d.ts +14 -0
  129. package/dist/runner/request-fixture.js +25 -0
  130. package/dist/runner/run-hooks.d.ts +16 -0
  131. package/dist/runner/run-hooks.js +52 -0
  132. package/dist/runner/run-test.d.ts +1 -3
  133. package/dist/runner/run-test.js +98 -35
  134. package/dist/runner/run.d.ts +1 -0
  135. package/dist/runner/run.js +1 -0
  136. package/dist/runner/select.d.ts +15 -4
  137. package/dist/runner/select.js +38 -12
  138. package/dist/runner/serial-run.d.ts +23 -0
  139. package/dist/runner/serial-run.js +88 -0
  140. package/dist/runner/serial.d.ts +34 -0
  141. package/dist/runner/serial.js +63 -0
  142. package/dist/runner/shard.d.ts +22 -0
  143. package/dist/runner/shard.js +73 -0
  144. package/dist/runner/steps.d.ts +13 -0
  145. package/dist/runner/steps.js +67 -0
  146. package/dist/runner/suite.d.ts +8 -3
  147. package/dist/runner/suite.js +12 -3
  148. package/dist/runner/test-annotations.d.ts +16 -0
  149. package/dist/runner/test-annotations.js +47 -0
  150. package/dist/runner/test-function.d.ts +115 -0
  151. package/dist/runner/test-function.js +1 -0
  152. package/dist/runner/test-info.d.ts +104 -0
  153. package/dist/runner/test-info.js +170 -0
  154. package/dist/runner/test-runner.d.ts +1 -0
  155. package/dist/runner/test-runner.js +64 -19
  156. package/dist/runner/test-timeout.d.ts +22 -0
  157. package/dist/runner/test-timeout.js +45 -0
  158. package/dist/runner/use-options.d.ts +7 -0
  159. package/dist/runner/use-options.js +25 -0
  160. package/dist/runner/wait-for-url.d.ts +20 -0
  161. package/dist/runner/wait-for-url.js +65 -0
  162. package/dist/runner/web-server.d.ts +16 -0
  163. package/dist/runner/web-server.js +165 -0
  164. package/dist/runner/worker-pool.d.ts +13 -5
  165. package/dist/runner/worker-pool.js +85 -51
  166. package/dist/runner/worker-protocol.d.ts +8 -0
  167. package/dist/runner/worker-session.js +5 -2
  168. package/dist/runner/worker.d.ts +2 -0
  169. package/dist/runner/worker.js +7 -1
  170. package/dist/script/element-handle.d.ts +1 -1
  171. package/dist/script/element-handle.js +1 -1
  172. package/dist/types/config.d.ts +86 -2
  173. package/dist/types/extend.d.ts +30 -0
  174. package/dist/types/test.d.ts +138 -0
  175. package/dist/types/use.d.ts +85 -0
  176. package/package.json +1 -1
  177. package/dist/browser/emulation.d.ts +0 -61
  178. package/dist/browser/emulation.js +0 -180
@@ -0,0 +1,73 @@
1
+ import { serialBlockOf } from "./serial.js";
2
+ import { suiteChain } from "./suite.js";
3
+ /**
4
+ * Reads a shard: `"2/4"` (the command line's `--shard`) or `{ current: 2, total: 4 }` (the `shard` run option).
5
+ * `where` names the source in the error.
6
+ */
7
+ export function parseShard(value, where) {
8
+ const shard = shardOf(value);
9
+ if (shard &&
10
+ Number.isInteger(shard.current) &&
11
+ Number.isInteger(shard.total) &&
12
+ shard.current >= 1 &&
13
+ shard.current <= shard.total)
14
+ return { current: shard.current, total: shard.total };
15
+ const shown = typeof value === "string" ? value : JSON.stringify(value);
16
+ throw new Error(`${where}: expected current/total with 1 <= current <= total, got ${typeof value === "string" ? `"${shown}"` : String(shown)}`);
17
+ }
18
+ function shardOf(value) {
19
+ if (typeof value === "string") {
20
+ const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(value);
21
+ return match
22
+ ? { current: Number(match[1]), total: Number(match[2]) }
23
+ : undefined;
24
+ }
25
+ if (typeof value === "object" && value !== null) {
26
+ const { current, total } = value;
27
+ if (typeof current === "number" && typeof total === "number")
28
+ return { current, total };
29
+ }
30
+ return undefined;
31
+ }
32
+ /**
33
+ * What a shard keeps together: a serial block is never split, and so is a file, unless `fullyParallel` is on or a
34
+ * `describe` around the test is in `"parallel"` mode: then the test goes by itself.
35
+ */
36
+ export function shardGroupOf(test, fullyParallel) {
37
+ const block = serialBlockOf(test);
38
+ if (block)
39
+ return block.key;
40
+ const alone = fullyParallel === true ||
41
+ suiteChain(test.suite).some((s) => s.mode === "parallel");
42
+ return alone ? `test:${test.file}::${test.name}` : `file:${test.file}`;
43
+ }
44
+ /**
45
+ * The tests of one shard. The groups (in the order their first test comes) are dealt out in order, so the shards are
46
+ * as equal in tests as whole groups allow: a group goes to the shard that holds its first test when all tests are
47
+ * cut into `total` equal ranges. The shards are disjoint and together hold every test; the order stays.
48
+ */
49
+ export function shardTests(tests, { current, total }, groupOf) {
50
+ const groups = new Map();
51
+ for (const test of tests) {
52
+ const key = groupOf(test);
53
+ const members = groups.get(key);
54
+ if (members)
55
+ members.push(test);
56
+ else
57
+ groups.set(key, [test]);
58
+ }
59
+ const size = Math.floor(tests.length / total);
60
+ const extra = tests.length - size * total;
61
+ const index = current - 1;
62
+ const from = size * index + Math.min(extra, index);
63
+ const to = from + size + (index < extra ? 1 : 0);
64
+ const kept = new Set();
65
+ let first = 0;
66
+ for (const members of groups.values()) {
67
+ if (first >= from && first < to)
68
+ for (const test of members)
69
+ kept.add(test);
70
+ first += members.length;
71
+ }
72
+ return tests.filter((test) => kept.has(test));
73
+ }
@@ -0,0 +1,13 @@
1
+ import type { TestStepEntry } from "../types/test.js";
2
+ /** The steps a test has run, in the order they started */
3
+ export declare class StepLog {
4
+ readonly entries: TestStepEntry[];
5
+ }
6
+ /**
7
+ * `test.step`: runs `body` as a step of `log`, nested under the step it is called in. The step is listed with how
8
+ * it ended; a throw fails the step and goes on. A step that outlasts `options.timeout` fails with a timeout error
9
+ * (its body keeps running, as nothing can stop it).
10
+ */
11
+ export declare function runStep<T>(log: StepLog, title: unknown, body: unknown, options?: {
12
+ timeout?: number;
13
+ }): Promise<T>;
@@ -0,0 +1,67 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { checkTimeout } from "./test-timeout.js";
3
+ import { isSkipSignal } from "./skip.js";
4
+ /** The steps a test has run, in the order they started */
5
+ export class StepLog {
6
+ constructor() {
7
+ this.entries = [];
8
+ }
9
+ }
10
+ /** The step being run: a step started inside it is listed under it */
11
+ const running = new AsyncLocalStorage();
12
+ const UNFINISHED = "the test ended before the step finished";
13
+ /**
14
+ * `test.step`: runs `body` as a step of `log`, nested under the step it is called in. The step is listed with how
15
+ * it ended; a throw fails the step and goes on. A step that outlasts `options.timeout` fails with a timeout error
16
+ * (its body keeps running, as nothing can stop it).
17
+ */
18
+ export async function runStep(log, title, body, options) {
19
+ if (typeof title !== "string" || title === "")
20
+ throw new TypeError("step(): the title must be a non-empty string");
21
+ if (typeof body !== "function")
22
+ throw new TypeError("step(): needs a function as its second argument");
23
+ const limit = options?.timeout === undefined
24
+ ? 0
25
+ : checkTimeout("step()", options.timeout);
26
+ // Listed as unfinished until it ends, so a test cut short still shows where it stopped
27
+ const entry = {
28
+ title,
29
+ status: "failed",
30
+ duration: 0,
31
+ error: UNFINISHED,
32
+ };
33
+ const parent = running.getStore();
34
+ if (parent)
35
+ (parent.steps ??= []).push(entry);
36
+ else
37
+ log.entries.push(entry);
38
+ const started = Date.now();
39
+ let timer;
40
+ try {
41
+ const call = Promise.resolve().then(() => running.run(entry, body));
42
+ const value = limit === 0
43
+ ? await call
44
+ : await Promise.race([
45
+ call,
46
+ new Promise((_, reject) => {
47
+ timer = setTimeout(() => reject(new Error(`step ${JSON.stringify(title)} timed out after ${limit} ms`)), limit);
48
+ }),
49
+ ]);
50
+ entry.status = "passed";
51
+ delete entry.error;
52
+ return value;
53
+ }
54
+ catch (error) {
55
+ if (isSkipSignal(error)) {
56
+ entry.status = "skipped";
57
+ delete entry.error;
58
+ }
59
+ else
60
+ entry.error = error instanceof Error ? error.message : String(error);
61
+ throw error;
62
+ }
63
+ finally {
64
+ clearTimeout(timer);
65
+ entry.duration = Date.now() - started;
66
+ }
67
+ }
@@ -1,11 +1,12 @@
1
- import type { EachHook, Modifier, Suite } from "../types/test.js";
1
+ import type { EachHook, Modifier, Suite, SuiteMode } from "../types/test.js";
2
+ import type { UseOptions } from "../types/use.js";
2
3
  /** The four hook lists a suite can hold */
3
4
  export type HookKind = "beforeEach" | "afterEach" | "beforeAll" | "afterAll";
4
- export declare function newSuite(title?: string, parent?: Suite, modifier?: Modifier, reason?: string): Suite;
5
+ export declare function newSuite(title?: string, parent?: Suite, modifier?: Modifier, reason?: string, mode?: SuiteMode): Suite;
5
6
  /** The suite a test or hook written now belongs to: the innermost open `describe`, else the file's own */
6
7
  export declare function currentSuite(file: string): Suite;
7
8
  /** Opens a `describe` block under the current suite; `leaveSuite` closes it */
8
- export declare function enterSuite(file: string, title: string, modifier?: Modifier, reason?: string): Suite;
9
+ export declare function enterSuite(file: string, title: string, modifier?: Modifier, reason?: string, mode?: SuiteMode): Suite;
9
10
  export declare function leaveSuite(): void;
10
11
  /** Forgets every suite: a run registers its specs afresh */
11
12
  export declare function resetSuites(): void;
@@ -28,3 +29,7 @@ export declare function beforeEachOf(suite: Suite): EachHook[];
28
29
  export declare function afterEachOf(suite: Suite): EachHook[];
29
30
  /** Adds a hook to the suite being registered */
30
31
  export declare function addHook<K extends HookKind>(suite: Suite, kind: K, hook: NonNullable<Suite[K]>[number]): void;
32
+ /** Adds the options of a `test.use()` to the suite being registered */
33
+ export declare function addUse(suite: Suite, options: UseOptions): void;
34
+ /** The `test.use()` options that apply to a test, outermost suite first: merge them in this order */
35
+ export declare function useOf(suite: Suite): UseOptions[];
@@ -2,12 +2,13 @@
2
2
  const roots = new Map();
3
3
  /** The `describe` blocks being registered, outermost first */
4
4
  const open = [];
5
- export function newSuite(title, parent, modifier, reason) {
5
+ export function newSuite(title, parent, modifier, reason, mode) {
6
6
  return {
7
7
  ...(title !== undefined && { title }),
8
8
  ...(parent && { parent }),
9
9
  ...(modifier && { modifier }),
10
10
  ...(reason !== undefined && { reason }),
11
+ ...(mode && { mode }),
11
12
  };
12
13
  }
13
14
  /** The suite a test or hook written now belongs to: the innermost open `describe`, else the file's own */
@@ -23,8 +24,8 @@ export function currentSuite(file) {
23
24
  return root;
24
25
  }
25
26
  /** Opens a `describe` block under the current suite; `leaveSuite` closes it */
26
- export function enterSuite(file, title, modifier, reason) {
27
- const suite = newSuite(title, currentSuite(file), modifier, reason);
27
+ export function enterSuite(file, title, modifier, reason, mode) {
28
+ const suite = newSuite(title, currentSuite(file), modifier, reason, mode);
28
29
  open.push(suite);
29
30
  return suite;
30
31
  }
@@ -80,3 +81,11 @@ export function afterEachOf(suite) {
80
81
  export function addHook(suite, kind, hook) {
81
82
  (suite[kind] ??= []).push(hook);
82
83
  }
84
+ /** Adds the options of a `test.use()` to the suite being registered */
85
+ export function addUse(suite, options) {
86
+ (suite.use ??= []).push(options);
87
+ }
88
+ /** The `test.use()` options that apply to a test, outermost suite first: merge them in this order */
89
+ export function useOf(suite) {
90
+ return suiteChain(suite).flatMap((s) => s.use ?? []);
91
+ }
@@ -0,0 +1,16 @@
1
+ import type { RegisteredTestCase, Suite, TestError, TestFixtures, TestInfo } from "../types/test.js";
2
+ type Condition = boolean | ((fixtures: TestFixtures) => boolean);
3
+ /**
4
+ * `test.fail(...)` and `test.slow(...)` without a title. In a running test or hook it marks that test when the
5
+ * condition holds (no condition: always). In a `describe` block, or at the top of a file, it marks every test of
6
+ * `suite` instead, and the condition must be a boolean as no test runs yet.
7
+ */
8
+ export declare function markTests(type: "fail" | "slow", suite: () => Suite, condition?: Condition, description?: string): void;
9
+ /** Applies what the test and the suites around it declared (`test.fail("t", fn)`, `test.slow(true)` in a `describe`) */
10
+ export declare function applyDeclaredMarks(info: TestInfo, test: Pick<RegisteredTestCase, "suite" | "expectedFail">): void;
11
+ /**
12
+ * What a test that ends by itself is reported as, given what it is expected to do: a test expected to fail is
13
+ * passed by a failure and failed by a pass. A timeout is never the failure it was waiting for.
14
+ */
15
+ export declare function judgeExpectation(info: TestInfo | undefined, error: TestError | undefined): TestError | undefined;
16
+ export {};
@@ -0,0 +1,47 @@
1
+ import { conditionHolds, infoFor, runningScope } from "./test-info.js";
2
+ /**
3
+ * `test.fail(...)` and `test.slow(...)` without a title. In a running test or hook it marks that test when the
4
+ * condition holds (no condition: always). In a `describe` block, or at the top of a file, it marks every test of
5
+ * `suite` instead, and the condition must be a boolean as no test runs yet.
6
+ */
7
+ export function markTests(type, suite, condition = true, description) {
8
+ const label = `${type}()`;
9
+ if (description !== undefined && typeof description !== "string")
10
+ throw new TypeError(`${label}: the reason must be a string`);
11
+ if (runningScope()) {
12
+ if (conditionHolds(label, condition))
13
+ infoFor(label)[type](true, description);
14
+ return;
15
+ }
16
+ if (typeof condition === "function")
17
+ throw new Error(`${label}: a condition function needs a running test`);
18
+ if (!condition)
19
+ return;
20
+ const target = suite();
21
+ (target.marks ??= []).push({
22
+ type,
23
+ ...(description !== undefined && { description }),
24
+ });
25
+ }
26
+ /** Applies what the test and the suites around it declared (`test.fail("t", fn)`, `test.slow(true)` in a `describe`) */
27
+ export function applyDeclaredMarks(info, test) {
28
+ const chain = [];
29
+ for (let s = test.suite; s; s = s.parent)
30
+ chain.unshift(s);
31
+ for (const suite of chain)
32
+ for (const mark of suite.marks ?? [])
33
+ info[mark.type](true, mark.description);
34
+ if (test.expectedFail)
35
+ info.fail(true, test.expectedFail.description);
36
+ }
37
+ /**
38
+ * What a test that ends by itself is reported as, given what it is expected to do: a test expected to fail is
39
+ * passed by a failure and failed by a pass. A timeout is never the failure it was waiting for.
40
+ */
41
+ export function judgeExpectation(info, error) {
42
+ if (info?.expectedStatus !== "failed")
43
+ return error;
44
+ if (!error)
45
+ return { message: "expected to fail, but passed", type: "error" };
46
+ return error.type === "timeout" ? error : undefined;
47
+ }
@@ -0,0 +1,115 @@
1
+ import type { AllHook, SuiteMode, TestCaseBase, TestDetails, TestInfo, TestOptions } from "../types/test.js";
2
+ import type { UseOptions } from "../types/use.js";
3
+ import type { FixtureDefinitions, FixturesOf, MergeFixtures } from "../types/extend.js";
4
+ /** What the registry keeps of a test body; the fixtures it gets are those of the test it was written for */
5
+ export type Body = TestCaseBase["function"];
6
+ /** The fixtures of tests made by `test.extend()`: none for the plain `test` */
7
+ export type NoFixtures = Record<never, never>;
8
+ /** A test body or hook that gets the built-in fixtures and the custom ones `X` */
9
+ export type BodyOf<X> = (fixtures: FixturesOf<X>) => Promise<void>;
10
+ export type HookOf<X> = (fixtures: FixturesOf<X>) => Promise<void> | void;
11
+ /** `test(title, fn)`, `test.skip(title, fn)`, `test.skip(condition, reason)` and the other forms */
12
+ export interface TestFunction<X = NoFixtures> {
13
+ (title: string, fn: BodyOf<X>): void;
14
+ (title: string, options: TestOptions, fn: BodyOf<X>): void;
15
+ /** Registers a test that is not run */
16
+ skip: SkipFunction<X>;
17
+ /** Registers a test that is not run because it is known to be broken */
18
+ fixme: SkipFunction<X>;
19
+ /**
20
+ * Registers a test that is expected to fail (`test.fail("title", fn)`): it is reported as passed when it fails and
21
+ * as failed when it passes. Inside a test or hook, `test.fail()` or `test.fail(condition, reason)` marks the running
22
+ * test the same way; in a `describe` block, `test.fail(true, reason)` marks every test in it.
23
+ */
24
+ fail: FailFunction<X>;
25
+ /**
26
+ * Triples the timeout of the running test (`test.slow()`, `test.slow(condition, reason)`); in a `describe` block
27
+ * `test.slow(true, reason)` does it for every test in it
28
+ */
29
+ slow: ConditionFunction<X>;
30
+ /** Sets the timeout, in milliseconds, of the running test (0: none); the time already spent counts */
31
+ setTimeout: (ms: number) => void;
32
+ /**
33
+ * Runs `body` as a named step of the running test and returns its value; steps nest, and the report lists them
34
+ * with their status, duration and error. A step that throws fails and rethrows.
35
+ */
36
+ step: StepFunction;
37
+ /** Runs only this test (and the other `only` ones) */
38
+ only: {
39
+ (title: string, fn: BodyOf<X>): void;
40
+ (title: string, options: TestOptions, fn: BodyOf<X>): void;
41
+ };
42
+ /** The running test's info, in the test and its hooks; throws outside one */
43
+ info: () => TestInfo;
44
+ /**
45
+ * Sets the `use` options (locale, colorScheme, storageState, ...) for the `describe` block it is written in, or for
46
+ * the whole file at the top level, whether before or after the tests. An inner block wins over an outer one, and
47
+ * both win over config `use`.
48
+ */
49
+ use: (options: UseOptions) => void;
50
+ /** `describe`, under the name other frameworks use */
51
+ describe: DescribeFunction;
52
+ /** `beforeEach`, as `test.beforeEach` */
53
+ beforeEach: (hook: HookOf<X>) => void;
54
+ /** `afterEach`, as `test.afterEach` */
55
+ afterEach: (hook: HookOf<X>) => void;
56
+ /** `beforeAll`, as `test.beforeAll` */
57
+ beforeAll: (hook: AllHook) => void;
58
+ /** `afterAll`, as `test.afterAll` */
59
+ afterAll: (hook: AllHook) => void;
60
+ /**
61
+ * A `test` that also has the fixtures you define: each is a value, or a function `async ({ page }, use) => {
62
+ * ...set up; await use(value); ...clean up }` that may use the built-in fixtures and the other ones. A fixture is
63
+ * set up when a test or hook destructures it, and cleaned up after the test and its `afterEach` hooks. Write
64
+ * `[fixture, { auto: true }]` to set it up for every test. A fixture with the name of a built-in one replaces it.
65
+ */
66
+ extend<T extends object>(fixtures: FixtureDefinitions<T, X>): TestFunction<MergeFixtures<X, T>>;
67
+ }
68
+ /**
69
+ * With a title and a body it registers a skipped test, `{ reason }` saying why; with a condition it skips the
70
+ * running test when the condition is true
71
+ */
72
+ export interface SkipFunction<X> {
73
+ (title: string, fn: BodyOf<X>): void;
74
+ (title: string, options: TestOptions, fn: BodyOf<X>): void;
75
+ /** A function gets the test's fixtures (`({ browserName }) => browserName === "firefox"`) */
76
+ (condition: boolean | ((fixtures: FixturesOf<X>) => boolean), reason?: string): void;
77
+ }
78
+ /** `describe.skip` and `describe.fixme`: every test inside gets the modifier, and `{ reason }` says why */
79
+ export interface DescribeSkipFunction {
80
+ (title: string, fn: () => void): void;
81
+ (title: string, details: TestDetails, fn: () => void): void;
82
+ }
83
+ /** `describe(title, fn)` and the same with a modifier on every test inside */
84
+ export interface DescribeFunction {
85
+ (title: string, fn: () => void): void;
86
+ skip: DescribeSkipFunction;
87
+ fixme: DescribeSkipFunction;
88
+ only(title: string, fn: () => void): void;
89
+ /**
90
+ * Sets how the tests of the `describe` block it is written in (of the file, at the top level) run:
91
+ * `"serial"` runs them in order on one worker and skips the rest after a failure, `"parallel"` lets them run on
92
+ * different workers even when the config does not, `"default"` follows the config's `fullyParallel`
93
+ */
94
+ configure(options: {
95
+ mode: SuiteMode;
96
+ }): void;
97
+ /** A `describe` block in `"serial"` mode */
98
+ serial(title: string, fn: () => void): void;
99
+ /** A `describe` block in `"parallel"` mode */
100
+ parallel(title: string, fn: () => void): void;
101
+ }
102
+ /** `test.fail()` and `test.slow()`: with a condition (a function gets the test's fixtures), or none for "always" */
103
+ export interface ConditionFunction<X> {
104
+ (): void;
105
+ (condition: boolean | ((fixtures: FixturesOf<X>) => boolean), reason?: string): void;
106
+ }
107
+ /** `test.fail("title", fn)` registers a test expected to fail; the other forms mark the running test */
108
+ export interface FailFunction<X> extends ConditionFunction<X> {
109
+ (title: string, fn: BodyOf<X>): void;
110
+ (title: string, options: TestOptions, fn: BodyOf<X>): void;
111
+ }
112
+ /** `test.step(title, body, { timeout })` */
113
+ export type StepFunction = <T>(title: string, body: () => Promise<T> | T, options?: {
114
+ timeout?: number;
115
+ }) => Promise<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,104 @@
1
+ import type { SupportedBrowser } from "../types/browser.js";
2
+ import type { RegisteredTestCase, TestAnnotation, TestAttachment, TestError, TestFixtures, TestInfo, TestStepEntry } from "../types/test.js";
3
+ import { type AttachInput } from "./attachments.js";
4
+ import { StepLog } from "./steps.js";
5
+ import { TestTimeout } from "./test-timeout.js";
6
+ /** What the running test's info needs of the run */
7
+ export interface TestInfoSources {
8
+ test: RegisteredTestCase;
9
+ browserName: SupportedBrowser;
10
+ workerIndex: number;
11
+ /** The attempt, counting from 0 */
12
+ retry: number;
13
+ settings: {
14
+ baseURL: string | undefined;
15
+ timeout: number;
16
+ };
17
+ /** The run's result folder: attachment paths in the report are relative to it */
18
+ resultDir: string;
19
+ outputDir: string;
20
+ /** The test's time limit, which the info can change; without it the info only keeps the number */
21
+ timeout?: TestTimeout;
22
+ }
23
+ /** What a report entry lists from the info, besides the rest of its extras */
24
+ export interface InfoReportExtras {
25
+ attachments?: {
26
+ name: string;
27
+ contentType: string;
28
+ path: string;
29
+ }[];
30
+ annotations?: TestAnnotation[];
31
+ steps?: TestStepEntry[];
32
+ }
33
+ /** The `TestInfo` of one attempt of a test. The runner creates it, the test only reads it. */
34
+ export declare class TestInfoImpl implements TestInfo {
35
+ readonly title: string;
36
+ readonly titlePath: string[];
37
+ readonly file: string;
38
+ readonly line?: number;
39
+ readonly column?: number;
40
+ readonly retry: number;
41
+ readonly workerIndex: number;
42
+ readonly project: {
43
+ name: string;
44
+ };
45
+ readonly config: {
46
+ baseURL: string | undefined;
47
+ timeout: number;
48
+ };
49
+ annotations: TestAnnotation[];
50
+ expectedStatus: TestInfo["expectedStatus"];
51
+ /** The steps the test ran with `test.step` */
52
+ readonly stepLog: StepLog;
53
+ readonly errors: TestError[];
54
+ readonly outputDir: string;
55
+ private readonly saved;
56
+ private readonly used;
57
+ private readonly started;
58
+ private ended;
59
+ private result;
60
+ private readonly resultDir;
61
+ private readonly limit;
62
+ constructor(from: TestInfoSources);
63
+ get timeout(): number;
64
+ setTimeout(ms: number): void;
65
+ slow(condition?: boolean, description?: string): void;
66
+ fail(condition?: boolean, description?: string): void;
67
+ skip(condition?: boolean, description?: string): void;
68
+ fixme(condition?: boolean, description?: string): void;
69
+ private annotated;
70
+ private note;
71
+ get status(): TestInfo["status"];
72
+ get duration(): number;
73
+ get attachments(): readonly TestAttachment[];
74
+ attach(name: string, options: AttachInput): Promise<void>;
75
+ /** The body (with its `beforeEach` hooks) ended: `error` is what it threw, if anything. Runs before the `afterEach` hooks. */
76
+ bodyDone(error: unknown): void;
77
+ /** The fields a report entry adds for this attempt: nothing when there is nothing to list */
78
+ reportExtras(): InfoReportExtras;
79
+ }
80
+ /** The running test: its info and its fixtures (a condition function of `test.skip` gets them) */
81
+ export interface TestScope {
82
+ info: TestInfo;
83
+ fixtures: TestFixtures;
84
+ }
85
+ /** Runs `fn` (the body and hooks of one test) so that `test.info()` and `test.skip(fn)` find the test */
86
+ export declare function runInTestScope<T>(scope: TestScope, fn: () => T): T;
87
+ /** `test.info()` */
88
+ export declare function currentTestInfo(): TestInfo;
89
+ /** The running test's scope, if a test (or one of its hooks) is running */
90
+ export declare function runningScope(): TestScope | undefined;
91
+ /** The running test's info for `label` (`setTimeout()`); throws when no test is running */
92
+ export declare function infoFor(label: string): TestInfo;
93
+ /** The running test's steps; throws when no test is running */
94
+ export declare function currentStepLog(): StepLog;
95
+ /**
96
+ * Whether a condition of `test.skip`, `test.fail` or `test.slow` holds: a boolean as it is, a function called with
97
+ * the running test's fixtures (it must be synchronous). `label` starts the errors (`test.skip()`).
98
+ */
99
+ export declare function conditionHolds(label: string, condition: boolean | ((fixtures: TestFixtures) => boolean)): boolean;
100
+ /**
101
+ * `test.skip(condition, reason)` and `test.fixme(...)` from inside a test or its hooks: throws the skip signal
102
+ * when `condition` is true, or when it is a function and returns something truthy.
103
+ */
104
+ export declare function skipFromCondition(annotation: "skip" | "fixme", condition: boolean | ((fixtures: TestFixtures) => boolean), reason?: string): void;
@@ -0,0 +1,170 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import path from "node:path";
3
+ import { saveAttachment } from "./attachments.js";
4
+ import { isSkipSignal, SkipSignal } from "./skip.js";
5
+ import { StepLog } from "./steps.js";
6
+ import { toTestError } from "./test-error.js";
7
+ import { checkTimeout, TestTimeout } from "./test-timeout.js";
8
+ /** The `TestInfo` of one attempt of a test. The runner creates it, the test only reads it. */
9
+ export class TestInfoImpl {
10
+ constructor(from) {
11
+ this.annotations = [];
12
+ this.expectedStatus = "passed";
13
+ /** The steps the test ran with `test.step` */
14
+ this.stepLog = new StepLog();
15
+ this.errors = [];
16
+ this.saved = [];
17
+ this.used = new Set();
18
+ this.started = Date.now();
19
+ const { test } = from;
20
+ this.title = test.titlePath[test.titlePath.length - 1] ?? test.name;
21
+ this.titlePath = [...test.titlePath];
22
+ this.file = test.file;
23
+ if (test.line !== undefined)
24
+ this.line = test.line;
25
+ if (test.column !== undefined)
26
+ this.column = test.column;
27
+ this.retry = from.retry;
28
+ this.workerIndex = from.workerIndex;
29
+ // A run for a group is named after it (as a Playwright project is), else after the browser
30
+ this.project = { name: test.group ?? from.browserName };
31
+ this.config = { ...from.settings };
32
+ this.resultDir = from.resultDir;
33
+ this.outputDir = from.outputDir;
34
+ this.limit =
35
+ from.timeout ?? new TestTimeout(from.settings.timeout, () => { });
36
+ }
37
+ get timeout() {
38
+ return this.limit.ms;
39
+ }
40
+ setTimeout(ms) {
41
+ this.limit.set(checkTimeout("setTimeout()", ms));
42
+ }
43
+ slow(condition = true, description) {
44
+ if (!condition || this.annotated("slow"))
45
+ return;
46
+ this.note("slow", description);
47
+ this.limit.set(this.limit.ms * 3);
48
+ }
49
+ fail(condition = true, description) {
50
+ if (!condition)
51
+ return;
52
+ this.expectedStatus = "failed";
53
+ if (!this.annotated("fail"))
54
+ this.note("fail", description);
55
+ }
56
+ skip(condition = true, description) {
57
+ if (condition)
58
+ throw new SkipSignal("skip", description);
59
+ }
60
+ fixme(condition = true, description) {
61
+ if (condition)
62
+ throw new SkipSignal("fixme", description);
63
+ }
64
+ annotated(type) {
65
+ return this.annotations.some((a) => a.type === type);
66
+ }
67
+ note(type, description) {
68
+ this.annotations.push({
69
+ type,
70
+ ...(description !== undefined && { description }),
71
+ });
72
+ }
73
+ get status() {
74
+ return this.result;
75
+ }
76
+ get duration() {
77
+ return (this.ended ?? Date.now()) - this.started;
78
+ }
79
+ get attachments() {
80
+ return this.saved;
81
+ }
82
+ async attach(name, options) {
83
+ this.saved.push(await saveAttachment(this.outputDir, this.used, name, options));
84
+ }
85
+ /** The body (with its `beforeEach` hooks) ended: `error` is what it threw, if anything. Runs before the `afterEach` hooks. */
86
+ bodyDone(error) {
87
+ this.ended = Date.now();
88
+ if (error === undefined)
89
+ this.result = "passed";
90
+ else if (isSkipSignal(error))
91
+ this.result = "skipped";
92
+ else {
93
+ this.result = "failed";
94
+ this.errors.push(toTestError(error));
95
+ }
96
+ }
97
+ /** The fields a report entry adds for this attempt: nothing when there is nothing to list */
98
+ reportExtras() {
99
+ return {
100
+ ...(this.saved.length > 0 && {
101
+ attachments: this.saved.map((a) => ({
102
+ name: a.name,
103
+ contentType: a.contentType,
104
+ path: path.relative(this.resultDir, a.path).split(path.sep).join("/"),
105
+ })),
106
+ }),
107
+ ...(this.annotations.length > 0 && {
108
+ annotations: this.annotations.map((a) => ({ ...a })),
109
+ }),
110
+ ...(this.stepLog.entries.length > 0 && {
111
+ steps: structuredClone(this.stepLog.entries),
112
+ }),
113
+ };
114
+ }
115
+ }
116
+ const scopes = new AsyncLocalStorage();
117
+ /** Runs `fn` (the body and hooks of one test) so that `test.info()` and `test.skip(fn)` find the test */
118
+ export function runInTestScope(scope, fn) {
119
+ return scopes.run(scope, fn);
120
+ }
121
+ /** `test.info()` */
122
+ export function currentTestInfo() {
123
+ const scope = scopes.getStore();
124
+ if (!scope)
125
+ throw new Error("info(): no test is running");
126
+ return scope.info;
127
+ }
128
+ /** The running test's scope, if a test (or one of its hooks) is running */
129
+ export function runningScope() {
130
+ return scopes.getStore();
131
+ }
132
+ /** The running test's info for `label` (`setTimeout()`); throws when no test is running */
133
+ export function infoFor(label) {
134
+ const scope = scopes.getStore();
135
+ if (!scope)
136
+ throw new Error(`${label}: no test is running`);
137
+ return scope.info;
138
+ }
139
+ /** The running test's steps; throws when no test is running */
140
+ export function currentStepLog() {
141
+ const info = infoFor("step()");
142
+ if (!(info instanceof TestInfoImpl))
143
+ throw new Error("step(): no test is running");
144
+ return info.stepLog;
145
+ }
146
+ /**
147
+ * Whether a condition of `test.skip`, `test.fail` or `test.slow` holds: a boolean as it is, a function called with
148
+ * the running test's fixtures (it must be synchronous). `label` starts the errors (`test.skip()`).
149
+ */
150
+ export function conditionHolds(label, condition) {
151
+ if (typeof condition !== "function")
152
+ return Boolean(condition);
153
+ const scope = scopes.getStore();
154
+ if (!scope)
155
+ throw new Error(`${label}: a condition function needs a running test`);
156
+ const result = condition(scope.fixtures);
157
+ if (result instanceof Promise) {
158
+ result.catch(() => { });
159
+ throw new Error(`${label}: the condition function must be synchronous`);
160
+ }
161
+ return Boolean(result);
162
+ }
163
+ /**
164
+ * `test.skip(condition, reason)` and `test.fixme(...)` from inside a test or its hooks: throws the skip signal
165
+ * when `condition` is true, or when it is a function and returns something truthy.
166
+ */
167
+ export function skipFromCondition(annotation, condition, reason) {
168
+ if (conditionHolds(`test.${annotation}()`, condition))
169
+ throw new SkipSignal(annotation, reason);
170
+ }