@itmetsam/samurai-framework 0.21.5 → 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 (141) hide show
  1. package/dist/api.d.ts +6 -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 +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-navigation.d.ts +0 -2
  17. package/dist/browser/page-navigation.js +1 -6
  18. package/dist/browser/page.d.ts +18 -8
  19. package/dist/browser/page.js +18 -9
  20. package/dist/browser/storage-state.d.ts +0 -2
  21. package/dist/browser/storage-state.js +0 -20
  22. package/dist/browser/use-applier.d.ts +16 -5
  23. package/dist/browser/use-applier.js +44 -17
  24. package/dist/browser/use-commands.d.ts +36 -0
  25. package/dist/browser/use-commands.js +120 -0
  26. package/dist/cli/args.d.ts +8 -4
  27. package/dist/cli/args.js +8 -5
  28. package/dist/cli/console-reporter.js +5 -1
  29. package/dist/cli/main.js +1 -0
  30. package/dist/cli/usage.d.ts +1 -1
  31. package/dist/cli/usage.js +4 -1
  32. package/dist/config/base-url.d.ts +4 -0
  33. package/dist/config/base-url.js +6 -0
  34. package/dist/config/config.js +6 -0
  35. package/dist/config/fully-parallel.d.ts +3 -0
  36. package/dist/config/fully-parallel.js +6 -0
  37. package/dist/config/group-use.d.ts +8 -0
  38. package/dist/config/group-use.js +41 -0
  39. package/dist/config/resolved-config.d.ts +2 -0
  40. package/dist/config/resolved-config.js +3 -0
  41. package/dist/config/storage-state-file.d.ts +3 -0
  42. package/dist/config/storage-state-file.js +20 -0
  43. package/dist/config/use-options.d.ts +3 -1
  44. package/dist/config/use-options.js +70 -14
  45. package/dist/config/web-server-options.d.ts +7 -0
  46. package/dist/config/web-server-options.js +111 -0
  47. package/dist/locator/check-actions.js +1 -1
  48. package/dist/locator/drag-actions.d.ts +17 -0
  49. package/dist/locator/drag-actions.js +80 -0
  50. package/dist/locator/element-actions.d.ts +10 -4
  51. package/dist/locator/element-actions.js +23 -16
  52. package/dist/locator/element-state.d.ts +5 -0
  53. package/dist/locator/element-state.js +2 -1
  54. package/dist/locator/event-actions.d.ts +10 -0
  55. package/dist/locator/event-actions.js +81 -0
  56. package/dist/locator/input-actions.d.ts +2 -11
  57. package/dist/locator/input-actions.js +3 -56
  58. package/dist/locator/key-actions.d.ts +11 -0
  59. package/dist/locator/key-actions.js +52 -0
  60. package/dist/locator/key-map.d.ts +16 -0
  61. package/dist/locator/key-map.js +76 -0
  62. package/dist/locator/locator.d.ts +35 -5
  63. package/dist/locator/locator.js +70 -16
  64. package/dist/locator/page-scripts.d.ts +2 -0
  65. package/dist/locator/page-scripts.js +7 -0
  66. package/dist/locator/pointer-actions.d.ts +44 -0
  67. package/dist/locator/pointer-actions.js +117 -0
  68. package/dist/locator/pointer-element-actions.d.ts +21 -0
  69. package/dist/locator/pointer-element-actions.js +42 -0
  70. package/dist/request/api-request-context.d.ts +65 -0
  71. package/dist/request/api-request-context.js +198 -0
  72. package/dist/request/api-response.d.ts +34 -0
  73. package/dist/request/api-response.js +57 -0
  74. package/dist/request/cookie-jar.d.ts +48 -0
  75. package/dist/request/cookie-jar.js +164 -0
  76. package/dist/request/http-transport.d.ts +31 -0
  77. package/dist/request/http-transport.js +113 -0
  78. package/dist/request/redirect.d.ts +10 -0
  79. package/dist/request/redirect.js +21 -0
  80. package/dist/request/request-body.d.ts +27 -0
  81. package/dist/request/request-body.js +71 -0
  82. package/dist/request/request.d.ts +16 -0
  83. package/dist/request/request.js +20 -0
  84. package/dist/runner/all-hooks.d.ts +7 -1
  85. package/dist/runner/all-hooks.js +17 -1
  86. package/dist/runner/fixture-defs.js +1 -0
  87. package/dist/runner/fixtures.d.ts +4 -1
  88. package/dist/runner/fixtures.js +3 -2
  89. package/dist/runner/global-hooks.d.ts +13 -0
  90. package/dist/runner/global-hooks.js +66 -0
  91. package/dist/runner/options.d.ts +13 -2
  92. package/dist/runner/output-tail.d.ts +14 -0
  93. package/dist/runner/output-tail.js +30 -0
  94. package/dist/runner/pool-schedule.d.ts +22 -0
  95. package/dist/runner/pool-schedule.js +55 -0
  96. package/dist/runner/pool-units.d.ts +34 -0
  97. package/dist/runner/pool-units.js +85 -0
  98. package/dist/runner/pooled-run.d.ts +7 -5
  99. package/dist/runner/pooled-run.js +19 -9
  100. package/dist/runner/registry.js +6 -2
  101. package/dist/runner/reporter.d.ts +10 -0
  102. package/dist/runner/reporter.js +12 -2
  103. package/dist/runner/request-fixture.d.ts +14 -0
  104. package/dist/runner/request-fixture.js +25 -0
  105. package/dist/runner/run-hooks.d.ts +16 -0
  106. package/dist/runner/run-hooks.js +52 -0
  107. package/dist/runner/run-test.js +7 -2
  108. package/dist/runner/run.d.ts +1 -0
  109. package/dist/runner/run.js +1 -0
  110. package/dist/runner/select.d.ts +15 -4
  111. package/dist/runner/select.js +38 -12
  112. package/dist/runner/serial-run.d.ts +23 -0
  113. package/dist/runner/serial-run.js +88 -0
  114. package/dist/runner/serial.d.ts +34 -0
  115. package/dist/runner/serial.js +63 -0
  116. package/dist/runner/shard.d.ts +22 -0
  117. package/dist/runner/shard.js +73 -0
  118. package/dist/runner/suite.d.ts +3 -3
  119. package/dist/runner/suite.js +4 -3
  120. package/dist/runner/test-function.d.ts +13 -1
  121. package/dist/runner/test-info.js +2 -1
  122. package/dist/runner/test-runner.d.ts +1 -0
  123. package/dist/runner/test-runner.js +62 -18
  124. package/dist/runner/use-options.d.ts +4 -4
  125. package/dist/runner/use-options.js +10 -3
  126. package/dist/runner/wait-for-url.d.ts +20 -0
  127. package/dist/runner/wait-for-url.js +65 -0
  128. package/dist/runner/web-server.d.ts +16 -0
  129. package/dist/runner/web-server.js +165 -0
  130. package/dist/runner/worker-pool.d.ts +13 -5
  131. package/dist/runner/worker-pool.js +85 -51
  132. package/dist/runner/worker-protocol.d.ts +8 -0
  133. package/dist/runner/worker-session.js +3 -1
  134. package/dist/runner/worker.d.ts +2 -0
  135. package/dist/runner/worker.js +7 -1
  136. package/dist/types/config.d.ts +83 -0
  137. package/dist/types/test.d.ts +18 -0
  138. package/dist/types/use.d.ts +45 -7
  139. package/package.json +1 -1
  140. package/dist/browser/emulation.d.ts +0 -61
  141. package/dist/browser/emulation.js +0 -180
