@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
@@ -1,3 +1,5 @@
1
+ import { failedResult, serialSkipped, UnitState } from "./pool-units.js";
2
+ import { eligibleCount, nextEligible } from "./pool-schedule.js";
1
3
  /** How long a busy worker may take to wind down after an abort before it is killed */
2
4
  const GRACE_MS = 2000;
3
5
  const SHUTDOWN_MS = 60000;
@@ -15,6 +17,7 @@ export class WorkerPool {
15
17
  this.slots = new Map();
16
18
  this.calls = new Set();
17
19
  this.timers = new Set();
20
+ this.units = new Map();
18
21
  this.nextId = 0;
19
22
  /** Workers in a row that exited before they were ready */
20
23
  this.startupFailures = 0;
@@ -95,51 +98,23 @@ export class WorkerPool {
95
98
  if (this.disposed)
96
99
  return;
97
100
  for (const slot of [...this.slots.values()]) {
98
- if (!slot.ready || slot.busy)
101
+ if (!slot.ready || slot.busy || slot.retiring)
99
102
  continue;
100
- const at = this.nextEligible();
103
+ const at = nextEligible(this.queue, [...this.slots.values()], slot);
101
104
  if (at < 0)
102
- break;
105
+ continue;
103
106
  const [entry] = this.queue.splice(at, 1);
104
107
  if (entry)
105
108
  this.dispatch(slot, entry);
106
109
  }
107
110
  let starting = [...this.slots.values()].filter((s) => !s.ready).length;
108
- const wanted = this.eligibleCount();
111
+ const wanted = eligibleCount(this.queue, [...this.slots.values()]);
109
112
  while (this.slots.size < this.options.workers && starting < wanted) {
110
113
  if (!this.start(this.freeIndex()))
111
114
  return;
112
115
  starting++;
113
116
  }
114
117
  }
115
- busyKeys() {
116
- const keys = new Set();
117
- for (const { busy } of this.slots.values()) {
118
- const key = busy?.entry.item.serialKey;
119
- if (key)
120
- keys.add(key);
121
- }
122
- return keys;
123
- }
124
- /** The queue position of the first item whose serial key is not running */
125
- nextEligible() {
126
- const busy = this.busyKeys();
127
- return this.queue.findIndex(({ item }) => !item.serialKey || !busy.has(item.serialKey));
128
- }
129
- /** How many queued items could run at the same time right now */
130
- eligibleCount() {
131
- const taken = this.busyKeys();
132
- let count = 0;
133
- for (const { item } of this.queue) {
134
- if (item.serialKey) {
135
- if (taken.has(item.serialKey))
136
- continue;
137
- taken.add(item.serialKey);
138
- }
139
- count++;
140
- }
141
- return count;
142
- }
143
118
  freeIndex() {
144
119
  let index = 0;
145
120
  while (this.slots.has(index))
@@ -165,6 +140,13 @@ export class WorkerPool {
165
140
  }
166
141
  dispatch(slot, entry) {
167
142
  const id = this.nextId++;
143
+ const { unit, serialKey } = entry.item;
144
+ if (unit) {
145
+ slot.unit = unit;
146
+ slot.unitKey = serialKey;
147
+ if (!this.units.has(unit))
148
+ this.units.set(unit, new UnitState(entry.call));
149
+ }
168
150
  slot.busy = { id, entry, startTime: Date.now() };
169
151
  slot.handle.send({ type: "run", id, item: entry.item });
170
152
  }
@@ -187,7 +169,7 @@ export class WorkerPool {
187
169
  type: "test-start",
188
170
  item: busy.entry.item,
189
171
  worker: slot.index,
190
- ...(message.retry && { retry: message.retry }),
172
+ ...this.retryOf(busy.entry, message.retry),
191
173
  });
192
174
  case "test-end":
193
175
  if (busy?.id !== message.id)
@@ -209,23 +191,84 @@ export class WorkerPool {
209
191
  const message = call.aborted
210
192
  ? "Run aborted"
211
193
  : `The worker process exited (code ${code})`;
212
- this.finish(slot, busy.entry, failure(item, message, busy.startTime));
194
+ this.finish(slot, busy.entry, failedResult(item, message, busy.startTime));
213
195
  }
214
196
  else {
215
197
  this.pump();
216
198
  }
217
199
  }
200
+ /** The attempt a started item is on: the worker's own retry, or the attempt at its serial block */
201
+ retryOf({ item }, workerRetry) {
202
+ const retry = workerRetry || (item.serial ? this.units.get(item.unit)?.attempt : 0);
203
+ return retry ? { retry } : {};
204
+ }
218
205
  /** Records an item's result, tells the listener, and gives the worker its next item */
