@itmetsam/samurai-framework 0.21.5 → 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 (174) hide show
  1. package/dist/api.d.ts +8 -3
  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 +4 -4
  6. package/dist/browser/browser.js +11 -8
  7. package/dist/browser/chrome-launch.js +9 -1
  8. package/dist/browser/context-cookie-store.d.ts +17 -0
  9. package/dist/browser/context-cookie-store.js +90 -0
  10. package/dist/browser/index.d.ts +3 -1
  11. package/dist/browser/index.js +1 -1
  12. package/dist/browser/keyboard.d.ts +35 -0
  13. package/dist/browser/keyboard.js +83 -0
  14. package/dist/browser/mouse.d.ts +42 -0
  15. package/dist/browser/mouse.js +66 -0
  16. package/dist/browser/page-content.d.ts +4 -0
  17. package/dist/browser/page-content.js +12 -0
  18. package/dist/browser/page-events.d.ts +11 -0
  19. package/dist/browser/page-events.js +23 -0
  20. package/dist/browser/page-logging.d.ts +24 -0
  21. package/dist/browser/page-logging.js +43 -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-viewport.d.ts +21 -0
  26. package/dist/browser/page-viewport.js +33 -0
  27. package/dist/browser/page.d.ts +23 -16
  28. package/dist/browser/page.js +48 -71
  29. package/dist/browser/screenshot.d.ts +6 -0
  30. package/dist/browser/screenshot.js +14 -0
  31. package/dist/browser/storage-state.d.ts +0 -2
  32. package/dist/browser/storage-state.js +0 -20
  33. package/dist/browser/use-applier.d.ts +16 -5
  34. package/dist/browser/use-applier.js +44 -17
  35. package/dist/browser/use-commands.d.ts +36 -0
  36. package/dist/browser/use-commands.js +120 -0
  37. package/dist/cli/args.d.ts +8 -4
  38. package/dist/cli/args.js +8 -5
  39. package/dist/cli/console-reporter.js +5 -1
  40. package/dist/cli/main.js +1 -0
  41. package/dist/cli/usage.d.ts +1 -1
  42. package/dist/cli/usage.js +4 -1
  43. package/dist/config/base-url.d.ts +4 -0
  44. package/dist/config/base-url.js +6 -0
  45. package/dist/config/config.js +7 -0
  46. package/dist/config/fixture-use.d.ts +19 -0
  47. package/dist/config/fixture-use.js +44 -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 +10 -0
  51. package/dist/config/group-use.js +51 -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 +3 -1
  57. package/dist/config/use-options.js +74 -14
  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/request/api-request-context.d.ts +65 -0
  84. package/dist/request/api-request-context.js +198 -0
  85. package/dist/request/api-response.d.ts +34 -0
  86. package/dist/request/api-response.js +57 -0
  87. package/dist/request/cookie-jar.d.ts +48 -0
  88. package/dist/request/cookie-jar.js +164 -0
  89. package/dist/request/http-transport.d.ts +31 -0
  90. package/dist/request/http-transport.js +113 -0
  91. package/dist/request/redirect.d.ts +10 -0
  92. package/dist/request/redirect.js +21 -0
  93. package/dist/request/request-body.d.ts +27 -0
  94. package/dist/request/request-body.js +71 -0
  95. package/dist/request/request.d.ts +16 -0
  96. package/dist/request/request.js +20 -0
  97. package/dist/runner/all-hooks.d.ts +7 -1
  98. package/dist/runner/all-hooks.js +17 -1
  99. package/dist/runner/fixture-defs.d.ts +2 -1
  100. package/dist/runner/fixture-defs.js +52 -18
  101. package/dist/runner/fixture-meta.d.ts +20 -0
  102. package/dist/runner/fixture-meta.js +109 -0
  103. package/dist/runner/fixture-options.d.ts +8 -0
  104. package/dist/runner/fixture-options.js +35 -0
  105. package/dist/runner/fixture-outputs.d.ts +11 -0
  106. package/dist/runner/fixture-outputs.js +28 -0
  107. package/dist/runner/fixture-registry.d.ts +35 -0
  108. package/dist/runner/fixture-registry.js +73 -0
  109. package/dist/runner/fixture-scope.d.ts +39 -9
  110. package/dist/runner/fixture-scope.js +139 -70
  111. package/dist/runner/fixture-start.d.ts +27 -0
  112. package/dist/runner/fixture-start.js +76 -0
  113. package/dist/runner/fixture-use.d.ts +23 -0
  114. package/dist/runner/fixture-use.js +64 -0
  115. package/dist/runner/fixture-worker.d.ts +26 -0
  116. package/dist/runner/fixture-worker.js +46 -0
  117. package/dist/runner/fixtures.d.ts +4 -1
  118. package/dist/runner/fixtures.js +3 -2
  119. package/dist/runner/global-hooks.d.ts +13 -0
  120. package/dist/runner/global-hooks.js +66 -0
  121. package/dist/runner/options.d.ts +13 -2
  122. package/dist/runner/output-tail.d.ts +14 -0
  123. package/dist/runner/output-tail.js +30 -0
  124. package/dist/runner/pool-schedule.d.ts +22 -0
  125. package/dist/runner/pool-schedule.js +55 -0
  126. package/dist/runner/pool-units.d.ts +34 -0
  127. package/dist/runner/pool-units.js +85 -0
  128. package/dist/runner/pooled-run.d.ts +7 -5
  129. package/dist/runner/pooled-run.js +19 -9
  130. package/dist/runner/prepare-run.js +5 -0
  131. package/dist/runner/registry.js +21 -7
  132. package/dist/runner/reporter.d.ts +13 -1
  133. package/dist/runner/reporter.js +13 -2
  134. package/dist/runner/request-fixture.d.ts +14 -0
  135. package/dist/runner/request-fixture.js +25 -0
  136. package/dist/runner/run-hooks.d.ts +16 -0
  137. package/dist/runner/run-hooks.js +52 -0
  138. package/dist/runner/run-test.d.ts +3 -0
  139. package/dist/runner/run-test.js +23 -3
  140. package/dist/runner/run.d.ts +8 -0
  141. package/dist/runner/run.js +10 -0
  142. package/dist/runner/select.d.ts +15 -4
  143. package/dist/runner/select.js +38 -12
  144. package/dist/runner/serial-run.d.ts +23 -0
  145. package/dist/runner/serial-run.js +88 -0
  146. package/dist/runner/serial.d.ts +34 -0
  147. package/dist/runner/serial.js +63 -0
  148. package/dist/runner/shard.d.ts +22 -0
  149. package/dist/runner/shard.js +73 -0
  150. package/dist/runner/suite.d.ts +3 -3
  151. package/dist/runner/suite.js +4 -3
  152. package/dist/runner/test-function.d.ts +15 -3
  153. package/dist/runner/test-info.js +2 -1
  154. package/dist/runner/test-runner.d.ts +1 -0
  155. package/dist/runner/test-runner.js +67 -18
  156. package/dist/runner/use-options.d.ts +18 -4
  157. package/dist/runner/use-options.js +33 -6
  158. package/dist/runner/wait-for-url.d.ts +20 -0
  159. package/dist/runner/wait-for-url.js +65 -0
  160. package/dist/runner/web-server.d.ts +16 -0
  161. package/dist/runner/web-server.js +165 -0
  162. package/dist/runner/worker-pool.d.ts +13 -5
  163. package/dist/runner/worker-pool.js +85 -51
  164. package/dist/runner/worker-protocol.d.ts +8 -0
  165. package/dist/runner/worker-session.js +9 -1
  166. package/dist/runner/worker.d.ts +2 -0
  167. package/dist/runner/worker.js +7 -1
  168. package/dist/types/config.d.ts +117 -21
  169. package/dist/types/extend.d.ts +54 -2
  170. package/dist/types/test.d.ts +42 -0
  171. package/dist/types/use.d.ts +59 -7
  172. package/package.json +1 -1
  173. package/dist/browser/emulation.d.ts +0 -61
  174. package/dist/browser/emulation.js +0 -180