@@ -1,5 +1,6 @@
1
1
  import type { SupportedBrowser } from "../types/browser.js";
2
2
  import type { RunEvent } from "./reporter.js";
3
+ import type { Shard } from "./shard.js";
3
4
  /** What decides which tests of the found files run, and how the browser starts */
4
5
  export interface RunnerOptions {
5
6
  /** Spec files to run, absolute or relative to the project folder. Default: every `*.spec.ts` under `srcDir` */
@@ -8,8 +9,12 @@ export interface RunnerOptions {
8
9
  grep?: string | RegExp;
9
10
  /** Only tests with exactly one of these full names (`describe` titles joined with " > ") */
10
11
  testNames?: string[];
11
- /** Only the tests of this group of the config's `groups` (unknown name: an error). Combines with the other filters */
12
- group?: string;
12
+ /**
13
+ * Only the tests of this group of the config's `groups`, or of any of these (unknown name: an error). Combines with
14
+ * the other filters. The run applies the group's `use` and `browsers`; a test in two selected groups runs once per
15
+ * group, and its results name the group.
16
+ */
17
+ group?: string | string[];
13
18
  /** The browsers to run in, instead of the group's or the project's `browsers` (the command line's `--browser`) */
14
19
  browsers?: SupportedBrowser[];
15
20
  /**
@@ -24,6 +29,12 @@ export interface RunnerOptions {
24
29
  * @default the config's `retries`, else 0
25
30
  */
26
31
  retries?: number | string;
32
+ /**
33
+ * Runs only one slice of the selected tests, to split a run over several machines (the command line's
34
+ * `--shard=current/total`). Whole files, and serial blocks, go to one shard; the shards are disjoint and together
35
+ * hold every test. The report names the shard. A shard without tests succeeds.
36
+ */
37
+ shard?: Shard;
27
38
  /**
28
39
  * Values for the inputs the tests declare, by name: every test of the run reads the names it declares from
29
40
  * here, and its own default for a name missing here
@@ -0,0 +1,14 @@
1
+ /** The last few lines a child process printed, kept for the error message when it fails */
2
+ export declare class OutputTail {
3
+ private readonly max;
4
+ /** Values to hide besides the registered secrets (the child's own `env` values) */
5
+ private readonly hidden;
6
+ private lines;
7
+ private partial;
8
+ constructor(max?: number,
9
+ /** Values to hide besides the registered secrets (the child's own `env` values) */
10
+ hidden?: readonly string[]);
11
+ add(chunk: Buffer | string): void;
12
+ /** The kept lines, masked, one per line; empty when the child printed nothing */
13
+ text(): string;
14
+ }
@@ -0,0 +1,30 @@
1
+ import { MASK, maskText } from "../config/mask.js";
2
+ /** The last few lines a child process printed, kept for the error message when it fails */
3
+ export class OutputTail {
4
+ constructor(max = 20,
5
+ /** Values to hide besides the registered secrets (the child's own `env` values) */
6
+ hidden = []) {
7
+ this.max = max;
8
+ this.hidden = hidden;
9
+ this.lines = [];
10
+ this.partial = "";
11
+ }
12
+ add(chunk) {
13
+ const text = this.partial + chunk.toString();
14
+ const parts = text.split(/\r?\n/);
15
+ this.partial = parts.pop() ?? "";
16
+ this.lines.push(...parts);
17
+ if (this.lines.length > this.max)
18
+ this.lines.splice(0, this.lines.length - this.max);
19
+ }
20
+ /** The kept lines, masked, one per line; empty when the child printed nothing */
21
+ text() {
22
+ let text = [...this.lines, this.partial]
23
+ .filter((line) => line !== "")
24
+ .join("\n");
25
+ for (const value of this.hidden)
26
+ if (value.length >= 4)
27
+ text = text.split(value).join(MASK);
28
+ return maskText(text);
29
+ }
30
+ }
@@ -0,0 +1,22 @@
1
+ import type { WorkItem } from "./worker-protocol.js";
2
+ /** What scheduling needs to know of a queued item and of a worker */
3
+ interface Queued {
4
+ item: WorkItem;
5
+ }
6
+ export interface Worker {
7
+ /** The unit this worker is running; it keeps the unit's items to itself until none is left */
8
+ unit?: string | undefined;
9
+ /** The serial key of the unit, held while the worker keeps the unit */
10
+ unitKey?: string | undefined;
11
+ busy?: {
12
+ entry: Queued;
13
+ } | undefined;
14
+ }
15
+ /**
16
+ * The queue position of the next item for `worker`: one of the unit it is running, else the first whose serial
17
+ * key is not running and whose unit is not held by a worker. -1 when there is none.
18
+ */
19
+ export declare function nextEligible(queue: Queued[], workers: Worker[], worker: Worker): number;
20
+ /** How many queued items could run at the same time right now: the items of a unit count one */
21
+ export declare function eligibleCount(queue: Queued[], workers: Worker[]): number;
22
+ export {};
@@ -0,0 +1,55 @@
1
+ /** The serial keys that are running, or kept by a worker between the items of its unit */
2
+ function busyKeys(workers) {
3
+ const keys = new Set();
4
+ for (const { busy, unitKey } of workers) {
5
+ const key = busy?.entry.item.serialKey ?? unitKey;
6
+ if (key)
7
+ keys.add(key);
8
+ }
9
+ return keys;
10
+ }
11
+ /** The units workers are running: no other worker may take their items */
12
+ function heldUnits(workers) {
13
+ const units = new Set();
14
+ for (const { unit } of workers)
15
+ if (unit)
16
+ units.add(unit);
17
+ return units;
18
+ }
19
+ /**
20
+ * The queue position of the next item for `worker`: one of the unit it is running, else the first whose serial
21
+ * key is not running and whose unit is not held by a worker. -1 when there is none.
22
+ */
23
+ export function nextEligible(queue, workers, worker) {
24
+ if (worker.unit) {
25
+ const unit = worker.unit;
26
+ const at = queue.findIndex(({ item }) => item.unit === unit);
27
+ if (at >= 0)
28
+ return at;
29
+ worker.unit = worker.unitKey = undefined;
30
+ }
31
+ const busy = busyKeys(workers);
32
+ const units = heldUnits(workers);
33
+ return queue.findIndex(({ item }) => (!item.serialKey || !busy.has(item.serialKey)) &&
34
+ (!item.unit || !units.has(item.unit)));
35
+ }
36
+ /** How many queued items could run at the same time right now: the items of a unit count one */
37
+ export function eligibleCount(queue, workers) {
38
+ const taken = busyKeys(workers);
39
+ const units = heldUnits(workers);
40
+ let count = 0;
41
+ for (const { item } of queue) {
42
+ if (item.unit) {
43
+ if (units.has(item.unit))
44
+ continue;
45
+ units.add(item.unit);
46
+ }
47
+ if (item.serialKey) {
48
+ if (taken.has(item.serialKey))
49
+ continue;
50
+ taken.add(item.serialKey);
51
+ }
52
+ count++;
53
+ }
54
+ return count;
55
+ }
@@ -0,0 +1,34 @@
1
+ import type { TestError, TestResult } from "../types/test.js";
2
+ import type { WorkItem } from "./worker-protocol.js";
3
+ /** The error a failed result carries, as the next result lists it in `previousAttempts` */
4
+ export declare function errorOf(result: TestResult): TestError;
5
+ /** The result of a test of a serial block that did not run, because an earlier test of the block failed */
6
+ export declare function serialSkipped(item: WorkItem): TestResult;
7
+ /** What the pool keeps of a queued item: the item, and whatever else it needs to find its place again */
8
+ interface Held<E> {
9
+ entry: E;
10
+ result: TestResult;
11
+ }
12
+ /**
13
+ * A serial block as the pool runs it. With retries the results are held until the block's last attempt is known,
14
+ * so a test ends once, saying which attempt that was; without, the pool reports them as they come and uses this
15
+ * only to count the attempts.
16
+ */
17
+ export declare class UnitState<E extends {
18
+ item: WorkItem;
19
+ }> {
20
+ readonly call: unknown;
21
+ /** Which attempt at the block this is, counting from 0 */
22
+ attempt: number;
23
+ private held;
24
+ private readonly previous;
25
+ constructor(call: unknown);
26
+ hold(entry: E, result: TestResult): void;
27
+ /** The block failed and runs again: forgets this attempt's results, remembers the error and returns the entries to queue again, in order */
28
+ restart(failed: E, result: TestResult): E[];
29
+ /** The held results of the last attempt, each saying which attempt it was and what failed before; they are not held any more */
30
+ release(): Held<E>[];
31
+ }
32
+ /** The result of an item that ended without one: the worker died, or the run was aborted */
33
+ export declare function failedResult(item: WorkItem, message: string, startTime?: number): TestResult;
34
+ export {};
@@ -0,0 +1,85 @@
1
+ import { SERIAL_SKIP_REASON } from "./serial.js";
2
+ /** The error a failed result carries, as the next result lists it in `previousAttempts` */
3
+ export function errorOf(result) {
4
+ const { message, type, stack, expected, actual } = result;
5
+ return {
6
+ message: message ?? "The test failed",
7
+ type: type ?? "error",
8
+ ...(stack !== undefined && { stack }),
9
+ ...(expected !== undefined && { expected }),
10
+ ...(actual !== undefined && { actual }),
11
+ };
12
+ }
13
+ /** The result of a test of a serial block that did not run, because an earlier test of the block failed */
14
+ export function serialSkipped(item) {
15
+ return {
16
+ name: item.testName,
17
+ file: item.file,
18
+ browser: item.browser,
19
+ status: "skipped",
20
+ startTime: Date.now(),
21
+ duration: 0,
22
+ annotation: "skip",
23
+ reason: SERIAL_SKIP_REASON,
24
+ };
25
+ }
26
+ /**
27
+ * A serial block as the pool runs it. With retries the results are held until the block's last attempt is known,
28
+ * so a test ends once, saying which attempt that was; without, the pool reports them as they come and uses this
29
+ * only to count the attempts.
30
+ */
31
+ export class UnitState {
32
+ constructor(call) {
33
+ this.call = call;
34
+ /** Which attempt at the block this is, counting from 0 */
35
+ this.attempt = 0;
36
+ this.held = [];
37
+ this.previous = new Map();
38
+ }
39
+ hold(entry, result) {
40
+ this.held.push({ entry, result });
41
+ }
42
+ /** The block failed and runs again: forgets this attempt's results, remembers the error and returns the entries to queue again, in order */
43
+ restart(failed, result) {
44
+ const entries = this.held.map(({ entry }) => entry);
45
+ this.held = [];
46
+ const errors = this.previous.get(failed) ?? [];
47
+ errors.push(errorOf(result));
48
+ this.previous.set(failed, errors);
49
+ this.attempt++;
50
+ return entries;
51
+ }
52
+ /** The held results of the last attempt, each saying which attempt it was and what failed before; they are not held any more */
53
+ release() {
54
+ const released = this.held.map(({ entry, result }) => {
55
+ const before = this.previous.get(entry) ?? [];
56
+ return {
57
+ entry,
58
+ result: {
59
+ ...result,
60
+ ...(this.attempt > 0 && {
61
+ retry: this.attempt,
62
+ previousAttempts: [...before],
63
+ ...(result.status === "success" &&
64
+ before.length > 0 && { flaky: true }),
65
+ }),
66
+ },
67
+ };
68
+ });
69
+ this.held = [];
70
+ return released;
71
+ }
72
+ }
73
+ /** The result of an item that ended without one: the worker died, or the run was aborted */
74
+ export function failedResult(item, message, startTime = Date.now()) {
75
+ return {
76
+ name: item.testName,
77
+ file: item.file,
78
+ browser: item.browser,
79
+ status: "failed",
80
+ type: "error",
81
+ message,
82
+ startTime,
83
+ duration: Date.now() - startTime,
84
+ };
85
+ }
@@ -3,6 +3,7 @@ import type { SamuraiGroup } from "../types/config.js";
3
3
  import type { RegisteredTestCase, TestResult } from "../types/test.js";
4
4
  import type { ResolvedGroup } from "./groups.js";
5
5
  import type TestReporter from "./reporter.js";
6
+ import type { RunUnit } from "./serial.js";
6
7
  import type { RunHooks } from "./worker-pool.js";
7
8
  import type { WorkItem } from "./worker-protocol.js";
8
9
  /** What a run needs from a worker pool: `WorkerPool` itself, or a fake in tests */
@@ -16,10 +17,11 @@ export interface PlannedTest {
16
17
  }
17
18
  /**
18
19
  * Lists the run: every selected test in the first browser, then every selected test in the next one. The
19
- * position in the list is the item's `index`. `serialKey` says which group makes a test run alone. `inputs`, the
20
- * run's input values, go with every item.
20
+ * position in the list is the item's `index`. `serialKey` says which group makes a test run alone, `unitOf` which
21
+ * tests must run one after the other on one worker (a serial block, a file). `inputs`, the run's input values, go
22
+ * with every item. `runsIn` says which browsers a test runs in (all of them unless a group names its own).
21
23
  */
22
- export declare function planItems(selected: RegisteredTestCase[], browsers: SupportedBrowser[], serialKey: (test: RegisteredTestCase) => string | undefined, inputs?: Record<string, string>): {
24
+ export declare function planItems(selected: RegisteredTestCase[], browsers: SupportedBrowser[], serialKey: (test: RegisteredTestCase) => string | undefined, inputs?: Record<string, string>, unitOf?: (test: RegisteredTestCase) => RunUnit | undefined, runsIn?: (test: RegisteredTestCase, browser: SupportedBrowser) => boolean): {
23
25
  items: WorkItem[];
24
26
  planned: PlannedTest[];
25
27
  };
@@ -29,8 +31,8 @@ export declare function planItems(selected: RegisteredTestCase[], browsers: Supp
29
31
  */
30
32
  export declare function serialKeyOf(groups: ResolvedGroup[], configGroups: SamuraiGroup[] | undefined, srcDir: string): (test: RegisteredTestCase) => string | undefined;
31
33
  /**
32
- * How many items can run at the same time: each item alone counts one, and the items of one serial key together
33
- * count one. More workers than this would only sit idle.
34
+ * How many items can run at the same time: each item alone counts one, and the items of one unit, or of one serial
35
+ * key, together count one. More workers than this would only sit idle.
34
36
  */
35
37
  export declare function concurrency(items: WorkItem[]): number;
36
38
  /**
@@ -2,23 +2,31 @@ import { idOf } from "./select.js";
2
2
  import { staticSkip } from "./skip.js";
3
3
  /**
4
4
  * Lists the run: every selected test in the first browser, then every selected test in the next one. The
5
- * position in the list is the item's `index`. `serialKey` says which group makes a test run alone. `inputs`, the
6
- * run's input values, go with every item.
5
+ * position in the list is the item's `index`. `serialKey` says which group makes a test run alone, `unitOf` which
6
+ * tests must run one after the other on one worker (a serial block, a file). `inputs`, the run's input values, go
7
+ * with every item. `runsIn` says which browsers a test runs in (all of them unless a group names its own).
7
8
  */
8
- export function planItems(selected, browsers, serialKey, inputs) {
9
- const planned = browsers.flatMap((browser) => selected.map((test) => ({ test, browser })));
9
+ export function planItems(selected, browsers, serialKey, inputs, unitOf = () => undefined, runsIn = () => true) {
10
+ const planned = browsers.flatMap((browser) => selected
11
+ .filter((test) => runsIn(test, browser))
12
+ .map((test) => ({ test, browser })));
10
13
  // A test that does not run (skip, fixme) is in the plan but not in the queue; `index` is its place in the plan
11
14
  const items = planned.flatMap(({ test, browser }, index) => {
12
15
  if (staticSkip(test))
13
16
  return [];
14
17
  const key = serialKey(test);
18
+ const unit = unitOf(test);
15
19
  return [
16
20
  {
17
21
  index,
18
22
  file: test.file,
19
23
  testName: test.name,
20
24
  browser,
25
+ ...(test.group !== undefined && { group: test.group }),
21
26
  ...(key !== undefined && { serialKey: key }),
27
+ // One unit per browser: the same block in two browsers is two runs
28
+ ...(unit && { unit: `${unit.key}@${browser}` }),
29
+ ...(unit?.serial && { serial: true }),
22
30
  ...(inputs !== undefined && { inputs }),
23
31
  },
24
32
  ];
@@ -41,17 +49,19 @@ export function serialKeyOf(groups, configGroups, srcDir) {
41
49
  return (test) => keys.get(idOf(test, srcDir));
42
50
  }
43
51
  /**
44
- * How many items can run at the same time: each item alone counts one, and the items of one serial key together
45
- * count one. More workers than this would only sit idle.
52
+ * How many items can run at the same time: each item alone counts one, and the items of one unit, or of one serial
53
+ * key, together count one. More workers than this would only sit idle.
46
54
  */
47
55
  export function concurrency(items) {
48
56
  const keys = new Set();
49
57
  let free = 0;
50
58
  for (const item of items) {
51
- if (item.serialKey === undefined)
52
- free++;
59
+ if (item.unit !== undefined)
60
+ keys.add(`unit:${item.unit}`);
61
+ else if (item.serialKey !== undefined)
62
+ keys.add(`key:${item.serialKey}`);
53
63
  else
54
- keys.add(item.serialKey);
64
+ free++;
55
65
  }
56
66
  return free + keys.size;
57
67
  }
@@ -7,10 +7,11 @@ import { runStep } from "./steps.js";
7
7
  import { markTests } from "./test-annotations.js";
8
8
  import { validateTestIO } from "./test-io.js";
9
9
  import { addHook, addUse, currentSuite, enterSuite, leaveSuite, modifierOf, resetSuites, } from "./suite.js";
10
+ import { configureSuite } from "./serial.js";
10
11
  const registered = [];
11
12
  /** 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);
13
+ function openDescribe(file, title, fn, modifier, reason, mode) {
14
+ enterSuite(file, title, modifier, reason, mode);
14
15
  try {
15
16
  const result = fn();
16
17
  if (result instanceof Promise) {
@@ -129,6 +130,9 @@ export const describe = Object.assign((title, fn) => openDescribe(callerFile(),
129
130
  skip: describeSkipping("skip"),
130
131
  fixme: describeSkipping("fixme"),
131
132
  only: (title, fn) => openDescribe(callerFile(), title, fn, "only"),
133
+ configure: (options) => configureSuite(currentSuite(callerFile()), options),
134
+ serial: (title, fn) => openDescribe(callerFile(), title, fn, undefined, undefined, "serial"),
135
+ parallel: (title, fn) => openDescribe(callerFile(), title, fn, undefined, undefined, "parallel"),
132
136
  });
133
137
  /** Runs before each test of the `describe` block it is written in (of the file, at the top level) */
134
138
  export function beforeEach(hook) {
@@ -1,5 +1,6 @@
1
1
  import type { TestAnnotation, TestStepEntry, TestAttachmentEntry, RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
2
2
  import type { SupportedBrowser } from "../types/browser.js";
3
+ 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
  }
@@ -74,6 +83,7 @@ export default class TestReporter implements TestSink {
74
83
  private browsers;
75
84
  private output;
76
85
  private listener;
86
+ private shard;
77
87
  /** Keys of the run's tests in the order the report lists them; unset: the order they started in */
78
88
  private order;
79
89
  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
  }
@@ -163,6 +172,7 @@ export default class TestReporter {
163
172
  name: test.name,
164
173
  file: test.file,
165
174
  browser,
175
+ ...groupOf(test),
166
176
  result,
167
177
  });
168
178
  }
@@ -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
+ }