@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,64 @@
|
|
|
1
|
+
import { FIXTURES_KEY, mergeFixtureUse, pickFixtureUse, } from "../config/fixture-use.js";
|
|
2
|
+
import { peekRunSettings } from "../config/run-settings.js";
|
|
3
|
+
import { useLayers } from "./use-options.js";
|
|
4
|
+
/**
|
|
5
|
+
* The fixtures a test gets from `use`: config, then the run's environment, then the group, then `test.use()`, a
|
|
6
|
+
* later layer winning key by key. The environment's `false` is the exception: it wins over a `true` anywhere, so a
|
|
7
|
+
* fixture an environment turns off cannot be switched back on for it.
|
|
8
|
+
*/
|
|
9
|
+
export async function effectiveFixtureUse(test) {
|
|
10
|
+
const layers = await useLayers(test);
|
|
11
|
+
const merged = mergeFixtureUse([
|
|
12
|
+
layers.config,
|
|
13
|
+
layers.environment,
|
|
14
|
+
layers.group,
|
|
15
|
+
...layers.tests,
|
|
16
|
+
]);
|
|
17
|
+
const environment = peekRunSettings()?.environment ?? "";
|
|
18
|
+
const disabled = new Map();
|
|
19
|
+
for (const [name, on] of Object.entries(mergeFixtureUse([layers.environment]).fixtures))
|
|
20
|
+
if (!on)
|
|
21
|
+
disabled.set(name, `Fixture ${name} is turned off in ${environment}`);
|
|
22
|
+
return {
|
|
23
|
+
attach: Object.entries(merged.fixtures)
|
|
24
|
+
.filter(([, on]) => on)
|
|
25
|
+
.map(([name]) => name),
|
|
26
|
+
options: merged.options,
|
|
27
|
+
disabled,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const list = (names, none) => names.length > 0 ? names.join(", ") : none;
|
|
31
|
+
/** Throws when a `use` names a fixture or an option fixture that `catalog` does not have */
|
|
32
|
+
function checkPlace(place, use, catalog) {
|
|
33
|
+
const fixtures = catalog.map((entry) => entry.name);
|
|
34
|
+
const options = catalog.flatMap((entry) => entry.options?.name ?? []);
|
|
35
|
+
for (const [key, value] of Object.entries(pickFixtureUse(use))) {
|
|
36
|
+
if (key === FIXTURES_KEY) {
|
|
37
|
+
for (const name of Object.keys(value))
|
|
38
|
+
if (!fixtures.includes(name))
|
|
39
|
+
throw new Error(`${place}.fixtures: unknown fixture "${name}". ${fixtures.length > 0 ? `Known fixtures: ${list(fixtures, "")}` : "No fixtures are listed in config.fixtures"}`);
|
|
40
|
+
}
|
|
41
|
+
else if (!options.includes(key))
|
|
42
|
+
throw new Error(`${place}.${key}: unknown option fixture "${key}". ${options.length > 0 ? `Known option fixtures: ${list(options, "")}` : "No option fixtures are listed in config.fixtures"}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Refuses a config whose `use` (config, environments, groups) names a fixture that is not in `config.fixtures` */
|
|
46
|
+
export function checkFixtureUseNames(config, catalog) {
|
|
47
|
+
checkPlace("config.use", config.use, catalog);
|
|
48
|
+
for (const [name, environment] of Object.entries(config.environments ?? {}))
|
|
49
|
+
checkPlace(`config.environments.${name}.use`, environment.use, catalog);
|
|
50
|
+
config.groups?.forEach((group, index) => checkPlace(`config.groups[${index}].use`, group.use, catalog));
|
|
51
|
+
}
|
|
52
|
+
/** Refuses a `test.use()` that names a fixture the test's own `test.extend()` does not define */
|
|
53
|
+
export function checkTestFixtureUse(use, layers) {
|
|
54
|
+
for (const [key, value] of Object.entries(pickFixtureUse(use))) {
|
|
55
|
+
const known = [...(layers?.keys() ?? [])];
|
|
56
|
+
if (key === FIXTURES_KEY) {
|
|
57
|
+
for (const name of Object.keys(value))
|
|
58
|
+
if (!layers?.has(name))
|
|
59
|
+
throw new TypeError(`use(): unknown fixture "${name}". ${known.length > 0 ? `This test has: ${known.join(", ")}` : "This test has no fixtures; write it with test.extend()"}`);
|
|
60
|
+
}
|
|
61
|
+
else if (!layers?.get(key)?.slice(-1)[0]?.option)
|
|
62
|
+
throw new TypeError(`use(): unknown option fixture "${key}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FixtureDef } from "./fixture-defs.js";
|
|
2
|
+
import { type Started } from "./fixture-start.js";
|
|
3
|
+
/** A worker fixture that is set up: its value, how to finish it, and the outputs the report lists */
|
|
4
|
+
export interface WorkerInstance {
|
|
5
|
+
value: unknown;
|
|
6
|
+
started: Started;
|
|
7
|
+
outputs?: Record<string, string | number | boolean> | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The worker-scoped fixtures of one worker. An instance is shared by the tests that need the same definition with
|
|
11
|
+
* the same key (the environment, the resolved options and the instances it uses), is set up on first use and is
|
|
12
|
+
* torn down when the worker is done, last set up first.
|
|
13
|
+
*/
|
|
14
|
+
export declare class WorkerFixtures {
|
|
15
|
+
private readonly limitMs;
|
|
16
|
+
private readonly byDef;
|
|
17
|
+
private readonly live;
|
|
18
|
+
/** `limitMs`: how long a teardown may take when the fixture has no `timeout` of its own */
|
|
19
|
+
constructor(limitMs: number);
|
|
20
|
+
/** The instance of `def` under `key`: the one already set up, else the result of `make`. A failed setup is not kept */
|
|
21
|
+
acquire(def: FixtureDef, key: string, make: () => Promise<WorkerInstance>): Promise<WorkerInstance & {
|
|
22
|
+
reused: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
/** Tears every instance down, last set up first, and returns the errors; a second call has nothing to do */
|
|
25
|
+
closeAll(): Promise<Error[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { finishWithin } from "./fixture-start.js";
|
|
2
|
+
/**
|
|
3
|
+
* The worker-scoped fixtures of one worker. An instance is shared by the tests that need the same definition with
|
|
4
|
+
* the same key (the environment, the resolved options and the instances it uses), is set up on first use and is
|
|
5
|
+
* torn down when the worker is done, last set up first.
|
|
6
|
+
*/
|
|
7
|
+
export class WorkerFixtures {
|
|
8
|
+
/** `limitMs`: how long a teardown may take when the fixture has no `timeout` of its own */
|
|
9
|
+
constructor(limitMs) {
|
|
10
|
+
this.limitMs = limitMs;
|
|
11
|
+
this.byDef = new Map();
|
|
12
|
+
this.live = [];
|
|
13
|
+
}
|
|
14
|
+
/** The instance of `def` under `key`: the one already set up, else the result of `make`. A failed setup is not kept */
|
|
15
|
+
async acquire(def, key, make) {
|
|
16
|
+
let keys = this.byDef.get(def);
|
|
17
|
+
if (!keys)
|
|
18
|
+
this.byDef.set(def, (keys = new Map()));
|
|
19
|
+
const existing = keys.get(key);
|
|
20
|
+
if (existing)
|
|
21
|
+
return { ...(await existing), reused: true };
|
|
22
|
+
const created = make().then((instance) => {
|
|
23
|
+
this.live.push(instance);
|
|
24
|
+
return instance;
|
|
25
|
+
});
|
|
26
|
+
keys.set(key, created);
|
|
27
|
+
try {
|
|
28
|
+
return { ...(await created), reused: false };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
keys.delete(key);
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Tears every instance down, last set up first, and returns the errors; a second call has nothing to do */
|
|
36
|
+
async closeAll() {
|
|
37
|
+
const errors = [];
|
|
38
|
+
this.byDef.clear();
|
|
39
|
+
for (let one = this.live.pop(); one; one = this.live.pop()) {
|
|
40
|
+
const error = await finishWithin(one.started, this.limitMs);
|
|
41
|
+
if (error)
|
|
42
|
+
errors.push(error);
|
|
43
|
+
}
|
|
44
|
+
return errors;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { Browser } from "../browser/browser.js";
|
|
|
2
2
|
import type Page from "../browser/page.js";
|
|
3
3
|
import type { SupportedBrowser } from "../types/browser.js";
|
|
4
4
|
import type { AllHookFixtures, RegisteredTestCase, TestFixtures, TestInfo } from "../types/test.js";
|
|
5
|
+
import type { RequestFixture } from "./request-fixture.js";
|
|
5
6
|
import type { RunnerOptions } from "./options.js";
|
|
6
7
|
import type { PreparedRun } from "./prepare-run.js";
|
|
7
8
|
import type { OutputSink } from "../wait/output-sink.js";
|
|
@@ -20,6 +21,8 @@ export interface FixtureSources {
|
|
|
20
21
|
options: RunnerOptions;
|
|
21
22
|
sink: OutputSink;
|
|
22
23
|
info: TestInfo;
|
|
24
|
+
/** The test's `request` fixture, made on first use */
|
|
25
|
+
request: RequestFixture;
|
|
23
26
|
/** What the `use` options came to in this browser */
|
|
24
27
|
device: {
|
|
25
28
|
isMobile: boolean;
|
|
@@ -27,7 +30,7 @@ export interface FixtureSources {
|
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
|
-
* The fixtures a test body and its each-hooks receive. `context`, `baseURL` and `testInfo` are getters: nothing
|
|
33
|
+
* The fixtures a test body and its each-hooks receive. `context`, `baseURL`, `request` and `testInfo` are getters: nothing
|
|
31
34
|
* is looked up until a test reads one (and `page.context()` throws for a page without one, which only a test
|
|
32
35
|
* that asks for it should see).
|
|
33
36
|
*/
|
package/dist/runner/fixtures.js
CHANGED
|
@@ -12,12 +12,12 @@ export function allHookFixtures(run, browserName, workerIndex = 0) {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* The fixtures a test body and its each-hooks receive. `context`, `baseURL` and `testInfo` are getters: nothing
|
|
15
|
+
* The fixtures a test body and its each-hooks receive. `context`, `baseURL`, `request` and `testInfo` are getters: nothing
|
|
16
16
|
* is looked up until a test reads one (and `page.context()` throws for a page without one, which only a test
|
|
17
17
|
* that asks for it should see).
|
|
18
18
|
*/
|
|
19
19
|
export function buildTestFixtures(from) {
|
|
20
|
-
const { launched, browserName, workerIndex, run, test, options, sink, info, device, } = from;
|
|
20
|
+
const { launched, browserName, workerIndex, run, test, options, sink, info, device, request, } = from;
|
|
21
21
|
const { settings, secrets } = run;
|
|
22
22
|
const fixtures = {
|
|
23
23
|
page: launched.page,
|
|
@@ -34,6 +34,7 @@ export function buildTestFixtures(from) {
|
|
|
34
34
|
return Object.defineProperties(fixtures, {
|
|
35
35
|
context: { enumerable: true, get: () => launched.page.context() },
|
|
36
36
|
baseURL: { enumerable: true, get: () => settings.baseURL },
|
|
37
|
+
request: { enumerable: true, get: () => request.get() },
|
|
37
38
|
testInfo: { enumerable: true, get: () => info },
|
|
38
39
|
});
|
|
39
40
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GlobalHookConfig, SamuraiTestConfig } from "../types/config.js";
|
|
2
|
+
/** The run-level hooks of a config: `setup` before the tests, `teardown` after them */
|
|
3
|
+
export interface GlobalHooks {
|
|
4
|
+
/** Runs `globalSetup`. A throw is `globalSetup(): message` */
|
|
5
|
+
setup(): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Runs what the setup returned, then `globalTeardown`; both even when the setup failed half way. Resolves with the
|
|
8
|
+
* first error as `globalTeardown(): message`, else undefined
|
|
9
|
+
*/
|
|
10
|
+
teardown(): Promise<Error | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/** Binds the config's `globalSetup` / `globalTeardown` to the config they receive and the project folder they load from */
|
|
13
|
+
export declare function globalHooks(config: SamuraiTestConfig, hookConfig: GlobalHookConfig, projectDir: string): GlobalHooks;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
4
|
+
/** The function a hook names: given directly, or the default export of the module at `projectDir`/`hook` */
|
|
5
|
+
async function loadHook(name, hook, projectDir) {
|
|
6
|
+
if (typeof hook === "function")
|
|
7
|
+
return hook;
|
|
8
|
+
const file = path.resolve(projectDir, hook);
|
|
9
|
+
let module;
|
|
10
|
+
try {
|
|
11
|
+
module = (await import(pathToFileURL(file).href));
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
throw new Error(`${name}(): cannot load ${hook}: ${messageOf(error)}`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof module.default !== "function")
|
|
17
|
+
throw new Error(`${name}(): ${hook} must export a function as default`);
|
|
18
|
+
return module.default;
|
|
19
|
+
}
|
|
20
|
+
/** Binds the config's `globalSetup` / `globalTeardown` to the config they receive and the project folder they load from */
|
|
21
|
+
export function globalHooks(config, hookConfig, projectDir) {
|
|
22
|
+
const returned = [];
|
|
23
|
+
return {
|
|
24
|
+
async setup() {
|
|
25
|
+
if (config.globalSetup === undefined)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
const setup = (await loadHook("globalSetup", config.globalSetup, projectDir));
|
|
29
|
+
const teardown = await setup(hookConfig);
|
|
30
|
+
if (typeof teardown === "function")
|
|
31
|
+
returned.push(teardown);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = messageOf(error);
|
|
35
|
+
throw new Error(message.startsWith("globalSetup(): ")
|
|
36
|
+
? message
|
|
37
|
+
: `globalSetup(): ${message}`);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async teardown() {
|
|
41
|
+
let first;
|
|
42
|
+
const record = (error) => {
|
|
43
|
+
const message = messageOf(error);
|
|
44
|
+
first ??= new Error(message.startsWith("globalTeardown(): ")
|
|
45
|
+
? message
|
|
46
|
+
: `globalTeardown(): ${message}`);
|
|
47
|
+
};
|
|
48
|
+
for (const teardown of returned.splice(0).reverse())
|
|
49
|
+
try {
|
|
50
|
+
await teardown();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
record(error);
|
|
54
|
+
}
|
|
55
|
+
if (config.globalTeardown !== undefined)
|
|
56
|
+
try {
|
|
57
|
+
const teardown = (await loadHook("globalTeardown", config.globalTeardown, projectDir));
|
|
58
|
+
await teardown(hookConfig);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
record(error);
|
|
62
|
+
}
|
|
63
|
+
return first;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/runner/options.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
12
|
-
|
|
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
|
|
20
|
-
* run's input values, go
|
|
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
|
|
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
|
|
6
|
-
* run's input values, go
|
|
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
|
|
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
|
|
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.
|
|
52
|
-
|
|
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
|
-
|
|
64
|
+
free++;
|
|
55
65
|
}
|
|
56
66
|
return free + keys.size;
|
|
57
67
|
}
|