219
206
  finish(slot, entry, result) {
220
207
  const stamped = { ...result, worker: slot.index };
208
+ if (entry.item.serial)
209
+ this.finishInBlock(slot, entry, stamped);
210
+ else
211
+ this.report(entry, slot.index, stamped);
212
+ this.pump();
213
+ }
214
+ report(entry, worker, result) {
221
215
  this.emit(entry.call, {
222
216
  type: "test-end",
223
217
  item: entry.item,
224
- worker: slot.index,
225
- result: stamped,
218
+ worker,
219
+ result,
226
220
  });
227
- this.settle(entry, stamped);
228
- this.pump();
221
+ this.settle(entry, result);
222
+ }
223
+ /**
224
+ * An item of a serial block ended. After a failure the rest of the block is skipped; with retries the block
225
+ * starts over on a new worker first, and what its tests ended with is held until the last attempt is known.
226
+ */
227
+ finishInBlock(slot, entry, result) {
228
+ const state = this.units.get(entry.item.unit);
229
+ const retries = this.options.init.retries ?? 0;
230
+ if (retries === 0)
231
+ this.report(entry, slot.index, result);
232
+ else
233
+ state.hold(entry, result);
234
+ if (result.status !== "failed") {
235
+ const last = !this.queue.some((e) => e.item.unit === entry.item.unit);
236
+ if (last)
237
+ this.release(state);
238
+ return;
239
+ }
240
+ const aborted = entry.call.aborted || result.message === "Run aborted";
241
+ if (state.attempt < retries && !aborted && !this.disposed) {
242
+ return this.restartBlock(slot, state, entry, result);
243
+ }
244
+ this.release(state);
245
+ for (let i = 0; i < this.queue.length; i++) {
246
+ const next = this.queue[i];
247
+ if (next.item.unit !== entry.item.unit)
248
+ continue;
249
+ this.queue.splice(i--, 1);
250
+ this.report(next, slot.index, serialSkipped(next.item));
251
+ }
252
+ }
253
+ /** Reports what a block's tests ended with in its last attempt */
254
+ release(state) {
255
+ for (const { entry, result } of state.release())
256
+ this.report(entry, result.worker ?? 0, result);
257
+ }
258
+ /** Puts the block back in the queue, from its first test, and tells the worker that ran it to leave */
259
+ restartBlock(slot, state, failed, result) {
260
+ this.queue.unshift(...state.restart(failed, result));
261
+ slot.unit = slot.unitKey = undefined;
262
+ if (this.slots.get(slot.index) !== slot)
263
+ return;
264
+ slot.retiring = true;
265
+ slot.handle.send({ type: "exit" });
266
+ const timer = setTimeout(() => {
267
+ this.timers.delete(timer);
268
+ slot.handle.kill();
269
+ }, this.options.shutdownMs ?? SHUTDOWN_MS);
270
+ timer.unref();
271
+ this.timers.add(timer);
229
272
  }
