@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.
- package/dist/api.d.ts +8 -3
- package/dist/api.js +1 -0
- package/dist/browser/browser-context.d.ts +18 -6
- package/dist/browser/browser-context.js +25 -7
- package/dist/browser/browser.d.ts +4 -4
- package/dist/browser/browser.js +11 -8
- package/dist/browser/chrome-launch.js +9 -1
- package/dist/browser/context-cookie-store.d.ts +17 -0
- package/dist/browser/context-cookie-store.js +90 -0
- package/dist/browser/index.d.ts +3 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/keyboard.d.ts +35 -0
- package/dist/browser/keyboard.js +83 -0
- package/dist/browser/mouse.d.ts +42 -0
- package/dist/browser/mouse.js +66 -0
- package/dist/browser/page-content.d.ts +4 -0
- package/dist/browser/page-content.js +12 -0
- package/dist/browser/page-events.d.ts +11 -0
- package/dist/browser/page-events.js +23 -0
- package/dist/browser/page-logging.d.ts +24 -0
- package/dist/browser/page-logging.js +43 -0
- package/dist/browser/page-navigation.d.ts +0 -2
- package/dist/browser/page-navigation.js +1 -6
- package/dist/browser/page-settings.d.ts +1 -1
- package/dist/browser/page-viewport.d.ts +21 -0
- package/dist/browser/page-viewport.js +33 -0
- package/dist/browser/page.d.ts +23 -16
- package/dist/browser/page.js +48 -71
- package/dist/browser/screenshot.d.ts +6 -0
- package/dist/browser/screenshot.js +14 -0
- package/dist/browser/storage-state.d.ts +0 -2
- package/dist/browser/storage-state.js +0 -20
- package/dist/browser/use-applier.d.ts +16 -5
- package/dist/browser/use-applier.js +44 -17
- package/dist/browser/use-commands.d.ts +36 -0
- package/dist/browser/use-commands.js +120 -0
- package/dist/cli/args.d.ts +8 -4
- package/dist/cli/args.js +8 -5
- package/dist/cli/console-reporter.js +5 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +4 -1
- package/dist/config/base-url.d.ts +4 -0
- package/dist/config/base-url.js +6 -0
- package/dist/config/config.js +7 -0
- package/dist/config/fixture-use.d.ts +19 -0
- package/dist/config/fixture-use.js +44 -0
- package/dist/config/fully-parallel.d.ts +3 -0
- package/dist/config/fully-parallel.js +6 -0
- package/dist/config/group-use.d.ts +10 -0
- package/dist/config/group-use.js +51 -0
- package/dist/config/resolved-config.d.ts +2 -0
- package/dist/config/resolved-config.js +3 -0
- package/dist/config/storage-state-file.d.ts +3 -0
- package/dist/config/storage-state-file.js +20 -0
- package/dist/config/use-options.d.ts +3 -1
- package/dist/config/use-options.js +74 -14
- package/dist/config/web-server-options.d.ts +7 -0
- package/dist/config/web-server-options.js +111 -0
- package/dist/locator/check-actions.js +1 -1
- package/dist/locator/drag-actions.d.ts +17 -0
- package/dist/locator/drag-actions.js +80 -0
- package/dist/locator/element-actions.d.ts +10 -4
- package/dist/locator/element-actions.js +23 -16
- package/dist/locator/element-state.d.ts +5 -0
- package/dist/locator/element-state.js +2 -1
- package/dist/locator/event-actions.d.ts +10 -0
- package/dist/locator/event-actions.js +81 -0
- package/dist/locator/input-actions.d.ts +2 -11
- package/dist/locator/input-actions.js +3 -56
- package/dist/locator/key-actions.d.ts +11 -0
- package/dist/locator/key-actions.js +52 -0
- package/dist/locator/key-map.d.ts +16 -0
- package/dist/locator/key-map.js +76 -0
- package/dist/locator/locator.d.ts +35 -5
- package/dist/locator/locator.js +70 -16
- package/dist/locator/page-scripts.d.ts +2 -0
- package/dist/locator/page-scripts.js +7 -0
- package/dist/locator/pointer-actions.d.ts +44 -0
- package/dist/locator/pointer-actions.js +117 -0
- package/dist/locator/pointer-element-actions.d.ts +21 -0
- package/dist/locator/pointer-element-actions.js +42 -0
- package/dist/request/api-request-context.d.ts +65 -0
- package/dist/request/api-request-context.js +198 -0
- package/dist/request/api-response.d.ts +34 -0
- package/dist/request/api-response.js +57 -0
- package/dist/request/cookie-jar.d.ts +48 -0
- package/dist/request/cookie-jar.js +164 -0
- package/dist/request/http-transport.d.ts +31 -0
- package/dist/request/http-transport.js +113 -0
- package/dist/request/redirect.d.ts +10 -0
- package/dist/request/redirect.js +21 -0
- package/dist/request/request-body.d.ts +27 -0
- package/dist/request/request-body.js +71 -0
- package/dist/request/request.d.ts +16 -0
- package/dist/request/request.js +20 -0
- package/dist/runner/all-hooks.d.ts +7 -1
- package/dist/runner/all-hooks.js +17 -1
- package/dist/runner/fixture-defs.d.ts +2 -1
- package/dist/runner/fixture-defs.js +52 -18
- package/dist/runner/fixture-meta.d.ts +20 -0
- package/dist/runner/fixture-meta.js +109 -0
- package/dist/runner/fixture-options.d.ts +8 -0
- package/dist/runner/fixture-options.js +35 -0
- package/dist/runner/fixture-outputs.d.ts +11 -0
- package/dist/runner/fixture-outputs.js +28 -0
- package/dist/runner/fixture-registry.d.ts +35 -0
- package/dist/runner/fixture-registry.js +73 -0
- package/dist/runner/fixture-scope.d.ts +39 -9
- package/dist/runner/fixture-scope.js +139 -70
- package/dist/runner/fixture-start.d.ts +27 -0
- package/dist/runner/fixture-start.js +76 -0
- package/dist/runner/fixture-use.d.ts +23 -0
- package/dist/runner/fixture-use.js +64 -0
- package/dist/runner/fixture-worker.d.ts +26 -0
- package/dist/runner/fixture-worker.js +46 -0
- package/dist/runner/fixtures.d.ts +4 -1
- package/dist/runner/fixtures.js +3 -2
- package/dist/runner/global-hooks.d.ts +13 -0
- package/dist/runner/global-hooks.js +66 -0
- package/dist/runner/options.d.ts +13 -2
- package/dist/runner/output-tail.d.ts +14 -0
- package/dist/runner/output-tail.js +30 -0
- package/dist/runner/pool-schedule.d.ts +22 -0
- package/dist/runner/pool-schedule.js +55 -0
- package/dist/runner/pool-units.d.ts +34 -0
- package/dist/runner/pool-units.js +85 -0
- package/dist/runner/pooled-run.d.ts +7 -5
- package/dist/runner/pooled-run.js +19 -9
- package/dist/runner/prepare-run.js +5 -0
- package/dist/runner/registry.js +21 -7
- package/dist/runner/reporter.d.ts +13 -1
- package/dist/runner/reporter.js +13 -2
- package/dist/runner/request-fixture.d.ts +14 -0
- package/dist/runner/request-fixture.js +25 -0
- package/dist/runner/run-hooks.d.ts +16 -0
- package/dist/runner/run-hooks.js +52 -0
- package/dist/runner/run-test.d.ts +3 -0
- package/dist/runner/run-test.js +23 -3
- package/dist/runner/run.d.ts +8 -0
- package/dist/runner/run.js +10 -0
- package/dist/runner/select.d.ts +15 -4
- package/dist/runner/select.js +38 -12
- package/dist/runner/serial-run.d.ts +23 -0
- package/dist/runner/serial-run.js +88 -0
- package/dist/runner/serial.d.ts +34 -0
- package/dist/runner/serial.js +63 -0
- package/dist/runner/shard.d.ts +22 -0
- package/dist/runner/shard.js +73 -0
- package/dist/runner/suite.d.ts +3 -3
- package/dist/runner/suite.js +4 -3
- package/dist/runner/test-function.d.ts +15 -3
- package/dist/runner/test-info.js +2 -1
- package/dist/runner/test-runner.d.ts +1 -0
- package/dist/runner/test-runner.js +67 -18
- package/dist/runner/use-options.d.ts +18 -4
- package/dist/runner/use-options.js +33 -6
- package/dist/runner/wait-for-url.d.ts +20 -0
- package/dist/runner/wait-for-url.js +65 -0
- package/dist/runner/web-server.d.ts +16 -0
- package/dist/runner/web-server.js +165 -0
- package/dist/runner/worker-pool.d.ts +13 -5
- package/dist/runner/worker-pool.js +85 -51
- package/dist/runner/worker-protocol.d.ts +8 -0
- package/dist/runner/worker-session.js +9 -1
- package/dist/runner/worker.d.ts +2 -0
- package/dist/runner/worker.js +7 -1
- package/dist/types/config.d.ts +117 -21
- package/dist/types/extend.d.ts +54 -2
- package/dist/types/test.d.ts +42 -0
- package/dist/types/use.d.ts +59 -7
- package/package.json +1 -1
- package/dist/browser/emulation.d.ts +0 -61
- package/dist/browser/emulation.js +0 -180
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { SERIAL_SKIP_REASON } from "./serial.js";
|
|
2
|
+
import { staticSkip } from "./skip.js";
|
|
3
|
+
/**
|
|
4
|
+
* Runs the tests of a serial block in order. After a failure the rest of the block is reported as skipped; with
|
|
5
|
+
* `retries`, the whole block then runs again from its first test (after `restart`), up to `retries` times.
|
|
6
|
+
* Without retries every test is reported as it ends. With them the ends are held until the block's last attempt is
|
|
7
|
+
* known, as `runWithRetries` does for one test: each test ends once, with `retry` saying which attempt that was.
|
|
8
|
+
* An aborted run is never run again.
|
|
9
|
+
*/
|
|
10
|
+
export async function serialBlockRun(run) {
|
|
11
|
+
const { tests, browser, retries, signal, sink } = run;
|
|
12
|
+
const live = retries === 0;
|
|
13
|
+
const previous = tests.map(() => []);
|
|
14
|
+
for (let attempt = 0;; attempt++) {
|
|
15
|
+
const ended = [];
|
|
16
|
+
let failedAt = -1;
|
|
17
|
+
for (const [at, test] of tests.entries()) {
|
|
18
|
+
const skip = staticSkip(test);
|
|
19
|
+
if (skip) {
|
|
20
|
+
sink.onTestSkipped(test, browser, skip);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (signal?.aborted) {
|
|
24
|
+
for (const rest of tests.slice(at)) {
|
|
25
|
+
if (staticSkip(rest))
|
|
26
|
+
continue;
|
|
27
|
+
sink.onTestStart(rest, browser);
|
|
28
|
+
sink.onTestEnd(rest, browser, {
|
|
29
|
+
message: "Run aborted",
|
|
30
|
+
type: "error",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const attemptSink = {
|
|
36
|
+
onTestStart: (t, b) => sink.onTestStart(t, b, attempt > 0 ? attempt : undefined),
|
|
37
|
+
onTestEnd: (t, b, error, extras) => {
|
|
38
|
+
ended[at] = { error, extras };
|
|
39
|
+
if (live)
|
|
40
|
+
sink.onTestEnd(t, b, error, extras);
|
|
41
|
+
},
|
|
42
|
+
onTestSkipped: (t, b, why) => sink.onTestSkipped(t, b, why),
|
|
43
|
+
};
|
|
44
|
+
await run.runTest(test, attemptSink, attempt);
|
|
45
|
+
if (ended[at]?.error) {
|
|
46
|
+
failedAt = at;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const error = ended[failedAt]?.error;
|
|
51
|
+
if (error &&
|
|
52
|
+
attempt < retries &&
|
|
53
|
+
!signal?.aborted &&
|
|
54
|
+
error.message !== "Run aborted") {
|
|
55
|
+
previous[failedAt].push(error);
|
|
56
|
+
await run.restart();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!live)
|
|
60
|
+
flush(run, ended, previous, attempt);
|
|
61
|
+
if (failedAt >= 0)
|
|
62
|
+
for (const rest of tests.slice(failedAt + 1)) {
|
|
63
|
+
const skip = staticSkip(rest) ?? {
|
|
64
|
+
annotation: "skip",
|
|
65
|
+
reason: SERIAL_SKIP_REASON,
|
|
66
|
+
};
|
|
67
|
+
sink.onTestSkipped(rest, browser, skip);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Reports the held ends of the last attempt, each saying which attempt it was and what failed before */
|
|
73
|
+
function flush({ tests, browser, sink }, ended, previous, attempt) {
|
|
74
|
+
for (const [at, test] of tests.entries()) {
|
|
75
|
+
const end = ended[at];
|
|
76
|
+
if (!end)
|
|
77
|
+
continue;
|
|
78
|
+
const before = previous[at];
|
|
79
|
+
sink.onTestEnd(test, browser, end.error, {
|
|
80
|
+
...end.extras,
|
|
81
|
+
...(attempt > 0 && {
|
|
82
|
+
retry: attempt,
|
|
83
|
+
previousAttempts: [...before],
|
|
84
|
+
...(!end.error && before.length > 0 && { flaky: true }),
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RegisteredTestCase, Suite } from "../types/test.js";
|
|
2
|
+
/** Why a test of a serial block did not run: an earlier test of the block failed */
|
|
3
|
+
export declare const SERIAL_SKIP_REASON = "serial: an earlier test failed";
|
|
4
|
+
/** `describe.configure(options)`: sets the mode of the suite the call is written in */
|
|
5
|
+
export declare function configureSuite(suite: Suite, options: unknown): void;
|
|
6
|
+
/** A group of tests that run in order on one worker; if one fails, the rest of the block is skipped */
|
|
7
|
+
export interface SerialBlock {
|
|
8
|
+
/** The file and the `describe` titles of the block's outermost serial suite; the same in every process */
|
|
9
|
+
key: string;
|
|
10
|
+
/** The outermost suite in serial mode */
|
|
11
|
+
suite: Suite;
|
|
12
|
+
}
|
|
13
|
+
/** The serial block a test belongs to: the outermost suite around it in `"serial"` mode, else none */
|
|
14
|
+
export declare function serialBlockOf(test: RegisteredTestCase): SerialBlock | undefined;
|
|
15
|
+
/** The tests that must run one after the other on one worker, in the order they are listed */
|
|
16
|
+
export interface RunUnit {
|
|
17
|
+
/** The same for every test of the unit, in every process */
|
|
18
|
+
key: string;
|
|
19
|
+
/** A serial block: a failure skips the rest and a retry runs the whole block again */
|
|
20
|
+
serial: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The unit a test runs in. A serial block is one. With `fullyParallel: false` every other test is in the unit of
|
|
24
|
+
* its file, unless a suite around it is in `"parallel"` mode. Otherwise (`true`, or not set: the tests of a file
|
|
25
|
+
* go to workers one by one) there is no unit.
|
|
26
|
+
*/
|
|
27
|
+
export declare function unitOf(test: RegisteredTestCase, fullyParallel: boolean | undefined): RunUnit | undefined;
|
|
28
|
+
/** Tests that follow each other and belong to one serial block (`block`), or one test outside any block */
|
|
29
|
+
export interface Chunk {
|
|
30
|
+
block?: SerialBlock;
|
|
31
|
+
tests: RegisteredTestCase[];
|
|
32
|
+
}
|
|
33
|
+
/** Splits a list of tests into serial blocks and the free tests between them, keeping the order */
|
|
34
|
+
export declare function chunkBySerialBlock(tests: RegisteredTestCase[]): Chunk[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { suiteChain } from "./suite.js";
|
|
2
|
+
/** Why a test of a serial block did not run: an earlier test of the block failed */
|
|
3
|
+
export const SERIAL_SKIP_REASON = "serial: an earlier test failed";
|
|
4
|
+
const MODES = ["default", "parallel", "serial"];
|
|
5
|
+
/** `describe.configure(options)`: sets the mode of the suite the call is written in */
|
|
6
|
+
export function configureSuite(suite, options) {
|
|
7
|
+
if (typeof options !== "object" || options === null)
|
|
8
|
+
throw new TypeError("describe.configure(): expected an options object like { mode: 'serial' }");
|
|
9
|
+
const { mode, ...rest } = options;
|
|
10
|
+
const unknown = Object.keys(rest);
|
|
11
|
+
if (unknown.length > 0)
|
|
12
|
+
throw new TypeError(`describe.configure(): unknown option "${unknown[0]}" (only mode is supported)`);
|
|
13
|
+
if (!MODES.includes(mode))
|
|
14
|
+
throw new TypeError(`describe.configure(): mode must be "default", "parallel" or "serial", got ${JSON.stringify(mode)}`);
|
|
15
|
+
if (mode === "default")
|
|
16
|
+
delete suite.mode;
|
|
17
|
+
else
|
|
18
|
+
suite.mode = mode;
|
|
19
|
+
}
|
|
20
|
+
const groupSuffix = (test) => test.group === undefined ? "" : `@${test.group}`;
|
|
21
|
+
/** The serial block a test belongs to: the outermost suite around it in `"serial"` mode, else none */
|
|
22
|
+
export function serialBlockOf(test) {
|
|
23
|
+
const suite = suiteChain(test.suite).find((s) => s.mode === "serial");
|
|
24
|
+
if (!suite)
|
|
25
|
+
return undefined;
|
|
26
|
+
const titles = suiteChain(suite).flatMap((s) => s.title === undefined ? [] : [s.title]);
|
|
27
|
+
// The same block run for two groups is two blocks
|
|
28
|
+
return {
|
|
29
|
+
key: `serial:${test.file}::${titles.join(" > ")}${groupSuffix(test)}`,
|
|
30
|
+
suite,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The unit a test runs in. A serial block is one. With `fullyParallel: false` every other test is in the unit of
|
|
35
|
+
* its file, unless a suite around it is in `"parallel"` mode. Otherwise (`true`, or not set: the tests of a file
|
|
36
|
+
* go to workers one by one) there is no unit.
|
|
37
|
+
*/
|
|
38
|
+
export function unitOf(test, fullyParallel) {
|
|
39
|
+
const block = serialBlockOf(test);
|
|
40
|
+
if (block)
|
|
41
|
+
return { key: block.key, serial: true };
|
|
42
|
+
if (fullyParallel !== false)
|
|
43
|
+
return undefined;
|
|
44
|
+
const nearest = suiteChain(test.suite)
|
|
45
|
+
.reverse()
|
|
46
|
+
.find((s) => s.mode === "parallel");
|
|
47
|
+
return nearest
|
|
48
|
+
? undefined
|
|
49
|
+
: { key: `file:${test.file}${groupSuffix(test)}`, serial: false };
|
|
50
|
+
}
|
|
51
|
+
/** Splits a list of tests into serial blocks and the free tests between them, keeping the order */
|
|
52
|
+
export function chunkBySerialBlock(tests) {
|
|
53
|
+
const chunks = [];
|
|
54
|
+
for (const test of tests) {
|
|
55
|
+
const block = serialBlockOf(test);
|
|
56
|
+
const last = chunks[chunks.length - 1];
|
|
57
|
+
if (block && last?.block?.key === block.key)
|
|
58
|
+
last.tests.push(test);
|
|
59
|
+
else
|
|
60
|
+
chunks.push({ ...(block && { block }), tests: [test] });
|
|
61
|
+
}
|
|
62
|
+
return chunks;
|
|
63
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RegisteredTestCase } from "../types/test.js";
|
|
2
|
+
/** One slice of a run split over several machines: slice `current` of `total`, counting from 1 */
|
|
3
|
+
export interface Shard {
|
|
4
|
+
current: number;
|
|
5
|
+
total: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Reads a shard: `"2/4"` (the command line's `--shard`) or `{ current: 2, total: 4 }` (the `shard` run option).
|
|
9
|
+
* `where` names the source in the error.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseShard(value: unknown, where: string): Shard;
|
|
12
|
+
/**
|
|
13
|
+
* What a shard keeps together: a serial block is never split, and so is a file, unless `fullyParallel` is on or a
|
|
14
|
+
* `describe` around the test is in `"parallel"` mode: then the test goes by itself.
|
|
15
|
+
*/
|
|
16
|
+
export declare function shardGroupOf(test: RegisteredTestCase, fullyParallel: boolean | undefined): string;
|
|
17
|
+
/**
|
|
18
|
+
* The tests of one shard. The groups (in the order their first test comes) are dealt out in order, so the shards are
|
|
19
|
+
* as equal in tests as whole groups allow: a group goes to the shard that holds its first test when all tests are
|
|
20
|
+
* cut into `total` equal ranges. The shards are disjoint and together hold every test; the order stays.
|
|
21
|
+
*/
|
|
22
|
+
export declare function shardTests(tests: RegisteredTestCase[], { current, total }: Shard, groupOf: (test: RegisteredTestCase) => string): RegisteredTestCase[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { serialBlockOf } from "./serial.js";
|
|
2
|
+
import { suiteChain } from "./suite.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reads a shard: `"2/4"` (the command line's `--shard`) or `{ current: 2, total: 4 }` (the `shard` run option).
|
|
5
|
+
* `where` names the source in the error.
|
|
6
|
+
*/
|
|
7
|
+
export function parseShard(value, where) {
|
|
8
|
+
const shard = shardOf(value);
|
|
9
|
+
if (shard &&
|
|
10
|
+
Number.isInteger(shard.current) &&
|
|
11
|
+
Number.isInteger(shard.total) &&
|
|
12
|
+
shard.current >= 1 &&
|
|
13
|
+
shard.current <= shard.total)
|
|
14
|
+
return { current: shard.current, total: shard.total };
|
|
15
|
+
const shown = typeof value === "string" ? value : JSON.stringify(value);
|
|
16
|
+
throw new Error(`${where}: expected current/total with 1 <= current <= total, got ${typeof value === "string" ? `"${shown}"` : String(shown)}`);
|
|
17
|
+
}
|
|
18
|
+
function shardOf(value) {
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(value);
|
|
21
|
+
return match
|
|
22
|
+
? { current: Number(match[1]), total: Number(match[2]) }
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "object" && value !== null) {
|
|
26
|
+
const { current, total } = value;
|
|
27
|
+
if (typeof current === "number" && typeof total === "number")
|
|
28
|
+
return { current, total };
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What a shard keeps together: a serial block is never split, and so is a file, unless `fullyParallel` is on or a
|
|
34
|
+
* `describe` around the test is in `"parallel"` mode: then the test goes by itself.
|
|
35
|
+
*/
|
|
36
|
+
export function shardGroupOf(test, fullyParallel) {
|
|
37
|
+
const block = serialBlockOf(test);
|
|
38
|
+
if (block)
|
|
39
|
+
return block.key;
|
|
40
|
+
const alone = fullyParallel === true ||
|
|
41
|
+
suiteChain(test.suite).some((s) => s.mode === "parallel");
|
|
42
|
+
return alone ? `test:${test.file}::${test.name}` : `file:${test.file}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The tests of one shard. The groups (in the order their first test comes) are dealt out in order, so the shards are
|
|
46
|
+
* as equal in tests as whole groups allow: a group goes to the shard that holds its first test when all tests are
|
|
47
|
+
* cut into `total` equal ranges. The shards are disjoint and together hold every test; the order stays.
|
|
48
|
+
*/
|
|
49
|
+
export function shardTests(tests, { current, total }, groupOf) {
|
|
50
|
+
const groups = new Map();
|
|
51
|
+
for (const test of tests) {
|
|
52
|
+
const key = groupOf(test);
|
|
53
|
+
const members = groups.get(key);
|
|
54
|
+
if (members)
|
|
55
|
+
members.push(test);
|
|
56
|
+
else
|
|
57
|
+
groups.set(key, [test]);
|
|
58
|
+
}
|
|
59
|
+
const size = Math.floor(tests.length / total);
|
|
60
|
+
const extra = tests.length - size * total;
|
|
61
|
+
const index = current - 1;
|
|
62
|
+
const from = size * index + Math.min(extra, index);
|
|
63
|
+
const to = from + size + (index < extra ? 1 : 0);
|
|
64
|
+
const kept = new Set();
|
|
65
|
+
let first = 0;
|
|
66
|
+
for (const members of groups.values()) {
|
|
67
|
+
if (first >= from && first < to)
|
|
68
|
+
for (const test of members)
|
|
69
|
+
kept.add(test);
|
|
70
|
+
first += members.length;
|
|
71
|
+
}
|
|
72
|
+
return tests.filter((test) => kept.has(test));
|
|
73
|
+
}
|
package/dist/runner/suite.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { EachHook, Modifier, Suite } from "../types/test.js";
|
|
1
|
+
import type { EachHook, Modifier, Suite, SuiteMode } from "../types/test.js";
|
|
2
2
|
import type { UseOptions } from "../types/use.js";
|
|
3
3
|
/** The four hook lists a suite can hold */
|
|
4
4
|
export type HookKind = "beforeEach" | "afterEach" | "beforeAll" | "afterAll";
|
|
5
|
-
export declare function newSuite(title?: string, parent?: Suite, modifier?: Modifier, reason?: string): Suite;
|
|
5
|
+
export declare function newSuite(title?: string, parent?: Suite, modifier?: Modifier, reason?: string, mode?: SuiteMode): Suite;
|
|
6
6
|
/** The suite a test or hook written now belongs to: the innermost open `describe`, else the file's own */
|
|
7
7
|
export declare function currentSuite(file: string): Suite;
|
|
8
8
|
/** Opens a `describe` block under the current suite; `leaveSuite` closes it */
|
|
9
|
-
export declare function enterSuite(file: string, title: string, modifier?: Modifier, reason?: string): Suite;
|
|
9
|
+
export declare function enterSuite(file: string, title: string, modifier?: Modifier, reason?: string, mode?: SuiteMode): Suite;
|
|
10
10
|
export declare function leaveSuite(): void;
|
|
11
11
|
/** Forgets every suite: a run registers its specs afresh */
|
|
12
12
|
export declare function resetSuites(): void;
|
package/dist/runner/suite.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
const roots = new Map();
|
|
3
3
|
/** The `describe` blocks being registered, outermost first */
|
|
4
4
|
const open = [];
|
|
5
|
-
export function newSuite(title, parent, modifier, reason) {
|
|
5
|
+
export function newSuite(title, parent, modifier, reason, mode) {
|
|
6
6
|
return {
|
|
7
7
|
...(title !== undefined && { title }),
|
|
8
8
|
...(parent && { parent }),
|
|
9
9
|
...(modifier && { modifier }),
|
|
10
10
|
...(reason !== undefined && { reason }),
|
|
11
|
+
...(mode && { mode }),
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
/** The suite a test or hook written now belongs to: the innermost open `describe`, else the file's own */
|
|
@@ -23,8 +24,8 @@ export function currentSuite(file) {
|
|
|
23
24
|
return root;
|
|
24
25
|
}
|
|
25
26
|
/** Opens a `describe` block under the current suite; `leaveSuite` closes it */
|
|
26
|
-
export function enterSuite(file, title, modifier, reason) {
|
|
27
|
-
const suite = newSuite(title, currentSuite(file), modifier, reason);
|
|
27
|
+
export function enterSuite(file, title, modifier, reason, mode) {
|
|
28
|
+
const suite = newSuite(title, currentSuite(file), modifier, reason, mode);
|
|
28
29
|
open.push(suite);
|
|
29
30
|
return suite;
|
|
30
31
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AllHook, TestCaseBase, TestDetails, TestInfo, TestOptions } from "../types/test.js";
|
|
2
|
-
import type { UseOptions } from "../types/use.js";
|
|
1
|
+
import type { AllHook, SuiteMode, TestCaseBase, TestDetails, TestInfo, TestOptions } from "../types/test.js";
|
|
2
|
+
import type { FixtureUse, UseOptions } from "../types/use.js";
|
|
3
3
|
import type { FixtureDefinitions, FixturesOf, MergeFixtures } from "../types/extend.js";
|
|
4
4
|
/** What the registry keeps of a test body; the fixtures it gets are those of the test it was written for */
|
|
5
5
|
export type Body = TestCaseBase["function"];
|
|
@@ -46,7 +46,7 @@ export interface TestFunction<X = NoFixtures> {
|
|
|
46
46
|
* the whole file at the top level, whether before or after the tests. An inner block wins over an outer one, and
|
|
47
47
|
* both win over config `use`.
|
|
48
48
|
*/
|
|
49
|
-
use: (options: UseOptions) => void;
|
|
49
|
+
use: (options: UseOptions & FixtureUse) => void;
|
|
50
50
|
/** `describe`, under the name other frameworks use */
|
|
51
51
|
describe: DescribeFunction;
|
|
52
52
|
/** `beforeEach`, as `test.beforeEach` */
|
|
@@ -86,6 +86,18 @@ export interface DescribeFunction {
|
|
|
86
86
|
skip: DescribeSkipFunction;
|
|
87
87
|
fixme: DescribeSkipFunction;
|
|
88
88
|
only(title: string, fn: () => void): void;
|
|
89
|
+
/**
|
|
90
|
+
* Sets how the tests of the `describe` block it is written in (of the file, at the top level) run:
|
|
91
|
+
* `"serial"` runs them in order on one worker and skips the rest after a failure, `"parallel"` lets them run on
|
|
92
|
+
* different workers even when the config does not, `"default"` follows the config's `fullyParallel`
|
|
93
|
+
*/
|
|
94
|
+
configure(options: {
|
|
95
|
+
mode: SuiteMode;
|
|
96
|
+
}): void;
|
|
97
|
+
/** A `describe` block in `"serial"` mode */
|
|
98
|
+
serial(title: string, fn: () => void): void;
|
|
99
|
+
/** A `describe` block in `"parallel"` mode */
|
|
100
|
+
parallel(title: string, fn: () => void): void;
|
|
89
101
|
}
|
|
90
102
|
/** `test.fail()` and `test.slow()`: with a condition (a function gets the test's fixtures), or none for "always" */
|
|
91
103
|
export interface ConditionFunction<X> {
|
package/dist/runner/test-info.js
CHANGED
|
@@ -26,7 +26,8 @@ export class TestInfoImpl {
|
|
|
26
26
|
this.column = test.column;
|
|
27
27
|
this.retry = from.retry;
|
|
28
28
|
this.workerIndex = from.workerIndex;
|
|
29
|
-
|
|
29
|
+
// A run for a group is named after it (as a Playwright project is), else after the browser
|
|
30
|
+
this.project = { name: test.group ?? from.browserName };
|
|
30
31
|
this.config = { ...from.settings };
|
|
31
32
|
this.resultDir = from.resultDir;
|
|
32
33
|
this.outputDir = from.outputDir;
|
|
@@ -39,6 +39,7 @@ export default class TestRunner {
|
|
|
39
39
|
* was also written to the report. With more than one worker the tests run at the same time in worker processes.
|
|
40
40
|
*/
|
|
41
41
|
start(): Promise<import("../types/test.js").TestSummary>;
|
|
42
|
+
private execute;
|
|
42
43
|
/** Whether anything but the default says how many workers to use */
|
|
43
44
|
private workersAsked;
|
|
44
45
|
/** The pool of a run: up to `workers` worker processes that read the same project and settings as this run */
|
|
@@ -6,6 +6,7 @@ import { loadRawConfig, projectDir, readConfig } from "../config/config.js";
|
|
|
6
6
|
import { forbidsOnly } from "../config/forbid-only.js";
|
|
7
7
|
import logger from "../logger/index.js";
|
|
8
8
|
import { AllHooks } from "./all-hooks.js";
|
|
9
|
+
import { WorkerFixtures } from "./fixture-worker.js";
|
|
9
10
|
import { assertNoOnly, narrowToOnly } from "./focus.js";
|
|
10
11
|
import { DEFAULT_SRC_DIR, DEFAULT_TEST_MATCH } from "../config/defaults.js";
|
|
11
12
|
import { DEFAULT_PORT } from "./ports.js";
|
|
@@ -14,11 +15,15 @@ import { concurrency, planItems, poolMode, runPooled, serialKeyOf, } from "./poo
|
|
|
14
15
|
import { allHookFixtures } from "./fixtures.js";
|
|
15
16
|
import { runOneTest } from "./run-test.js";
|
|
16
17
|
import { staticSkip } from "./skip.js";
|
|
17
|
-
import {
|
|
18
|
+
import { browserPlan, expandByGroup, loadSpecs, selectTests, } from "./select.js";
|
|
18
19
|
import { resolveRetries } from "./retries-option.js";
|
|
19
20
|
import { runWithRetries } from "./retry.js";
|
|
21
|
+
import { chunkBySerialBlock, unitOf } from "./serial.js";
|
|
22
|
+
import { parseShard, shardGroupOf, shardTests } from "./shard.js";
|
|
23
|
+
import { serialBlockRun } from "./serial-run.js";
|
|
20
24
|
import { resolveWorkers } from "../config/workers-option.js";
|
|
21
25
|
import { runOutput } from "./output.js";
|
|
26
|
+
import { withRunHooks } from "./run-hooks.js";
|
|
22
27
|
import { WorkerPool } from "./worker-pool.js";
|
|
23
28
|
import { processWorkerFactory } from "./worker-process.js";
|
|
24
29
|
/** Finds the spec files, picks the tests and browsers of a run and runs each test in each browser */
|
|
@@ -63,18 +68,37 @@ export default class TestRunner {
|
|
|
63
68
|
* was also written to the report. With more than one worker the tests run at the same time in worker processes.
|
|
64
69
|
*/
|
|
65
70
|
async start() {
|
|
71
|
+
const config = await loadRawConfig();
|
|
72
|
+
// Web servers and `globalSetup` come first and `globalTeardown` last, around everything below
|
|
73
|
+
return withRunHooks({
|
|
74
|
+
config,
|
|
75
|
+
settings: this.run.settings,
|
|
76
|
+
projectDir: projectDir(),
|
|
77
|
+
signal: this.options.signal,
|
|
78
|
+
}, () => this.execute(config));
|
|
79
|
+
}
|
|
80
|
+
async execute(config) {
|
|
66
81
|
const { onEvent, signal } = this.options;
|
|
67
82
|
// The specs are imported once: their top-level code runs once per run
|
|
68
83
|
const loaded = await loadSpecs(this.testFiles);
|
|
69
|
-
const config = await loadRawConfig();
|
|
70
84
|
// A focused test the config refuses stops the run before anything starts; else only the focused ones run
|
|
71
85
|
assertNoOnly(loaded.all, forbidsOnly(config), loaded.srcDir);
|
|
72
|
-
const
|
|
73
|
-
|
|
86
|
+
const shard = this.options.shard
|
|
87
|
+
? parseShard(this.options.shard, "shard")
|
|
88
|
+
: undefined;
|
|
89
|
+
const focused = narrowToOnly(selectTests(loaded, this.options));
|
|
90
|
+
const sharded = shard
|
|
91
|
+
? shardTests(focused, shard, (test) => shardGroupOf(test, config.fullyParallel))
|
|
92
|
+
: focused;
|
|
93
|
+
// A test of two selected groups is two runs, each with its group's `use` and `browsers`
|
|
94
|
+
const selected = expandByGroup(sharded, loaded, this.options);
|
|
95
|
+
const { browsers, browsersOf } = await browserPlan(loaded.groups, this.options);
|
|
96
|
+
const runsIn = (test, browser) => browsersOf(test).includes(browser);
|
|
74
97
|
const reporter = new TestReporter({
|
|
75
98
|
environment: this.run.settings.environment,
|
|
76
99
|
browsers,
|
|
77
100
|
output: runOutput(this.options, projectDir()).report,
|
|
101
|
+
...(shard && { shard }),
|
|
78
102
|
...(onEvent && { onEvent }),
|
|
79
103
|
});
|
|
80
104
|
// How the run goes is settled first: a bad `workers` is an error before any browser starts
|
|
@@ -89,7 +113,7 @@ export default class TestRunner {
|
|
|
89
113
|
env: process.env.SAMURAI_RETRIES,
|
|
90
114
|
config: config.retries,
|
|
91
115
|
});
|
|
92
|
-
const plan = planItems(selected, browsers, serialKeyOf(loaded.groups, config.groups, loaded.srcDir), this.options.inputs);
|
|
116
|
+
const plan = planItems(selected, browsers, serialKeyOf(loaded.groups, config.groups, loaded.srcDir), this.options.inputs, (test) => unitOf(test, config.fullyParallel), runsIn);
|
|
93
117
|
// A pool larger than what can overlap would only hold idle workers: a group that runs one at a time needs one
|
|
94
118
|
const lanes = concurrency(plan.items);
|
|
95
119
|
const mode = poolMode({
|
|
@@ -120,35 +144,59 @@ export default class TestRunner {
|
|
|
120
144
|
}
|
|
121
145
|
return reporter.onEnd();
|
|
122
146
|
}
|
|
123
|
-
reporter.onStart(
|
|
147
|
+
reporter.onStart(plan.planned.length);
|
|
124
148
|
// The `afterAll` hooks run when the last test is done, before the run ends
|
|
125
149
|
const allHooks = new AllHooks({
|
|
126
150
|
timeout: this.run.settings.timeout,
|
|
127
151
|
fixtures: (browser) => allHookFixtures(this.run, browser),
|
|
128
152
|
});
|
|
153
|
+
// The worker-scoped fixtures of the run: torn down when the last test is done
|
|
154
|
+
const workerFixtures = new WorkerFixtures(this.run.settings.timeout);
|
|
129
155
|
try {
|
|
130
156
|
for (const browser of browsers) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
157
|
+
const forBrowser = selected.filter((test) => runsIn(test, browser));
|
|
158
|
+
for (const chunk of chunkBySerialBlock(forBrowser)) {
|
|
159
|
+
const runTest = (test, sink, attempt) => runOneTest(test, browser, this.run, this.options, loaded.srcDir, sink, { allHooks, workerFixtures, attempt });
|
|
160
|
+
if (chunk.block) {
|
|
161
|
+
const { suite } = chunk.block;
|
|
162
|
+
await serialBlockRun({
|
|
163
|
+
tests: chunk.tests,
|
|
164
|
+
browser,
|
|
165
|
+
retries,
|
|
166
|
+
signal,
|
|
167
|
+
sink: reporter,
|
|
168
|
+
runTest,
|
|
169
|
+
restart: async () => {
|
|
170
|
+
for (const error of await allHooks.closeWithin(suite))
|
|
171
|
+
logger.error("%s", error.message);
|
|
172
|
+
},
|
|
142
173
|
});
|
|
143
174
|
continue;
|
|
144
175
|
}
|
|
145
|
-
|
|
176
|
+
for (const test of chunk.tests) {
|
|
177
|
+
const skip = staticSkip(test);
|
|
178
|
+
if (skip) {
|
|
179
|
+
reporter.onTestSkipped(test, browser, skip);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (signal?.aborted) {
|
|
183
|
+
reporter.onTestStart(test, browser);
|
|
184
|
+
reporter.onTestEnd(test, browser, {
|
|
185
|
+
message: "Run aborted",
|
|
186
|
+
type: "error",
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
await runWithRetries(test, browser, retries, signal, (sink, attempt) => runTest(test, sink, attempt), reporter);
|
|
191
|
+
}
|
|
146
192
|
}
|
|
147
193
|
}
|
|
148
194
|
}
|
|
149
195
|
finally {
|
|
150
196
|
for (const error of await allHooks.closeAll())
|
|
151
197
|
logger.error("%s", error.message);
|
|
198
|
+
for (const error of await workerFixtures.closeAll())
|
|
199
|
+
logger.error("%s", error.message);
|
|
152
200
|
}
|
|
153
201
|
return reporter.onEnd();
|
|
154
202
|
}
|
|
@@ -171,6 +219,7 @@ export default class TestRunner {
|
|
|
171
219
|
dataDir: this.project.dataDir,
|
|
172
220
|
}),
|
|
173
221
|
environment: settings.environment,
|
|
222
|
+
...(settings.baseURL !== undefined && { baseURL: settings.baseURL }),
|
|
174
223
|
timeout: settings.timeout,
|
|
175
224
|
expectTimeout: settings.expectTimeout,
|
|
176
225
|
...(headless !== undefined && { headless }),
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
import type { GroupUse } from "../types/config.js";
|
|
1
2
|
import type { RegisteredTestCase } from "../types/test.js";
|
|
2
|
-
|
|
3
|
+
/** The `use` objects that apply to one test, as written, lowest layer first */
|
|
4
|
+
export interface UseLayers {
|
|
5
|
+
/** Config `use` */
|
|
6
|
+
config: object;
|
|
7
|
+
/** `environments.<name>.use` of the run's environment */
|
|
8
|
+
environment: object;
|
|
9
|
+
/** `use` of the group the test runs for */
|
|
10
|
+
group: object;
|
|
11
|
+
/** The `test.use()` calls around the test, outermost first */
|
|
12
|
+
tests: object[];
|
|
13
|
+
}
|
|
14
|
+
/** The `use` layers of a test: config, then the run's environment, then the group it runs for, then `test.use()` */
|
|
15
|
+
export declare function useLayers(test: RegisteredTestCase): Promise<UseLayers>;
|
|
3
16
|
/**
|
|
4
|
-
* The `use` options one test runs with: config `use`, then the `
|
|
5
|
-
* first, a later one
|
|
17
|
+
* The `use` options one test runs with: config `use`, then the `use` of the run's environment, then the `use` of
|
|
18
|
+
* the group the test runs for (if any), then the `test.use()` calls around the test, outermost first, a later one
|
|
19
|
+
* winning key by key. The fixture keys (`fixtures`, `orderOptions`) are for the fixtures: see `effectiveFixtureUse`.
|
|
6
20
|
*/
|
|
7
|
-
export declare function effectiveUse(test: RegisteredTestCase): Promise<
|
|
21
|
+
export declare function effectiveUse(test: RegisteredTestCase): Promise<GroupUse>;
|
|
@@ -1,18 +1,45 @@
|
|
|
1
1
|
import { readConfig } from "../config/config.js";
|
|
2
|
+
import { omitFixtureUse } from "../config/fixture-use.js";
|
|
3
|
+
import { groupUseOf } from "../config/group-use.js";
|
|
4
|
+
import { peekRunSettings } from "../config/run-settings.js";
|
|
2
5
|
import { mergeUse, useOptionsOf } from "../config/use-options.js";
|
|
3
6
|
import logger from "../logger/index.js";
|
|
4
7
|
import { useOf } from "./suite.js";
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
* first, a later one winning key by key
|
|
8
|
-
*/
|
|
9
|
-
export async function effectiveUse(test) {
|
|
8
|
+
/** The `use` layers of a test: config, then the run's environment, then the group it runs for, then `test.use()` */
|
|
9
|
+
export async function useLayers(test) {
|
|
10
10
|
let config;
|
|
11
|
+
let groups;
|
|
12
|
+
let environments;
|
|
11
13
|
try {
|
|
12
14
|
config = await readConfig("use");
|
|
15
|
+
groups = await readConfig("groups");
|
|
16
|
+
environments = await readConfig("environments");
|
|
13
17
|
}
|
|
14
18
|
catch (err) {
|
|
15
19
|
logger.debug("Could not read use config, using defaults", { err });
|
|
16
20
|
}
|
|
17
|
-
|
|
21
|
+
const environment = peekRunSettings()?.environment;
|
|
22
|
+
return {
|
|
23
|
+
config: config ?? {},
|
|
24
|
+
environment: (environment !== undefined &&
|
|
25
|
+
Object.prototype.hasOwnProperty.call(environments ?? {}, environment) &&
|
|
26
|
+
environments?.[environment]?.use) ||
|
|
27
|
+
{},
|
|
28
|
+
group: test.group === undefined ? {} : groupUseOf(groups, test.group),
|
|
29
|
+
tests: useOf(test.suite),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The `use` options one test runs with: config `use`, then the `use` of the run's environment, then the `use` of
|
|
34
|
+
* the group the test runs for (if any), then the `test.use()` calls around the test, outermost first, a later one
|
|
35
|
+
* winning key by key. The fixture keys (`fixtures`, `orderOptions`) are for the fixtures: see `effectiveFixtureUse`.
|
|
36
|
+
*/
|
|
37
|
+
export async function effectiveUse(test) {
|
|
38
|
+
const layers = await useLayers(test);
|
|
39
|
+
return mergeUse([
|
|
40
|
+
useOptionsOf(layers.config),
|
|
41
|
+
omitFixtureUse(layers.environment),
|
|
42
|
+
omitFixtureUse(layers.group),
|
|
43
|
+
...layers.tests.map((layer) => omitFixtureUse(layer)),
|
|
44
|
+
]);
|
|
18
45
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** What to probe: an `http`/`https` URL, or a local port */
|
|
2
|
+
export type ReadyTarget = {
|
|
3
|
+
url: string;
|
|
4
|
+
ignoreHTTPSErrors?: boolean;
|
|
5
|
+
} | {
|
|
6
|
+
port: number;
|
|
7
|
+
};
|
|
8
|
+
/** One probe of `target` */
|
|
9
|
+
export declare function answers(target: ReadyTarget): Promise<boolean>;
|
|
10
|
+
/** The target as text for messages: the URL, or `localhost:<port>` */
|
|
11
|
+
export declare function describeTarget(target: ReadyTarget): string;
|
|
12
|
+
/**
|
|
13
|
+
* Probes `target` until it answers. Resolves "ready", or "timeout" after `timeout` ms, or "aborted" when `signal`
|
|
14
|
+
* fires or `stopped()` turns true (the caller saw the server die)
|
|
15
|
+
*/
|
|
16
|
+
export declare function waitUntilReady(target: ReadyTarget, options: {
|
|
17
|
+
timeout: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
stopped?: () => boolean;
|
|
20
|
+
}): Promise<"ready" | "timeout" | "aborted">;
|