@@ -2,11 +2,16 @@ import { isShortSecret, registerSecret } from "../config/mask.js";
2
2
  import { resolveRunSettings, setRunSettings, } from "../config/run-settings.js";
3
3
  import { loadSecrets } from "../config/secrets.js";
4
4
  import logger from "../logger/index.js";
5
+ import { fixtureCatalog } from "./fixture-registry.js";
6
+ import { checkFixtureUseNames } from "./fixture-use.js";
5
7
  /**
6
8
  * Starts a run: picks the environment, loads its secrets and registers every value for masking
7
9
  * (all of them, not only the ones a test reads), then makes the settings active.
8
10
  */
9
11
  export function prepareRun(config, overrides, options = {}) {
12
+ // A fixture name defined twice refuses the run before any test starts
13
+ const catalog = fixtureCatalog(config.fixtures ?? []);
14
+ checkFixtureUseNames(config, catalog);
10
15
  const settings = resolveRunSettings(config, overrides);
11
16
  const secrets = loadSecrets(settings.environment, options);
12
17
  for (const [name, value] of secrets) {
@@ -1,16 +1,19 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import logger from "../logger/index.js";
3
3
  import { extendLayers } from "./fixture-defs.js";
4
+ import { rememberLayers } from "./fixture-registry.js";
5
+ import { checkTestFixtureUse } from "./fixture-use.js";
4
6
  import { validateUseOptions } from "../config/use-options.js";
5
7
  import { currentStepLog, currentTestInfo, infoFor, skipFromCondition, } from "./test-info.js";
6
8
  import { runStep } from "./steps.js";
7
9
  import { markTests } from "./test-annotations.js";
8
10
  import { validateTestIO } from "./test-io.js";
9
11
  import { addHook, addUse, currentSuite, enterSuite, leaveSuite, modifierOf, resetSuites, } from "./suite.js";
12
+ import { configureSuite } from "./serial.js";
10
13
  const registered = [];
11
14
  /** Runs `fn` as the `describe` block `title`, under `modifier` when there is one. `fn` runs immediately and must be synchronous. */
12
- function openDescribe(file, title, fn, modifier, reason) {
13
- enterSuite(file, title, modifier, reason);
15
+ function openDescribe(file, title, fn, modifier, reason, mode) {
16
+ enterSuite(file, title, modifier, reason, mode);
14
17
  try {
15
18
  const result = fn();
16
19
  if (result instanceof Promise) {
@@ -129,6 +132,9 @@ export const describe = Object.assign((title, fn) => openDescribe(callerFile(),
129
132
  skip: describeSkipping("skip"),
130
133
  fixme: describeSkipping("fixme"),
131
134
  only: (title, fn) => openDescribe(callerFile(), title, fn, "only"),
135
+ configure: (options) => configureSuite(currentSuite(callerFile()), options),
136
+ serial: (title, fn) => openDescribe(callerFile(), title, fn, undefined, undefined, "serial"),
137
+ parallel: (title, fn) => openDescribe(callerFile(), title, fn, undefined, undefined, "parallel"),
132
138
  });
133
139
  /** Runs before each test of the `describe` block it is written in (of the file, at the top level) */
134
140
  export function beforeEach(hook) {
@@ -146,16 +152,21 @@ export function beforeAll(hook) {
146
152
  export function afterAll(hook) {
147
153
  addHook(currentSuite(callerFile()), "afterAll", hook);
148
154
  }
149
- /** `test.use(options)`: checked now, applied when each test of the scope runs */
150
- function use(options) {
151
- addUse(currentSuite(callerFile()), validateUseOptions(options));
155
+ /** `test.use(options)` of a test with the fixtures `extension`: checked now, applied when each test of the scope runs */
156
+ function usingFor(extension) {
157
+ return (options) => {
158
+ const checked = validateUseOptions(options);
159
+ // `fixtures` and option fixtures may only name what this test's `test.extend()` defines
160
+ checkTestFixtureUse(checked, extension);
161
+ addUse(currentSuite(callerFile()), checked);
162
+ };
152
163
  }
153
164
  /**
154
165
  * A `test` that registers its tests with the custom fixtures `extension`. The aliases are the same functions as
155
166
  * the named exports: they find the spec file from the stack, so they must not be wrapped.
156
167
  */
157
168
  function makeTest(extension) {
158
- return Object.assign(registering("test", undefined, extension), {
169
+ const made = Object.assign(registering("test", undefined, extension), {
159
170
  skip: skipping("skip", extension),
160
171
  fixme: skipping("fixme", extension),
161
172
  fail: failing(extension),
@@ -164,7 +175,7 @@ function makeTest(extension) {
164
175
  step: (title, body, options) => runStep(currentStepLog(), title, body, options),
165
176
  only: registering("test.only", "only", extension),
166
177
  info: currentTestInfo,
167
- use,
178
+ use: usingFor(extension),
168
179
  describe,
169
180
  beforeEach,
170
181
  afterEach,
@@ -172,6 +183,9 @@ function makeTest(extension) {
172
183
  afterAll,
173
184
  extend: (fixtures) => makeTest(extendLayers(extension, fixtures)),
174
185
  });
186
+ if (extension)
187
+ rememberLayers(made, extension);
188
+ return made;
175
189
  }
176
190
  export const test = makeTest();
177
191
  export function registeredTests() {
@@ -1,5 +1,6 @@
1
- import type { TestAnnotation, TestStepEntry, TestAttachmentEntry, RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
1
+ import type { TestAnnotation, TestFixtureEntry, TestStepEntry, TestAttachmentEntry, RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
2
2
  import type { SupportedBrowser } from "../types/browser.js";
3
+ import type { Shard } from "./shard.js";
3
4
  /** What a run reports while it happens */
4
5
  export type RunEvent = {
5
6
  type: "run-start";
@@ -9,11 +10,15 @@ export type RunEvent = {
9
10
  browsers: SupportedBrowser[];
10
11
  /** How many tests run at the same time; absent when they run one after the other */
11
12
  workers?: number;
13
+ /** The slice of the selected tests this run holds, when it is a shard */
14
+ shard?: Shard;
12
15
  } | {
13
16
  type: "test-start";
14
17
  name: string;
15
18
  file: string;
16
19
  browser: SupportedBrowser;
20
+ /** The group the test runs for, when the run selected groups; absent otherwise */
21
+ group?: string;
17
22
  /** The attempt, counting from 1, when the test is run again after a failure; absent for the first */
18
23
  retry?: number;
19
24
  } | {
@@ -21,6 +26,8 @@ export type RunEvent = {
21
26
  name: string;
22
27
  file: string;
23
28
  browser: SupportedBrowser;
29
+ /** The group the test ran for, when the run selected groups; absent otherwise */
30
+ group?: string;
24
31
  result: TestResult;
25
32
  } | {
26
33
  type: "run-end";
@@ -33,6 +40,8 @@ export interface ReporterOptions {
33
40
  browsers: SupportedBrowser[];
34
41
  /** Where the report is written. @default <cwd>/result/report.json */
35
42
  output?: string | false;
43
+ /** The slice of the selected tests this run holds; recorded in the report */
44
+ shard?: Shard;
36
45
  /** Called with every event, in order. A throwing listener is ignored */
37
46
  onEvent?: (event: RunEvent) => void;
38
47
  }
@@ -66,6 +75,8 @@ export interface TestEndExtras extends TestRetries {
66
75
  annotations?: TestAnnotation[];
67
76
  /** The steps the test ran with `test.step` */
68
77
  steps?: TestStepEntry[];
78
+ /** The fixtures the test set up, when it set any up */
79
+ fixtures?: TestFixtureEntry[];
69
80
  }
70
81
  export default class TestReporter implements TestSink {
71
82
  private testResults;
@@ -74,6 +85,7 @@ export default class TestReporter implements TestSink {
74
85
  private browsers;
75
86
  private output;
76
87
  private listener;
88
+ private shard;
77
89
  /** Keys of the run's tests in the order the report lists them; unset: the order they started in */
78
90
  private order;
79
91
  constructor(options: ReporterOptions);
@@ -2,10 +2,12 @@ import { mkdir, writeFile } from "fs/promises";
2
2
  import path from "path";
3
3
  import { projectDir } from "../config/config.js";
4
4
  import { maskDeep } from "../config/mask.js";
5
- /** Results are keyed by file, full title and browser, so equal titles in different files or describe blocks, and the same test in two browsers, stay apart */
5
+ /** Results are keyed by file, full title, browser and group, so equal titles in different files or describe blocks, and the same test in two browsers or for two groups, stay apart */
6
6
  function keyOf(test, browser) {
7
- return `${test.file}::${test.name}::${browser}`;
7
+ return `${test.file}::${test.name}::${browser}${test.group === undefined ? "" : `::${test.group}`}`;
8
8
  }
9
+ /** The `group` of a test's report entry and events: nothing when no group was selected */
10
+ const groupOf = (test) => test.group === undefined ? {} : { group: test.group };
9
11
  export default class TestReporter {
10
12
  constructor(options) {
11
13
  this.testResults = new Map();
@@ -14,6 +16,7 @@ export default class TestReporter {
14
16
  this.output =
15
17
  options.output ?? path.join(projectDir(), "result/report.json");
16
18
  this.listener = options.onEvent;
19
+ this.shard = options.shard;
17
20
  }
18
21
  emit(event) {
19
22
  try {
@@ -43,6 +46,7 @@ export default class TestReporter {
43
46
  total,
44
47
  browsers: this.browsers,
45
48
  ...(workers > 1 && { workers }),
49
+ ...(this.shard && { shard: this.shard }),
46
50
  });
47
51
  }
48
52
  async onEnd() {
@@ -61,6 +65,7 @@ export default class TestReporter {
61
65
  duration,
62
66
  environment: this.environment,
63
67
  browsers: this.browsers,
68
+ ...(this.shard && { shard: this.shard }),
64
69
  tests,
65
70
  status: tests.every((r) => r.status === "success" || r.status === "skipped")
66
71
  ? "success"
@@ -82,6 +87,7 @@ export default class TestReporter {
82
87
  name: test.name,
83
88
  file: test.file,
84
89
  browser,
90
+ ...groupOf(test),
85
91
  status: "started",
86
92
  startTime: t.startTime,
87
93
  });
@@ -90,6 +96,7 @@ export default class TestReporter {
90
96
  name: test.name,
91
97
  file: test.file,
92
98
  browser,
99
+ ...groupOf(test),
93
100
  ...(retry ? { retry } : {}),
94
101
  });
95
102
  }
@@ -111,6 +118,7 @@ export default class TestReporter {
111
118
  name: test.name,
112
119
  file: test.file,
113
120
  browser,
121
+ ...groupOf(test),
114
122
  result,
115
123
  });
116
124
  }
@@ -131,6 +139,7 @@ export default class TestReporter {
131
139
  name: test.name,
132
140
  file: test.file,
133
141
  browser,
142
+ ...groupOf(test),
134
143
  result: ended,
135
144
  });
136
145
  }
@@ -151,6 +160,7 @@ export default class TestReporter {
151
160
  ...(extras?.attachments && { attachments: extras.attachments }),
152
161
  ...(extras?.annotations && { annotations: extras.annotations }),
153
162
  ...(extras?.steps && { steps: extras.steps }),
163
+ ...(extras?.fixtures?.length && { fixtures: extras.fixtures }),
154
164
  ...(extras?.retry && { retry: extras.retry }),
155
165
  ...(extras?.flaky && { flaky: true }),
156
166
  ...(extras?.previousAttempts?.length && {
@@ -163,6 +173,7 @@ export default class TestReporter {
163
173
  name: test.name,
164
174
  file: test.file,
165
175
  browser,
176
+ ...groupOf(test),
166
177
  result,
167
178
  });
168
179
  }
@@ -0,0 +1,14 @@
1
+ import type { APIRequestContext } from "../request/api-request-context.js";
2
+ import type { UseOptions } from "../types/use.js";
3
+ /**
4
+ * The `request` fixture of one test: an `APIRequestContext` of its own (its own cookies), made when the test first
5
+ * reads it, from the run's `baseURL` and the `use` options, and disposed when the test is over
6
+ */
7
+ export declare class RequestFixture {
8
+ private readonly baseURL;
9
+ private readonly use;
10
+ private context;
11
+ constructor(baseURL: string | undefined, use: UseOptions);
12
+ get(): APIRequestContext;
13
+ dispose(): Promise<void>;
14
+ }
@@ -0,0 +1,25 @@
1
+ import { createRequestContext } from "../request/request.js";
2
+ /**
3
+ * The `request` fixture of one test: an `APIRequestContext` of its own (its own cookies), made when the test first
4
+ * reads it, from the run's `baseURL` and the `use` options, and disposed when the test is over
5
+ */
6
+ export class RequestFixture {
7
+ constructor(baseURL, use) {
8
+ this.baseURL = baseURL;
9
+ this.use = use;
10
+ }
11
+ get() {
12
+ const { use } = this;
13
+ return (this.context ??= createRequestContext({
14
+ baseURL: this.baseURL,
15
+ extraHTTPHeaders: use.extraHTTPHeaders,
16
+ ignoreHTTPSErrors: use.acceptInsecureCerts,
17
+ ...(use.storageState !== undefined && {
18
+ storageState: use.storageState,
19
+ }),
20
+ }));
21
+ }
22
+ async dispose() {
23
+ await this.context?.dispose();
24
+ }
25
+ }
@@ -0,0 +1,16 @@
1
+ import type { RunSettings } from "../config/run-settings.js";
2
+ import type { SamuraiTestConfig } from "../types/config.js";
3
+ /**
4
+ * Runs `work` between the config's run-level steps: web servers start, then `globalSetup`, then `work`, then
5
+ * `globalTeardown`, then the servers stop. The teardown runs when the work or the setup throws (not when a server
6
+ * failed to start, since nothing was set up), and the servers are stopped whatever happens. When the config has no
7
+ * `baseURL` for the run, the first server's address becomes `settings.baseURL`.
8
+ * Errors: a failed start is `webServer(): ...`, a failed setup `globalSetup(): ...`; the work's own error wins over
9
+ * a teardown error, which is logged then.
10
+ */
11
+ export declare function withRunHooks<T>(run: {
12
+ config: SamuraiTestConfig;
13
+ settings: RunSettings;
14
+ projectDir: string;
15
+ signal?: AbortSignal | undefined;
16
+ }, work: () => Promise<T>): Promise<T>;
@@ -0,0 +1,52 @@
1
+ import { resolveConfig } from "../config/resolved-config.js";
2
+ import { webServersOf } from "../config/web-server-options.js";
3
+ import logger from "../logger/index.js";
4
+ import { globalHooks } from "./global-hooks.js";
5
+ import { startWebServers } from "./web-server.js";
6
+ /**
7
+ * Runs `work` between the config's run-level steps: web servers start, then `globalSetup`, then `work`, then
8
+ * `globalTeardown`, then the servers stop. The teardown runs when the work or the setup throws (not when a server
9
+ * failed to start, since nothing was set up), and the servers are stopped whatever happens. When the config has no
10
+ * `baseURL` for the run, the first server's address becomes `settings.baseURL`.
11
+ * Errors: a failed start is `webServer(): ...`, a failed setup `globalSetup(): ...`; the work's own error wins over
12
+ * a teardown error, which is logged then.
13
+ */
14
+ export async function withRunHooks(run, work) {
15
+ const { config, settings, projectDir, signal } = run;
16
+ const servers = webServersOf(config);
17
+ if (servers.length === 0 &&
18
+ config.globalSetup === undefined &&
19
+ config.globalTeardown === undefined)
20
+ return work();
21
+ const running = await startWebServers(servers, { projectDir, signal });
22
+ let outcome;
23
+ try {
24
+ if (settings.baseURL === undefined && running.baseURL !== undefined)
25
+ settings.baseURL = running.baseURL;
26
+ const hooks = globalHooks(config, {
27
+ ...resolveConfig(config, { projectDir, env: process.env }),
28
+ environment: settings.environment,
29
+ ...(settings.baseURL !== undefined && { baseURL: settings.baseURL }),
30
+ }, projectDir);
31
+ try {
32
+ await hooks.setup();
33
+ outcome = { value: await work() };
34
+ }
35
+ catch (error) {
36
+ outcome = { error };
37
+ }
38
+ const teardownError = await hooks.teardown();
39
+ if (teardownError) {
40
+ if ("error" in outcome)
41
+ logger.error("%s", teardownError.message);
42
+ else
43
+ outcome = { error: teardownError };
44
+ }
45
+ }
46
+ finally {
47
+ await running.stop();
48
+ }
49
+ if (outcome && "error" in outcome)
50
+ throw outcome.error;
51
+ return outcome.value;
52
+ }
@@ -4,6 +4,7 @@ import type { RunnerOptions } from "./options.js";
4
4
  import type { AllHooks } from "./all-hooks.js";
5
5
  import type { PreparedRun } from "./prepare-run.js";
6
6
  import type { TestSink } from "./reporter.js";
7
+ import type { WorkerFixtures } from "./fixture-worker.js";
7
8
  /** What a worker gives its tests that the main process does not: its own port and profile, and its number */
8
9
  export interface WorkerResources {
9
10
  /** The browser's debugging port. @default `options.port`, else DEFAULT_PORT */
@@ -16,6 +17,8 @@ export interface WorkerResources {
16
17
  attempt?: number;
17
18
  /** The `beforeAll` / `afterAll` hooks of this worker: kept across tests, closed when the worker is done */
18
19
  allHooks?: AllHooks;
20
+ /** The worker-scoped fixtures of this worker: kept across tests, closed when the worker is done */
21
+ workerFixtures?: WorkerFixtures;
19
22
  }
20
23
  /** Runs one test in one browser and reports it: launch, run the body, judge the page's logs, close the browser */
21
24
  export declare function runOneTest(test: RegisteredTestCase, browserName: SupportedBrowser, run: PreparedRun, options: RunnerOptions, srcDir: string, reporter: TestSink, resources?: WorkerResources): Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { takePendingAssertions } from "../assert/expect.js";
3
3
  import { Browser } from "../browser/browser.js";
4
- import { Evidence } from "../browser/evidence.js";
4
+ import { cleanName, Evidence } from "../browser/evidence.js";
5
5
  import { projectDir, readConfig } from "../config/config.js";
6
6
  import logger from "../logger/index.js";
7
7
  import { ConnectionClosedError } from "../transport/bidi-connection.js";
@@ -18,7 +18,9 @@ import { TestTimeout } from "./test-timeout.js";
18
18
  import { ioOfAttempt, missingOutputs } from "./test-io.js";
19
19
  import { buildTestFixtures } from "./fixtures.js";
20
20
  import { FixtureScope } from "./fixture-scope.js";
21
+ import { RequestFixture } from "./request-fixture.js";
21
22
  import { effectiveUse } from "./use-options.js";
23
+ import { effectiveFixtureUse } from "./fixture-use.js";
22
24
  import { beginOutputs, endOutputs } from "../wait/output-sink.js";
23
25
  /**
24
26
  * The state of one test run. A test ends once, whichever of finishing, failing, timing out, an abort or a
@@ -37,6 +39,8 @@ class TestOutcome {
37
39
  this.browserClosed = false;
38
40
  /** The test's inputs and outputs, as its report entry lists them when it ends */
39
41
  this.io = () => ({});
42
+ /** The fixtures the test set up, as they stand when it ends */
43
+ this.fixtures = () => [];
40
44
  }
41
45
  get isEnded() {
42
46
  return this.ended;
@@ -64,6 +68,7 @@ class TestOutcome {
64
68
  screenshots: this.evidence?.screenshots,
65
69
  ...this.info?.reportExtras(),
66
70
  ...this.io(),
71
+ fixtures: this.fixtures(),
67
72
  });
68
73
  }
69
74
  }
@@ -84,6 +89,7 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
84
89
  let browser;
85
90
  let page;
86
91
  let customFixtures;
92
+ let requestFixture;
87
93
  let onRunAborted;
88
94
  const timeoutController = new AbortController();
89
95
  let browserGone = () => { };
@@ -117,6 +123,9 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
117
123
  // The `beforeAll` hooks come first, before a browser exists
118
124
  await resources.allHooks?.ensure(test, browserName);
119
125
  const use = await effectiveUse(test);
126
+ const fixtureUse = test.extension
127
+ ? await effectiveFixtureUse(test)
128
+ : undefined;
120
129
  const launched = await Browser.launch(browserName, {
121
130
  port: resources.port ?? options.port ?? DEFAULT_PORT,
122
131
  ...(resources.profileDir && { profileDir: resources.profileDir }),
@@ -142,7 +151,7 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
142
151
  const applied = Object.keys(use).length > 0
143
152
  ? await launched.browser.setUse(use, projectDir())
144
153
  : undefined;
145
- const testFolder = `${browserName}/${testDirOf(test, srcDir)}${resources.attempt ? `/retry-${resources.attempt}` : ""}`;
154
+ const testFolder = `${browserName}${test.group === undefined ? "" : `/${cleanName(test.group)}`}/${testDirOf(test, srcDir)}${resources.attempt ? `/retry-${resources.attempt}` : ""}`;
146
155
  outcome.evidence = new Evidence(resultDir, testFolder);
147
156
  launched.page.setEvidence(outcome.evidence);
148
157
  const info = new TestInfoImpl({
@@ -157,7 +166,9 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
157
166
  });
158
167
  outcome.info = info;
159
168
  applyDeclaredMarks(info, test);
169
+ requestFixture = new RequestFixture(settings.baseURL, use);
160
170
  const built = buildTestFixtures({
171
+ request: requestFixture,
161
172
  info,
162
173
  launched,
163
174
  browserName,
@@ -173,9 +184,17 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
173
184
  });
174
185
  // A test written with `test.extend()` gets its custom fixtures set up on demand, and torn down below
175
186
  const custom = test.extension
176
- ? new FixtureScope(test.extension, built)
187
+ ? new FixtureScope(test.extension, built, {
188
+ ...fixtureUse,
189
+ environment: settings.environment,
190
+ ...(resources.workerFixtures && {
191
+ workers: resources.workerFixtures,
192
+ }),
193
+ })
177
194
  : undefined;
178
195
  customFixtures = custom;
196
+ if (custom)
197
+ outcome.fixtures = () => custom.report();
179
198
  const fixtures = custom?.fixtures ?? built;
180
199
  let failure;
181
200
  let failed = false;
@@ -246,6 +265,7 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
246
265
  const late = await customFixtures?.teardown(timeout);
247
266
  if (late)
248
267
  logger.debug("A fixture failed after the test ended", { late });
268
+ await requestFixture?.dispose();
249
269
  endOutputs();
250
270
  }
251
271
  testTimeout?.stop();
@@ -1,6 +1,7 @@
1
1
  import type { Modifier, TestSummary } from "../types/test.js";
2
2
  import { type ProjectOptions } from "./project.js";
3
3
  import { type ResolvedGroup } from "./groups.js";
4
+ import { type FixtureCatalogEntry } from "./fixture-registry.js";
4
5
  import type { RunnerOptions } from "./options.js";
5
6
  export interface RunTestsOptions extends RunnerOptions, ProjectOptions {
6
7
  }
@@ -45,9 +46,16 @@ export interface FoundTest {
45
46
  export declare function listTests(options?: RunTestsOptions): Promise<FoundTest[]>;
46
47
  /** Imports the project's specs without running anything and lists the config's groups with the tests each holds */
47
48
  export declare function listGroups(options?: RunTestsOptions): Promise<ResolvedGroup[]>;
49
+ /**
50
+ * Lists the fixtures of the extended tests in the config's `fixtures`, with their label, icon, scope, option form
51
+ * and declared outputs, without running a test. Throws for a name defined twice or one that is not allowed.
52
+ */
53
+ export declare function listFixtures(options?: ProjectOptions): Promise<FixtureCatalogEntry[]>;
54
+ export type { FixtureCatalogEntry } from "./fixture-registry.js";
48
55
  export { BROWSER_NAMES, checkBrowserList } from "../config/browsers.js";
49
56
  export { parseWorkers } from "../config/workers.js";
50
57
  export { parseRetries } from "../config/retries.js";
58
+ export { parseShard, type Shard } from "./shard.js";
51
59
  export type { RunEvent } from "./reporter.js";
52
60
  export type { Modifier, TestError, TestResult, TestSummary, } from "../types/test.js";
53
61
  export type { ResolvedGroup } from "./groups.js";
@@ -1,6 +1,8 @@
1
1
  import path from "path";
2
2
  import { withProject } from "./project.js";
3
3
  import { testId, toPosix } from "./groups.js";
4
+ import { projectConfig } from "../config/config.js";
5
+ import { fixtureCatalog, } from "./fixture-registry.js";
4
6
  import TestRunner from "./test-runner.js";
5
7
  /**
6
8
  * Sets the project up (config, environment, secrets), builds a runner from the rest of the options and hands both to
@@ -67,7 +69,15 @@ export async function listTests(options = {}) {
67
69
  export async function listGroups(options = {}) {
68
70
  return inProject(options, (runner) => runner.groups());
69
71
  }
72
+ /**
73
+ * Lists the fixtures of the extended tests in the config's `fixtures`, with their label, icon, scope, option form
74
+ * and declared outputs, without running a test. Throws for a name defined twice or one that is not allowed.
75
+ */
76
+ export async function listFixtures(options = {}) {
77
+ return fixtureCatalog((await projectConfig(options)).fixtures ?? []);
78
+ }
70
79
  export { BROWSER_NAMES, checkBrowserList } from "../config/browsers.js";
71
80
  export { parseWorkers } from "../config/workers.js";
72
81
  export { parseRetries } from "../config/retries.js";
82
+ export { parseShard } from "./shard.js";
73
83
  export { DEFAULT_PORT } from "./ports.js";
@@ -15,10 +15,21 @@ export declare function isSelected(name: string, { grep, testNames }: Pick<Runne
15
15
  export declare function idOf(test: RegisteredTestCase, srcDir: string): string;
16
16
  /** Imports the spec files once and resolves the config's groups against what they registered */
17
17
  export declare function loadSpecs(testFiles: string[]): Promise<LoadedSpecs>;
18
- /** The tests the options select: by `group`, `testNames` and `grep` */
18
+ /** The group names the options select, once each and in the order given; none when no group is selected */
19
+ export declare function groupNamesOf(group: RunnerOptions["group"]): string[];
20
+ /** The tests the options select: by `group` (in any of them), `testNames` and `grep`; each test once */
19
21
  export declare function selectTests({ all, groups, srcDir }: LoadedSpecs, options: RunnerOptions): RegisteredTestCase[];
20
22
  /**
21
- * The browsers the run uses: the options' (`--browser`), else the selected group's, else the project's, else
22
- * Firefox
23
+ * The runs of the selected tests: without a selected group, the tests as they are; else one copy per selected group
24
+ * that holds the test (its `group` set), the first group's tests first
23
25
  */
24
- export declare function browsersOfRun(groups: ResolvedGroup[], options: RunnerOptions): Promise<SupportedBrowser[]>;
26
+ export declare function expandByGroup(selected: RegisteredTestCase[], { groups, srcDir }: LoadedSpecs, options: RunnerOptions): RegisteredTestCase[];
27
+ /**
28
+ * The browsers a run of a test uses: the options' (`--browser`), else its group's, else the project's, else Firefox
29
+ */
30
+ export declare function browserPlan(groups: ResolvedGroup[], options: RunnerOptions): Promise<{
31
+ /** Every browser of the run, in the order they run */
32
+ browsers: SupportedBrowser[];
33
+ /** The browsers one run of a test uses */
34
+ browsersOf(test: RegisteredTestCase): SupportedBrowser[];
35
+ }>;
@@ -53,25 +53,51 @@ export async function loadSpecs(testFiles) {
53
53
  const all = await registerAll(testFiles);
54
54
  return { all, ...(await resolveAll(all)) };
55
55
  }
56
- /** The tests the options select: by `group`, `testNames` and `grep` */
56
+ /** The group names the options select, once each and in the order given; none when no group is selected */
57
+ export function groupNamesOf(group) {
58
+ return [...new Set(group === undefined ? [] : [group].flat())];
59
+ }
60
+ /** The tests the options select: by `group` (in any of them), `testNames` and `grep`; each test once */
57
61
  export function selectTests({ all, groups, srcDir }, options) {
58
- const { group } = options;
62
+ const names = groupNamesOf(options.group);
59
63
  let inGroup = () => true;
60
- if (group !== undefined) {
61
- const members = new Set(findGroup(groups, group).testIds);
64
+ if (names.length > 0) {
65
+ const members = new Set(names.flatMap((name) => findGroup(groups, name).testIds));
62
66
  inGroup = (test) => members.has(idOf(test, srcDir));
63
67
  }
64
68
  return all.filter((test) => inGroup(test) && isSelected(test.name, options));
65
69
  }
66
70
  /**
67
- * The browsers the run uses: the options' (`--browser`), else the selected group's, else the project's, else
68
- * Firefox
71
+ * The runs of the selected tests: without a selected group, the tests as they are; else one copy per selected group
72
+ * that holds the test (its `group` set), the first group's tests first
73
+ */
74
+ export function expandByGroup(selected, { groups, srcDir }, options) {
75
+ const names = groupNamesOf(options.group);
76
+ if (names.length === 0)
77
+ return selected;
78
+ return names.flatMap((name) => {
79
+ const members = new Set(findGroup(groups, name).testIds);
80
+ return selected
81
+ .filter((test) => members.has(idOf(test, srcDir)))
82
+ .map((test) => ({ ...test, group: name }));
83
+ });
84
+ }
85
+ /**
86
+ * The browsers a run of a test uses: the options' (`--browser`), else its group's, else the project's, else Firefox
69
87
  */
70
- export async function browsersOfRun(groups, options) {
71
- const { browsers: flag, group } = options;
88
+ export async function browserPlan(groups, options) {
89
+ const { browsers: flag } = options;
72
90
  const config = await loadRawConfig();
73
- const ofGroup = group !== undefined && !(flag && flag.length > 0)
74
- ? findGroup(groups, group).browsers
75
- : undefined;
76
- return browsersFor({ flag, group: ofGroup, project: config.browsers });
91
+ const names = groupNamesOf(options.group);
92
+ const flagged = flag && flag.length > 0;
93
+ const ofGroup = new Map(names.map((name) => [name, findGroup(groups, name).browsers]));
94
+ const project = browsersFor({ flag, project: config.browsers });
95
+ const forGroup = (name) => flagged ? project : (ofGroup.get(name) ?? project);
96
+ const browsers = [
97
+ ...new Set(names.length > 0 ? names.flatMap(forGroup) : project),
98
+ ];
99
+ return {
100
+ browsers,
101
+ browsersOf: (test) => test.group === undefined ? project : forGroup(test.group),
102
+ };
77
103
  }
@@ -0,0 +1,23 @@
1
+ import type { SupportedBrowser } from "../types/browser.js";
2
+ import type { RegisteredTestCase } from "../types/test.js";
3
+ import type { TestSink } from "./reporter.js";
4
+ /** One attempt at one test of the block, reporting to `sink` */
5
+ export type BlockTest = (test: RegisteredTestCase, sink: TestSink, attempt: number) => Promise<void>;
6
+ export interface SerialBlockRun {
7
+ tests: RegisteredTestCase[];
8
+ browser: SupportedBrowser;
9
+ retries: number;
10
+ signal: AbortSignal | undefined;
11
+ runTest: BlockTest;
12
+ /** Called before the block starts again after a failure: closes what the block's tests share (`afterAll`) */
13
+ restart(): Promise<void>;
14
+ sink: TestSink;
15
+ }
16
+ /**
17
+ * Runs the tests of a serial block in order. After a failure the rest of the block is reported as skipped; with
18
+ * `retries`, the whole block then runs again from its first test (after `restart`), up to `retries` times.
19
+ * Without retries every test is reported as it ends. With them the ends are held until the block's last attempt is
20
+ * known, as `runWithRetries` does for one test: each test ends once, with `retry` saying which attempt that was.
21
+ * An aborted run is never run again.
22
+ */
23
+ export declare function serialBlockRun(run: SerialBlockRun): Promise<void>;