230
273
  settle({ call, at }, result) {
231
274
  call.results[at] = result;
@@ -246,12 +289,15 @@ export class WorkerPool {
246
289
  /** Fails the queued items of a call, asks its busy workers to stop, and kills the ones that do not */
247
290
  abort(call) {
248
291
  call.aborted = true;
292
+ for (const state of this.units.values())
293
+ if (state.call === call)
294
+ this.release(state);
249
295
  for (let i = this.queue.length - 1; i >= 0; i--) {
250
296
  const entry = this.queue[i];
251
297
  if (entry?.call !== call)
252
298
  continue;
253
299
  this.queue.splice(i, 1);
254
- this.settle(entry, failure(entry.item, "Run aborted"));
300
+ this.settle(entry, failedResult(entry.item, "Run aborted"));
255
301
  }
256
302
  for (const slot of this.slots.values()) {
257
303
  const busy = slot.busy;
@@ -294,15 +340,3 @@ export class WorkerPool {
294
340
  }
295
341
  }
296
342
  }
297
- function failure(item, message, startTime = Date.now()) {
298
- return {
299
- name: item.testName,
300
- file: item.file,
301
- browser: item.browser,
302
- status: "failed",
303
- type: "error",
304
- message,
305
- startTime,
306
- duration: Date.now() - startTime,
307
- };
308
- }
@@ -9,8 +9,14 @@ export interface WorkItem {
9
9
  /** The test's full name */
10
10
  testName: string;
11
11
  browser: SupportedBrowser;
12
+ /** The group this run of the test is for (`--group`): its `use` applies and the result names it */
13
+ group?: string;
12
14
  /** Items with the same key never run at the same time */
13
15
  serialKey?: string;
16
+ /** Items with the same unit run one after the other on the same worker, in the order they are queued */
17
+ unit?: string;
18
+ /** The unit is a serial block: a failure skips the rest, and `retries` run the whole block again (the pool does it, not the worker) */
19
+ serial?: boolean;
14
20
  /** Where this test's screenshots go, instead of the run's `outputDir` (a flow gives each node its own) */
15
21
  outputDir?: string;
16
22
  /** The values the run supplies for the inputs tests declare (`runTests({ inputs })`) */
@@ -21,6 +27,8 @@ export interface WorkerInit {
21
27
  projectDir: string;
22
28
  dataDir?: string;
23
29
  environment?: string;
30
+ /** The run's base URL when no config sets one (it came from a web server): used where the config has none */
31
+ baseURL?: string;
24
32
  timeout?: number;
25
33
  expectTimeout?: number;
26
34
  headless?: boolean;
@@ -3,7 +3,8 @@ import logger from "../logger/index.js";
3
3
  import { withProject } from "./project.js";
4
4
  import TestReporter from "./reporter.js";
5
5
  import { AllHooks } from "./all-hooks.js";
6
- import { allHookFixtures, runOneTest } from "./run-test.js";
6
+ import { allHookFixtures } from "./fixtures.js";
7
+ import { runOneTest } from "./run-test.js";
7
8
  import { runWithRetries } from "./retry.js";
8
9
  import { loadSpecs } from "./select.js";
9
10
  /**
@@ -28,6 +29,8 @@ export function openSession(init) {
28
29
  }),
29
30
  ...(headless !== undefined && { headless }),
30
31
  }, async (run) => {
32
+ if (run.settings.baseURL === undefined && init.baseURL !== undefined)
33
+ run.settings.baseURL = init.baseURL;
31
34
  const loaded = await loadSpecs(init.files);
32
35
  // The `beforeAll` hooks this worker ran: their `afterAll` hooks run when it closes
33
36
  const allHooks = new AllHooks({
@@ -55,7 +58,7 @@ export function openSession(init) {
55
58
  port,
56
59
  signal,
57
60
  };
58
- await runWithRetries(test, browser, init.retries ?? 0, signal, (sink, attempt) => runOneTest(test, browser, run, options, loaded.srcDir, sink, {
61
+ await runWithRetries(test, browser, perTest.retries ?? init.retries ?? 0, signal, (sink, attempt) => runOneTest(test, browser, run, options, loaded.srcDir, sink, {
59
62
  port,
60
63
  profileDir: workerProfileDir(browser, workerIndex),
61
64
  workerIndex,
@@ -9,6 +9,8 @@ export interface WorkerSession {
9
9
  runTest(test: RegisteredTestCase, browser: SupportedBrowser, signal: AbortSignal, onEvent: (event: RunEvent) => void, options?: {
10
10
  outputDir?: string;
11
11
  inputs?: Record<string, string>;
12
+ /** Instead of the run's `retries` */
13
+ retries?: number;
12
14
  }): Promise<void>;
13
15
  /** Ends the project's session */
14
16
  close(): Promise<void>;
@@ -3,6 +3,7 @@ const failedResult = (item, worker, message) => ({
3
3
  name: item.testName,
4
4
  file: item.file,
5
5
  browser: item.browser,
6
+ ...(item.group !== undefined && { group: item.group }),
6
7
  worker,
7
8
  status: "failed",
8
9
  startTime: performance.now(),
@@ -29,7 +30,10 @@ export function handleMessages(channel, { open, exit }) {
29
30
  if (!session)
30
31
  throw new Error("The worker was sent a test before it was initialised");
31
32
  channel.send({ type: "test-start", id });
32
- const test = session.tests.find(({ file, name }) => file === item.file && name === item.testName);
33
+ const found = session.tests.find(({ file, name }) => file === item.file && name === item.testName);
34
+ const test = found && item.group !== undefined
35
+ ? { ...found, group: item.group }
36
+ : found;
33
37
  if (!test) {
34
38
  const message = `Test "${item.testName}" was not found in ${item.file}`;
35
39
  channel.send({
@@ -57,6 +61,8 @@ export function handleMessages(channel, { open, exit }) {
57
61
  }, {
58
62
  ...(item.outputDir !== undefined && { outputDir: item.outputDir }),
59
63
  ...(item.inputs !== undefined && { inputs: item.inputs }),
64
+ // The pool runs a whole serial block again, not the test
65
+ ...(item.serial && { retries: 0 }),
60
66
  });
61
67
  }
62
68
  finally {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A reference to a DOM node in the browser. The `sharedId` stays valid across scripts in the
3
3
  * same document; after navigation or removal the browser answers `no such node`.
4
- * Internal: user code keeps using `Locator`.
4
+ * What `evaluate` returns for a DOM node; user code keeps using `Locator` to act on elements.
5
5
  */
6
6
  export declare class ElementHandle {
7
7
  readonly sharedId: string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A reference to a DOM node in the browser. The `sharedId` stays valid across scripts in the
3
3
  * same document; after navigation or removal the browser answers `no such node`.
4
- * Internal: user code keeps using `Locator`.
4
+ * What `evaluate` returns for a DOM node; user code keeps using `Locator` to act on elements.
5
5
  */
6
6
  export class ElementHandle {
7
7
  constructor(sharedId) {
@@ -1,5 +1,7 @@
1
1
  import type { StepDefinition } from "../flows/steps/define.js";
2
2
  import type { SupportedBrowser } from "./browser.js";
3
+ import type { UseOptions } from "./use.js";
4
+ import type { ResolvedConfig } from "../config/resolved-config.js";
3
5
 
4
6
  interface SamuraiTest {
5
7
  /** Path relative to srcDir, e.g. 'checkout/coupons.spec.ts' */
@@ -32,8 +34,67 @@ export interface SamuraiGroup {
32
34
  * nothing but such a group uses one worker.
33
35
  */
34
36
  workers?: number;
37
+ /**
38
+ * Options for this group's tests, over the config's `use` and under the `test.use()` of a spec: the same options
39
+ * as config `use` that make sense per group (`locale`, `isMobile`, `viewport`, ...). They apply when the group is
40
+ * selected (`--group`); a run without `--group` ignores them. The way to port a Playwright project.
41
+ */
42
+ use?: GroupUse;
43
+ }
44
+
45
+ /** The options of a group's `use`: those of `test.use()`, and the page's `viewport` */
46
+ export interface GroupUse extends UseOptions {
47
+ /** Size of the page's viewport, instead of config `use.viewport`. `null` keeps the browser's own size */
48
+ viewport?: { width: number; height: number } | null;
49
+ }
50
+
51
+ /** One server the run starts before the first test (see `SamuraiTestConfig.webServer`). Give either `url` or `port`. */
52
+ export interface WebServerConfig {
53
+ /** Shell command that starts the server. It leads its own process group, so the whole tree is stopped at the end */
54
+ command: string;
55
+ /** Where the server answers: ready once it responds with a status from 200 to 403 */
56
+ url?: string;
57
+ /** Where the server listens: ready once `localhost` accepts TCP connections on it */
58
+ port?: number;
59
+ /** Working directory of the command, relative to the project folder. @default the project folder */
60
+ cwd?: string;
61
+ /** Extra environment variables for the command, on top of this process's. Never printed in errors */
62
+ env?: Record<string, string>;
63
+ /** Time (ms) the server may take to be ready. @default 60000 */
64
+ timeout?: number;
65
+ /** When `url`/`port` already answers, use that server and start nothing (it is not stopped either). @default false */
66
+ reuseExistingServer?: boolean;
67
+ /** Send the command's stdout to this process's stdout, or drop it. @default "ignore" */
68
+ stdout?: "pipe" | "ignore";
69
+ /** Send the command's stderr to this process's stderr, or drop it. @default "pipe" */
70
+ stderr?: "pipe" | "ignore";
71
+ /** Do not fail on a certificate error while waiting for an `https` `url`. @default false */
72
+ ignoreHTTPSErrors?: boolean;
73
+ /**
74
+ * How the server is stopped: `signal` goes to the whole process group, and after `timeout` ms what is left is
75
+ * killed. @default { signal: "SIGTERM", timeout: 5000 }
76
+ */
77
+ gracefulShutdown?: { signal?: "SIGTERM" | "SIGINT"; timeout?: number };
35
78
  }
36
79
 
80
+ /** What `globalSetup` and `globalTeardown` receive: the project's resolved config, with the run's environment and base URL */
81
+ export type GlobalHookConfig = ResolvedConfig & {
82
+ /** The environment the run uses */
83
+ environment: string;
84
+ };
85
+
86
+ /** A run-level hook; `globalSetup` may return a function that runs as teardown too */
87
+ export type GlobalSetupFunction = (
88
+ config: GlobalHookConfig,
89
+ ) =>
90
+ | void
91
+ | (() => void | Promise<void>)
92
+ | Promise<void | (() => void | Promise<void>)>;
93
+
94
+ export type GlobalTeardownFunction = (
95
+ config: GlobalHookConfig,
96
+ ) => void | Promise<void>;
97
+
37
98
  export interface SamuraiEnvironment {
38
99
  /** Where page.goto('/…') resolves to in this environment, and where recordings start */
39
100
  baseURL?: string;
@@ -64,6 +125,14 @@ export interface SamuraiTestConfig {
64
125
  * @default half the CPUs
65
126
  */
66
127
  workers?: number | string;
128
+ /**
129
+ * Whether the tests of one file may run on different workers. Not set: they do, one test at a time goes to the
130
+ * next free worker (the same as `true`). `false` keeps the tests of a file together, in order, on one worker;
131
+ * `describe.configure({ mode: 'parallel' })` frees a part of it again. A serial block (`describe.serial`) is
132
+ * never split, whatever this says.
133
+ * @default not set
134
+ */
135
+ fullyParallel?: boolean;
67
136
  /**
68
137
  * How many times a failing test is run again, each time with a fresh browser. A test that passes on a retry is
69
138
  * reported as flaky and does not fail the run; a run that is aborted is never retried. `--retries` and
@@ -129,8 +198,8 @@ export interface SamuraiTestConfig {
129
198
  * @default "files"
130
199
  */
131
200
  filesDir?: string;
132
- /** How pages are opened */
133
- use?: {
201
+ /** How pages are opened, and what the browser pretends: the options of `test.use()` apply to every test */
202
+ use?: UseOptions & {
134
203
  /**
135
204
  * Size of the page's viewport, so screenshots and layout are the same on every machine.
136
205
  * `null` keeps the browser's own size
@@ -209,6 +278,21 @@ export interface SamuraiTestConfig {
209
278
  */
210
279
  maxBodySize?: number;
211
280
  };
281
+ /**
282
+ * Servers the run starts before the first test and stops after the last. Each waits until its `url` answers or
283
+ * its `port` accepts connections. When no `baseURL` is configured (project or environment), the first server's
284
+ * `url` origin (or `http://localhost:<port>`) becomes the base URL. Only a run (`samurai run`, `runTests`)
285
+ * starts them, not `list` or the recorder.
286
+ */
287
+ webServer?: WebServerConfig | WebServerConfig[];
288
+ /**
289
+ * Module (path relative to the project folder) whose default export `async (config) => void | teardown` runs once
290
+ * before the tests, after the web servers are up; or the function itself. Variables it sets in `process.env`
291
+ * reach the tests, workers included. A function it returns runs as teardown.
292
+ */
293
+ globalSetup?: string | GlobalSetupFunction;
294
+ /** Module (path relative to the project folder) whose default export runs once after the tests; or the function itself */
295
+ globalTeardown?: string | GlobalTeardownFunction;
212
296
  /** Keyed by name: 'dev', 'staging', 'production' */
213
297
  environments?: Record<string, SamuraiEnvironment>;
214
298
  /** Used when no environment is chosen with --env or SAMURAI_ENV */
@@ -0,0 +1,30 @@
1
+ import type { TestFixtures } from "./test.js";
2
+
3
+ /** What a test sees when it is built with `test.extend()`: the built-in fixtures, a custom one of the same name replacing it */
4
+ export type FixturesOf<X> = Omit<TestFixtures, keyof X> & X;
5
+
6
+ /** `B` on top of `A`: a key in both takes the type from `B` */
7
+ export type MergeFixtures<A, B> = Omit<A, keyof B> & B;
8
+
9
+ /** The options of a fixture written as `[fixture, options]` */
10
+ export interface FixtureOptions {
11
+ /** Set the fixture up for every test, whether or not the test asks for it */
12
+ auto?: boolean;
13
+ /** Fixtures live as long as one test; worker-scoped fixtures are not supported */
14
+ scope?: "test";
15
+ }
16
+
17
+ /** A value, or a function that sets the value up, calls `use(value)` and cleans up once `use` returns */
18
+ export type FixtureValue<R, Args> =
19
+ | Exclude<R, (...args: never[]) => unknown>
20
+ | ((args: Args, use: (value: R) => Promise<void>) => Promise<void>);
21
+
22
+ /** What `test.extend()` takes: each fixture alone, or as `[fixture, options]` */
23
+ export type FixtureDefinitions<T, Existing> = {
24
+ [K in keyof T]:
25
+ | FixtureValue<T[K], FixturesOf<MergeFixtures<Existing, T>>>
26
+ | [
27
+ FixtureValue<T[K], FixturesOf<MergeFixtures<Existing, T>>>,
28
+ FixtureOptions,
29
+ ];
30
+ };