@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SupportedBrowser } from "../types/browser.js";
|
|
2
2
|
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { GroupUse } from "../types/config.js";
|
|
4
4
|
import type { BrowserContext } from "./browser-context.js";
|
|
5
5
|
import type Page from "./page.js";
|
|
6
6
|
/** The `use` options a browser applies to its contexts and pages, by what each browser can do (see the wiki's support matrix) */
|
|
@@ -9,7 +9,7 @@ export declare class UseApplier {
|
|
|
9
9
|
private readonly options;
|
|
10
10
|
private readonly state;
|
|
11
11
|
private readonly chrome;
|
|
12
|
-
constructor(connector: BiDiConnector, browserName: SupportedBrowser, options:
|
|
12
|
+
constructor(connector: BiDiConnector, browserName: SupportedBrowser, options: GroupUse, projectDir: string);
|
|
13
13
|
/** What new contexts accept unless they are told otherwise */
|
|
14
14
|
get acceptInsecureCerts(): boolean | undefined;
|
|
15
15
|
/** Whether pages are laid out like a phone: Chrome only */
|
|
@@ -17,11 +17,22 @@ export declare class UseApplier {
|
|
|
17
17
|
/** Whether pages have touch input: Chrome only */
|
|
18
18
|
get hasTouch(): boolean;
|
|
19
19
|
/**
|
|
20
|
-
* Sets up a context:
|
|
21
|
-
*
|
|
20
|
+
* Sets up a context: the run-time options (`page.use()`'s) through BiDi, the extra headers, then the storage state.
|
|
21
|
+
* An option the browser does not support is skipped with a one-time warning (`page.use()` throws instead).
|
|
22
|
+
* Firefox gets JavaScript and the colour scheme when it starts (a profile setting).
|
|
22
23
|
*/
|
|
23
24
|
context(context: BrowserContext): Promise<void>;
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Sets up a page of a context this applier set up: Chrome's phone layout and colour scheme (CDP, so per page), and
|
|
27
|
+
* the viewport a group asked for (after the phone layout, so the size keeps it)
|
|
28
|
+
*/
|
|
25
29
|
page(page: Page): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* The declared options that go through BiDi. Left out: those that are not set (or `null`), the ones that change
|
|
32
|
+
* nothing (`offline: false`), Chrome-only touch on Firefox (warned once in the constructor) and JavaScript on
|
|
33
|
+
* Firefox (a profile setting).
|
|
34
|
+
*/
|
|
35
|
+
private startUp;
|
|
36
|
+
private chromePage;
|
|
26
37
|
private restore;
|
|
27
38
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { warnOnce } from "../logger/warn-once.js";
|
|
2
2
|
import { cdpSend, sendRaw } from "./cdp.js";
|
|
3
|
-
import {
|
|
3
|
+
import { applyUse, RUNTIME_OPTIONS } from "./use-commands.js";
|
|
4
|
+
import { cookiesOf } from "./storage-state.js";
|
|
5
|
+
import { loadStorageState } from "../config/storage-state-file.js";
|
|
4
6
|
/** The `use` options a browser applies to its contexts and pages, by what each browser can do (see the wiki's support matrix) */
|
|
5
7
|
export class UseApplier {
|
|
6
8
|
constructor(connector, browserName, options, projectDir) {
|
|
@@ -30,21 +32,17 @@ export class UseApplier {
|
|
|
30
32
|
return this.chrome && this.options.hasTouch === true;
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
|
-
* Sets up a context:
|
|
34
|
-
*
|
|
35
|
+
* Sets up a context: the run-time options (`page.use()`'s) through BiDi, the extra headers, then the storage state.
|
|
36
|
+
* An option the browser does not support is skipped with a one-time warning (`page.use()` throws instead).
|
|
37
|
+
* Firefox gets JavaScript and the colour scheme when it starts (a profile setting).
|
|
35
38
|
*/
|
|
36
39
|
async context(context) {
|
|
37
40
|
const { options } = this;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
...(this.chrome &&
|
|
44
|
-
options.javaScriptEnabled === false && { javaScriptEnabled: false }),
|
|
45
|
-
};
|
|
46
|
-
if (Object.keys(emulation).length > 0)
|
|
47
|
-
await context.emulate(emulation);
|
|
41
|
+
await applyUse(this.connector, { userContexts: [context.id] }, this.startUp(), (option) => warnOnce(`use.${option}`, `use.${option} is not supported by this browser: it is ignored`));
|
|
42
|
+
context.setRequestDefaults({
|
|
43
|
+
extraHTTPHeaders: options.extraHTTPHeaders,
|
|
44
|
+
ignoreHTTPSErrors: options.acceptInsecureCerts,
|
|
45
|
+
});
|
|
48
46
|
if (options.extraHTTPHeaders)
|
|
49
47
|
await sendRaw(this.connector, "network.setExtraHeaders", {
|
|
50
48
|
headers: Object.entries(options.extraHTTPHeaders).map(([name, value]) => ({ name, value: { type: "string", value } })),
|
|
@@ -53,12 +51,41 @@ export class UseApplier {
|
|
|
53
51
|
if (this.state)
|
|
54
52
|
await this.restore(context, this.state);
|
|
55
53
|
}
|
|
56
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Sets up a page of a context this applier set up: Chrome's phone layout and colour scheme (CDP, so per page), and
|
|
56
|
+
* the viewport a group asked for (after the phone layout, so the size keeps it)
|
|
57
|
+
*/
|
|
57
58
|
async page(page) {
|
|
58
|
-
if (
|
|
59
|
-
|
|
59
|
+
if (this.chrome)
|
|
60
|
+
await this.chromePage(page);
|
|
61
|
+
if (this.options.viewport !== undefined)
|
|
62
|
+
await page.setViewport(this.options.viewport);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The declared options that go through BiDi. Left out: those that are not set (or `null`), the ones that change
|
|
66
|
+
* nothing (`offline: false`), Chrome-only touch on Firefox (warned once in the constructor) and JavaScript on
|
|
67
|
+
* Firefox (a profile setting).
|
|
68
|
+
*/
|
|
69
|
+
startUp() {
|
|
70
|
+
const { options } = this;
|
|
71
|
+
const wanted = {};
|
|
72
|
+
for (const name of RUNTIME_OPTIONS) {
|
|
73
|
+
const value = options[name];
|
|
74
|
+
if (value === undefined || value === null)
|
|
75
|
+
continue;
|
|
76
|
+
if (name === "offline" && value === false)
|
|
77
|
+
continue;
|
|
78
|
+
if (name === "hasTouch" && !this.hasTouch)
|
|
79
|
+
continue;
|
|
80
|
+
if (name === "javaScriptEnabled" && !(this.chrome && value === false))
|
|
81
|
+
continue;
|
|
82
|
+
wanted[name] = value;
|
|
83
|
+
}
|
|
84
|
+
return wanted;
|
|
85
|
+
}
|
|
86
|
+
async chromePage(page) {
|
|
60
87
|
if (this.isMobile)
|
|
61
|
-
await page.
|
|
88
|
+
await page.setMobileLayout();
|
|
62
89
|
if (this.options.colorScheme !== undefined)
|
|
63
90
|
await cdpSend(this.connector, page.contextId, "Emulation.setEmulatedMedia", {
|
|
64
91
|
features: [
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { UnsupportedOperationError } from "../transport/unsupported.js";
|
|
2
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
3
|
+
import type { RuntimeUseOptions } from "../types/use.js";
|
|
4
|
+
export type UseTarget = {
|
|
5
|
+
contexts: string[];
|
|
6
|
+
} | {
|
|
7
|
+
userContexts: string[];
|
|
8
|
+
};
|
|
9
|
+
/** A part of the options could not be applied; what was applied before it stays applied */
|
|
10
|
+
export declare class UseError extends Error {
|
|
11
|
+
/** The options that were applied before the failure, in the order they were applied */
|
|
12
|
+
readonly applied: string[];
|
|
13
|
+
readonly failed: string;
|
|
14
|
+
readonly cause: unknown;
|
|
15
|
+
constructor(failed: string, applied: string[], cause: unknown);
|
|
16
|
+
}
|
|
17
|
+
/** The browser does not implement the command an option needs */
|
|
18
|
+
export declare class UseUnsupportedError extends UnsupportedOperationError {
|
|
19
|
+
readonly option: string;
|
|
20
|
+
readonly applied: string[];
|
|
21
|
+
constructor(option: string, applied: string[]);
|
|
22
|
+
}
|
|
23
|
+
/** The options that can change while the browser runs, in the order they are applied */
|
|
24
|
+
export declare const RUNTIME_OPTIONS: readonly ["locale", "timezone", "userAgent", "geolocation", "offline", "orientation", "screen", "hasTouch", "javaScriptEnabled"];
|
|
25
|
+
/**
|
|
26
|
+
* Checks the options of `page.use()` / `context.use()` / `newContext()`: the values like config `use` does, and
|
|
27
|
+
* only the options that can change at run time. Throws `use(): what is wrong`, before anything is sent.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateRuntimeUse(options: RuntimeUseOptions): void;
|
|
30
|
+
/**
|
|
31
|
+
* Applies the options to `target`, one command after the other, after checking all of them. Nothing is rolled
|
|
32
|
+
* back when one fails: the error says what was applied. An option the browser does not support throws
|
|
33
|
+
* `UseUnsupportedError`, unless `onUnsupported` is given: then it is reported there and skipped (how `use` in
|
|
34
|
+
* config behaves: it warns, `page.use()` throws).
|
|
35
|
+
*/
|
|
36
|
+
export declare function applyUse(connector: BiDiConnector, target: UseTarget, options: RuntimeUseOptions, onUnsupported?: (option: string) => void): Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { isUnsupported, UnsupportedOperationError, } from "../transport/unsupported.js";
|
|
2
|
+
import { USE_OPTION_NAMES, validateUseOptions } from "../config/use-options.js";
|
|
3
|
+
/** A part of the options could not be applied; what was applied before it stays applied */
|
|
4
|
+
export class UseError extends Error {
|
|
5
|
+
constructor(failed, applied, cause) {
|
|
6
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
7
|
+
super(`use(): ${failed} failed${applied.length ? ` after applying ${applied.join(", ")}` : ""}: ${detail}`);
|
|
8
|
+
this.name = "UseError";
|
|
9
|
+
this.failed = failed;
|
|
10
|
+
this.applied = applied;
|
|
11
|
+
this.cause = cause;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** The browser does not implement the command an option needs */
|
|
15
|
+
export class UseUnsupportedError extends UnsupportedOperationError {
|
|
16
|
+
constructor(option, applied) {
|
|
17
|
+
super(`use(): ${option} is not supported by the browser${AT_START_ONLY.has(option) ? ` at run time; set it in config use, a group's use or test.use()` : ""}`);
|
|
18
|
+
this.name = "UseUnsupportedError";
|
|
19
|
+
this.option = option;
|
|
20
|
+
this.applied = applied;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Options Firefox can only take when it starts (a profile setting), so run time is unsupported there */
|
|
24
|
+
const AT_START_ONLY = new Set(["javaScriptEnabled"]);
|
|
25
|
+
/** The options that can change while the browser runs, in the order they are applied */
|
|
26
|
+
export const RUNTIME_OPTIONS = [
|
|
27
|
+
"locale",
|
|
28
|
+
"timezone",
|
|
29
|
+
"userAgent",
|
|
30
|
+
"geolocation",
|
|
31
|
+
"offline",
|
|
32
|
+
"orientation",
|
|
33
|
+
"screen",
|
|
34
|
+
"hasTouch",
|
|
35
|
+
"javaScriptEnabled",
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Checks the options of `page.use()` / `context.use()` / `newContext()`: the values like config `use` does, and
|
|
39
|
+
* only the options that can change at run time. Throws `use(): what is wrong`, before anything is sent.
|
|
40
|
+
*/
|
|
41
|
+
export function validateRuntimeUse(options) {
|
|
42
|
+
validateUseOptions(options);
|
|
43
|
+
for (const key of Object.keys(options))
|
|
44
|
+
if (!RUNTIME_OPTIONS.includes(key))
|
|
45
|
+
throw new TypeError(`use(): ${USE_OPTION_NAMES.includes(key) ? `${key} can only be set before the browser starts: in config use, a group's use or test.use()` : `unknown option "${key}"`}`);
|
|
46
|
+
}
|
|
47
|
+
/** Turns validated options into BiDi commands, in a fixed order */
|
|
48
|
+
function plan(options) {
|
|
49
|
+
const steps = [];
|
|
50
|
+
const add = (option, method, params) => steps.push({ option, method, params });
|
|
51
|
+
if (options.locale !== undefined)
|
|
52
|
+
add("locale", "emulation.setLocaleOverride", { locale: options.locale });
|
|
53
|
+
if (options.timezone !== undefined)
|
|
54
|
+
add("timezone", "emulation.setTimezoneOverride", {
|
|
55
|
+
timezone: options.timezone,
|
|
56
|
+
});
|
|
57
|
+
if (options.userAgent !== undefined)
|
|
58
|
+
add("userAgent", "emulation.setUserAgentOverride", {
|
|
59
|
+
userAgent: options.userAgent,
|
|
60
|
+
});
|
|
61
|
+
if (options.geolocation !== undefined)
|
|
62
|
+
add("geolocation", "emulation.setGeolocationOverride", {
|
|
63
|
+
coordinates: options.geolocation,
|
|
64
|
+
});
|
|
65
|
+
if (options.offline !== undefined)
|
|
66
|
+
add("offline", "emulation.setNetworkConditions", {
|
|
67
|
+
networkConditions: options.offline ? { type: "offline" } : null,
|
|
68
|
+
});
|
|
69
|
+
if (options.orientation !== undefined) {
|
|
70
|
+
const type = options.orientation === null
|
|
71
|
+
? null
|
|
72
|
+
: options.orientation === "portrait" ||
|
|
73
|
+
options.orientation === "landscape"
|
|
74
|
+
? `${options.orientation}-primary`
|
|
75
|
+
: options.orientation;
|
|
76
|
+
add("orientation", "emulation.setScreenOrientationOverride", {
|
|
77
|
+
screenOrientation: type && {
|
|
78
|
+
natural: type.startsWith("portrait") ? "portrait" : "landscape",
|
|
79
|
+
type,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (options.screen !== undefined)
|
|
84
|
+
add("screen", "emulation.setScreenSettingsOverride", {
|
|
85
|
+
screenArea: options.screen,
|
|
86
|
+
});
|
|
87
|
+
if (options.hasTouch !== undefined)
|
|
88
|
+
add("hasTouch", "emulation.setTouchOverride", {
|
|
89
|
+
maxTouchPoints: options.hasTouch ? 1 : null,
|
|
90
|
+
});
|
|
91
|
+
if (options.javaScriptEnabled !== undefined)
|
|
92
|
+
add("javaScriptEnabled", "emulation.setScriptingEnabled", {
|
|
93
|
+
enabled: options.javaScriptEnabled === false ? false : null,
|
|
94
|
+
});
|
|
95
|
+
return steps;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Applies the options to `target`, one command after the other, after checking all of them. Nothing is rolled
|
|
99
|
+
* back when one fails: the error says what was applied. An option the browser does not support throws
|
|
100
|
+
* `UseUnsupportedError`, unless `onUnsupported` is given: then it is reported there and skipped (how `use` in
|
|
101
|
+
* config behaves: it warns, `page.use()` throws).
|
|
102
|
+
*/
|
|
103
|
+
export async function applyUse(connector, target, options, onUnsupported) {
|
|
104
|
+
validateRuntimeUse(options);
|
|
105
|
+
const applied = [];
|
|
106
|
+
for (const step of plan(options)) {
|
|
107
|
+
try {
|
|
108
|
+
await connector.send(step.method, { ...step.params, ...target });
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (!isUnsupported(err))
|
|
112
|
+
throw new UseError(step.option, applied, err);
|
|
113
|
+
if (!onUnsupported)
|
|
114
|
+
throw new UseUnsupportedError(step.option, applied);
|
|
115
|
+
onUnsupported(step.option);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
applied.push(step.option);
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ProjectOptions, type RunnerOptions } from "../runner/run.js";
|
|
1
|
+
import { type ProjectOptions, type RunnerOptions, type Shard } from "../runner/run.js";
|
|
2
2
|
/** `firefox` or `chrome` */
|
|
3
3
|
type SupportedBrowser = NonNullable<RunnerOptions["browsers"]>[number];
|
|
4
4
|
/** The values a command line can override for one run: the environment and the two timeouts */
|
|
@@ -13,8 +13,9 @@ export declare function parseCommandLine(argv: string[]): {
|
|
|
13
13
|
port?: string | undefined;
|
|
14
14
|
workers?: string | undefined;
|
|
15
15
|
retries?: string | undefined;
|
|
16
|
+
shard?: string | undefined;
|
|
16
17
|
grep?: string | undefined;
|
|
17
|
-
group?: string | undefined;
|
|
18
|
+
group?: string[] | undefined;
|
|
18
19
|
browser?: string[] | undefined;
|
|
19
20
|
file?: string[] | undefined;
|
|
20
21
|
json?: boolean | undefined;
|
|
@@ -43,13 +44,16 @@ export interface RunnerFlags {
|
|
|
43
44
|
workers?: string;
|
|
44
45
|
/** `--retries`: a whole number from 0, as typed */
|
|
45
46
|
retries?: string;
|
|
47
|
+
/** `--shard=current/total`, parsed */
|
|
48
|
+
shard?: Shard;
|
|
46
49
|
grep?: string;
|
|
47
|
-
group
|
|
50
|
+
/** `--group`: one name as typed, or the list when it is repeated */
|
|
51
|
+
group?: string | string[];
|
|
48
52
|
files?: string[];
|
|
49
53
|
/** `--browser`, repeated for more than one; replaces the config's `browsers` for this run */
|
|
50
54
|
browsers?: SupportedBrowser[];
|
|
51
55
|
}
|
|
52
|
-
/** Reads `--headless`, `--port <n>`, `--workers <n|n%>`, `--retries <n>`, `--grep <text>`, `--group <name>`, `--file <spec>` and `--browser <name>` (
|
|
56
|
+
/** Reads `--headless`, `--port <n>`, `--workers <n|n%>`, `--retries <n>`, `--shard <current/total>`, `--grep <text>`, `--group <name>`, `--file <spec>` and `--browser <name>` (all three repeatable) from `argv`, or from values that were parsed already */
|
|
53
57
|
export declare function parseRunnerFlags(source: string[] | CommandValues): RunnerFlags;
|
|
54
58
|
/** Reads `--env`, `--timeout` and `--expect-timeout` from `argv` (or values parsed already); the environment falls back to SAMURAI_ENV */
|
|
55
59
|
export declare function parseRunOverrides(source: string[] | CommandValues, env?: Partial<NodeJS.ProcessEnv>): EnvAndTimeouts;
|
package/dist/cli/args.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { checkBrowserList, parseRetries, parseWorkers, } from "../runner/run.js";
|
|
2
|
+
import { checkBrowserList, parseRetries, parseShard, parseWorkers, } from "../runner/run.js";
|
|
3
3
|
const FLAGS = {
|
|
4
4
|
env: { type: "string" },
|
|
5
5
|
timeout: { type: "string" },
|
|
@@ -8,8 +8,9 @@ const FLAGS = {
|
|
|
8
8
|
port: { type: "string" },
|
|
9
9
|
workers: { type: "string" },
|
|
10
10
|
retries: { type: "string" },
|
|
11
|
+
shard: { type: "string" },
|
|
11
12
|
grep: { type: "string" },
|
|
12
|
-
group: { type: "string" },
|
|
13
|
+
group: { type: "string", multiple: true },
|
|
13
14
|
browser: { type: "string", multiple: true },
|
|
14
15
|
file: { type: "string", multiple: true },
|
|
15
16
|
json: { type: "boolean" },
|
|
@@ -35,7 +36,7 @@ export function parseCommandLine(argv) {
|
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
const valuesOf = (source) => Array.isArray(source) ? parseCommandLine(source).values : source;
|
|
38
|
-
/** Reads `--headless`, `--port <n>`, `--workers <n|n%>`, `--retries <n>`, `--grep <text>`, `--group <name>`, `--file <spec>` and `--browser <name>` (
|
|
39
|
+
/** Reads `--headless`, `--port <n>`, `--workers <n|n%>`, `--retries <n>`, `--shard <current/total>`, `--grep <text>`, `--group <name>`, `--file <spec>` and `--browser <name>` (all three repeatable) from `argv`, or from values that were parsed already */
|
|
39
40
|
export function parseRunnerFlags(source) {
|
|
40
41
|
const values = valuesOf(source);
|
|
41
42
|
const flags = {};
|
|
@@ -56,10 +57,12 @@ export function parseRunnerFlags(source) {
|
|
|
56
57
|
parseRetries(values.retries, "--retries");
|
|
57
58
|
flags.retries = values.retries;
|
|
58
59
|
}
|
|
60
|
+
if (values.shard !== undefined)
|
|
61
|
+
flags.shard = parseShard(values.shard, "--shard");
|
|
59
62
|
if (values.grep !== undefined)
|
|
60
63
|
flags.grep = values.grep;
|
|
61
|
-
if (values.group
|
|
62
|
-
flags.group = values.group;
|
|
64
|
+
if (values.group?.length)
|
|
65
|
+
flags.group = values.group.length === 1 ? values.group[0] : values.group;
|
|
63
66
|
if (values.file?.length)
|
|
64
67
|
flags.files = values.file;
|
|
65
68
|
if (values.browser?.length) {
|
|
@@ -20,7 +20,7 @@ export class ConsoleReporter {
|
|
|
20
20
|
switch (event.type) {
|
|
21
21
|
case "run-start":
|
|
22
22
|
this.labelled = event.browsers.length > 1;
|
|
23
|
-
this.write(`Running ${event.total} ${event.total === 1 ? "test" : "tests"}${event.workers ? ` with ${event.workers} workers` : ""} against ${event.environment}${this.labelled ? ` in ${event.browsers.join(", ")}` : ""}\n`);
|
|
23
|
+
this.write(`Running ${event.total} ${event.total === 1 ? "test" : "tests"}${event.shard ? ` (shard ${event.shard.current}/${event.shard.total})` : ""}${event.workers ? ` with ${event.workers} workers` : ""} against ${event.environment}${this.labelled ? ` in ${event.browsers.join(", ")}` : ""}\n`);
|
|
24
24
|
return;
|
|
25
25
|
case "test-end":
|
|
26
26
|
this.write(` ${this.line(event.name, event.result, event.browser)}\n`);
|
|
@@ -47,6 +47,10 @@ export class ConsoleReporter {
|
|
|
47
47
|
: `${this.paint(31, "✖")} ${name}${this.tag(browser)}${took}`;
|
|
48
48
|
}
|
|
49
49
|
summary(summary) {
|
|
50
|
+
if (summary.shard && summary.tests.length === 0) {
|
|
51
|
+
this.write(`\n${this.paint(90, `Shard ${summary.shard.current}/${summary.shard.total} has no tests to run`)}\n`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
50
54
|
const skipped = summary.tests.filter((test) => test.status === "skipped");
|
|
51
55
|
const failed = summary.tests.filter((test) => test.status !== "success" && test.status !== "skipped");
|
|
52
56
|
for (const test of failed) {
|
package/dist/cli/main.js
CHANGED
|
@@ -49,6 +49,7 @@ export async function main(argv, io) {
|
|
|
49
49
|
...(flags.port !== undefined && { port: flags.port }),
|
|
50
50
|
...(flags.workers !== undefined && { workers: flags.workers }),
|
|
51
51
|
...(flags.retries !== undefined && { retries: flags.retries }),
|
|
52
|
+
...(flags.shard && { shard: flags.shard }),
|
|
52
53
|
...(flags.grep !== undefined && { grep: flags.grep }),
|
|
53
54
|
...(flags.group !== undefined && { group: flags.group }),
|
|
54
55
|
...(flags.files && { files: flags.files }),
|
package/dist/cli/usage.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n samurai flow <command> List, check and run the flows in ./flows (see below)\n samurai browsers [command] List, install and remove the browsers SAMURAI launches\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --group <name> Only the tests of this group of the config's groups\n --browser <name> Run in this browser (firefox or chrome) instead of the config's\n browsers; repeat it for more than one\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one.\n Default: the config's use.headless, else headless when CI is set\n --port <n> Browser debugging port (default 9223)\n --workers <n|n%> Tests that run at the same time, each in a worker process: a number\n from 1, or a share of the CPUs like 50%. Default: the config's\n workers (or SAMURAI_WORKERS), else half the CPUs\n --retries <n> Run a failing test again up to n times, each with a fresh browser\n (or SAMURAI_RETRIES; default 0)\n\nOptions for record (also --env, --port, --json, --headless, --no-headless):\n --browser <name> Record in this browser (default: the first of the config's browsers)\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.\n\nsamurai flow:\n list The flows in ./flows\n check [<flow>...] Check flows without running them\n run <flow>... | --all Run flows; each writes result/flows/<id>.json\n Options for run: --env, --headless, --timeout, --expect-timeout, --port, --json, --allow-unsupported\n Run `samurai flow` alone for the details.\n\nsamurai browsers:\n list The pinned browsers and every build in the cache (the default)\n install [firefox|chrome|all]\n Download the pinned builds (default: all)\n uninstall <name> [--build <id>] | --unused\n\nRun it in the project folder, the one holding samurai.config.ts.\n";
|
|
1
|
+
export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n samurai flow <command> List, check and run the flows in ./flows (see below)\n samurai browsers [command] List, install and remove the browsers SAMURAI launches\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --group <name> Only the tests of this group of the config's groups; repeat it for several\n --browser <name> Run in this browser (firefox or chrome) instead of the config's\n browsers; repeat it for more than one\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one.\n Default: the config's use.headless, else headless when CI is set\n --port <n> Browser debugging port (default 9223)\n --workers <n|n%> Tests that run at the same time, each in a worker process: a number\n from 1, or a share of the CPUs like 50%. Default: the config's\n workers (or SAMURAI_WORKERS), else half the CPUs\n --retries <n> Run a failing test again up to n times, each with a fresh browser\n (or SAMURAI_RETRIES; default 0)\n --shard <current/total> Run one slice of the selected tests, to split a run over machines:\n --shard=2/4 runs the second of four. Whole files (and serial\n blocks) go to one shard; an empty shard succeeds\n\nOptions for record (also --env, --port, --json, --headless, --no-headless):\n --browser <name> Record in this browser (default: the first of the config's browsers)\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.\n\nsamurai flow:\n list The flows in ./flows\n check [<flow>...] Check flows without running them\n run <flow>... | --all Run flows; each writes result/flows/<id>.json\n Options for run: --env, --headless, --timeout, --expect-timeout, --port, --json, --allow-unsupported\n Run `samurai flow` alone for the details.\n\nsamurai browsers:\n list The pinned browsers and every build in the cache (the default)\n install [firefox|chrome|all]\n Download the pinned builds (default: all)\n uninstall <name> [--build <id>] | --unused\n\nRun it in the project folder, the one holding samurai.config.ts.\n";
|
package/dist/cli/usage.js
CHANGED
|
@@ -13,7 +13,7 @@ Options for run and list:
|
|
|
13
13
|
--env <name> Environment to run against (or SAMURAI_ENV)
|
|
14
14
|
--file <spec> Only this spec file; repeatable
|
|
15
15
|
--grep <text> Only tests whose full name contains the text
|
|
16
|
-
--group <name> Only the tests of this group of the config's groups
|
|
16
|
+
--group <name> Only the tests of this group of the config's groups; repeat it for several
|
|
17
17
|
--browser <name> Run in this browser (firefox or chrome) instead of the config's
|
|
18
18
|
browsers; repeat it for more than one
|
|
19
19
|
--json Machine-readable output: JSON lines for run, one array for list
|
|
@@ -29,6 +29,9 @@ Options for run:
|
|
|
29
29
|
workers (or SAMURAI_WORKERS), else half the CPUs
|
|
30
30
|
--retries <n> Run a failing test again up to n times, each with a fresh browser
|
|
31
31
|
(or SAMURAI_RETRIES; default 0)
|
|
32
|
+
--shard <current/total> Run one slice of the selected tests, to split a run over machines:
|
|
33
|
+
--shard=2/4 runs the second of four. Whole files (and serial
|
|
34
|
+
blocks) go to one shard; an empty shard succeeds
|
|
32
35
|
|
|
33
36
|
Options for record (also --env, --port, --json, --headless, --no-headless):
|
|
34
37
|
--browser <name> Record in this browser (default: the first of the config's browsers)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** A URL starting with "/", "./" or "../" */
|
|
2
|
+
export const RELATIVE_URL = /^\.{0,2}\//;
|
|
3
|
+
/** Resolves a slash-led `url` against `base`; any other URL is returned as given */
|
|
4
|
+
export function resolveRelativeURL(url, base) {
|
|
5
|
+
return RELATIVE_URL.test(url) ? new URL(url, base).href : url;
|
|
6
|
+
}
|
package/dist/config/config.js
CHANGED
|
@@ -2,10 +2,13 @@ import path from "path";
|
|
|
2
2
|
import logger from "../logger/index.js";
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
4
|
import { validateBrowserConfig } from "./browsers.js";
|
|
5
|
+
import { validateEnvironmentUse, validateGroupUse } from "./group-use.js";
|
|
5
6
|
import { validateUseConfig } from "./use-options.js";
|
|
6
7
|
import { validateForbidOnlyConfig } from "./forbid-only.js";
|
|
7
8
|
import { validateRetriesConfig } from "./retries.js";
|
|
9
|
+
import { validateFullyParallelConfig } from "./fully-parallel.js";
|
|
8
10
|
import { validateWorkersConfig } from "./workers.js";
|
|
11
|
+
import { validateWebServerConfig } from "./web-server-options.js";
|
|
9
12
|
let activeProject;
|
|
10
13
|
/** Makes `project` the one config, secrets, reports and downloads are resolved for; `undefined` ends it */
|
|
11
14
|
export function setActiveProject(project) {
|
|
@@ -31,7 +34,11 @@ export function validateConfig(config) {
|
|
|
31
34
|
validateWorkersConfig(config);
|
|
32
35
|
validateForbidOnlyConfig(config);
|
|
33
36
|
validateRetriesConfig(config);
|
|
37
|
+
validateFullyParallelConfig(config);
|
|
34
38
|
validateUseConfig(config);
|
|
39
|
+
validateGroupUse(config);
|
|
40
|
+
validateEnvironmentUse(config);
|
|
41
|
+
validateWebServerConfig(config);
|
|
35
42
|
}
|
|
36
43
|
/** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
|
|
37
44
|
export async function loadRawConfig() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The key of `use` that attaches fixtures by name (`true`) or removes them (`false`) */
|
|
2
|
+
export declare const FIXTURES_KEY = "fixtures";
|
|
3
|
+
/** Whether `key` of a `use` object is about fixtures: `fixtures`, or an option fixture (`orderOptions`) */
|
|
4
|
+
export declare function isFixtureUseKey(key: string): boolean;
|
|
5
|
+
/** The fixture keys of one `use` layer */
|
|
6
|
+
export declare function pickFixtureUse(layer: object | undefined): Record<string, unknown>;
|
|
7
|
+
/** The layer without its fixture keys: what is left is for the browser */
|
|
8
|
+
export declare function omitFixtureUse<T extends object>(layer: T | undefined): T;
|
|
9
|
+
/** Checks the fixture keys of one `use` layer; throws `use(): what is wrong` */
|
|
10
|
+
export declare function checkFixtureUse(options: Record<string, unknown>): void;
|
|
11
|
+
/** What the fixture keys of the layers come to, later layers winning key by key */
|
|
12
|
+
export interface MergedFixtureUse {
|
|
13
|
+
/** Fixture name to attached (`true`) or removed (`false`) */
|
|
14
|
+
fixtures: Record<string, boolean>;
|
|
15
|
+
/** Option fixture name to its values from `use`, over the fixture's own default */
|
|
16
|
+
options: Record<string, Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
/** Merges the fixture keys of the `use` layers in order (config, environment, group, `test.use()`) */
|
|
19
|
+
export declare function mergeFixtureUse(layers: readonly object[]): MergedFixtureUse;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The key of `use` that attaches fixtures by name (`true`) or removes them (`false`) */
|
|
2
|
+
export const FIXTURES_KEY = "fixtures";
|
|
3
|
+
const OPTION_FIXTURE_KEY = /^[a-z][A-Za-z0-9]*Options$/;
|
|
4
|
+
/** Whether `key` of a `use` object is about fixtures: `fixtures`, or an option fixture (`orderOptions`) */
|
|
5
|
+
export function isFixtureUseKey(key) {
|
|
6
|
+
return key === FIXTURES_KEY || OPTION_FIXTURE_KEY.test(key);
|
|
7
|
+
}
|
|
8
|
+
/** The fixture keys of one `use` layer */
|
|
9
|
+
export function pickFixtureUse(layer) {
|
|
10
|
+
return Object.fromEntries(Object.entries(layer ?? {}).filter(([key]) => isFixtureUseKey(key)));
|
|
11
|
+
}
|
|
12
|
+
/** The layer without its fixture keys: what is left is for the browser */
|
|
13
|
+
export function omitFixtureUse(layer) {
|
|
14
|
+
return Object.fromEntries(Object.entries(layer ?? {}).filter(([key]) => !isFixtureUseKey(key)));
|
|
15
|
+
}
|
|
16
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
/** Checks the fixture keys of one `use` layer; throws `use(): what is wrong` */
|
|
18
|
+
export function checkFixtureUse(options) {
|
|
19
|
+
const attached = options[FIXTURES_KEY];
|
|
20
|
+
if (attached !== undefined) {
|
|
21
|
+
if (!isObject(attached))
|
|
22
|
+
throw new TypeError("use(): fixtures must be an object of fixture names and true or false");
|
|
23
|
+
for (const [name, on] of Object.entries(attached))
|
|
24
|
+
if (typeof on !== "boolean")
|
|
25
|
+
throw new TypeError(`use(): fixtures.${name} must be true or false, got ${JSON.stringify(on)}`);
|
|
26
|
+
}
|
|
27
|
+
for (const [key, value] of Object.entries(options))
|
|
28
|
+
if (key !== FIXTURES_KEY && isFixtureUseKey(key) && !isObject(value))
|
|
29
|
+
throw new TypeError(`use(): ${key} must be an object`);
|
|
30
|
+
}
|
|
31
|
+
/** Merges the fixture keys of the `use` layers in order (config, environment, group, `test.use()`) */
|
|
32
|
+
export function mergeFixtureUse(layers) {
|
|
33
|
+
const merged = { fixtures: {}, options: {} };
|
|
34
|
+
for (const layer of layers)
|
|
35
|
+
for (const [key, value] of Object.entries(pickFixtureUse(layer)))
|
|
36
|
+
if (key === FIXTURES_KEY)
|
|
37
|
+
Object.assign(merged.fixtures, value);
|
|
38
|
+
else
|
|
39
|
+
merged.options[key] = {
|
|
40
|
+
...merged.options[key],
|
|
41
|
+
...value,
|
|
42
|
+
};
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Checks the `fullyParallel` key of a config. Throws an Error that names the key */
|
|
2
|
+
export function validateFullyParallelConfig(config) {
|
|
3
|
+
if (config.fullyParallel !== undefined &&
|
|
4
|
+
typeof config.fullyParallel !== "boolean")
|
|
5
|
+
throw new Error(`fullyParallel must be true or false, got ${JSON.stringify(config.fullyParallel)}`);
|
|
6
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GroupUse, SamuraiGroup, SamuraiTestConfig } from "../types/config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Checks the `use` of every group of the config: the options of config `use` (`test.use()` ones, and `viewport`),
|
|
4
|
+
* and the fixture keys. Throws `config.groups[1].use.locale: what is wrong`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateGroupUse(config: SamuraiTestConfig): void;
|
|
7
|
+
/** Checks the `use` of every environment the same way: `config.environments.production.use.locale: what is wrong` */
|
|
8
|
+
export declare function validateEnvironmentUse(config: SamuraiTestConfig): void;
|
|
9
|
+
/** The `use` of the group called `name`; nothing when there is no such group or it has none */
|
|
10
|
+
export declare function groupUseOf(groups: readonly SamuraiGroup[] | undefined, name: string): GroupUse;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isFixtureUseKey } from "./fixture-use.js";
|
|
2
|
+
import { USE_OPTION_NAMES, validateUseOptions } from "./use-options.js";
|
|
3
|
+
const isSize = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
4
|
+
/** Rewrites `use(): locale must ...` as `config.groups[1].use.locale: locale must ...` */
|
|
5
|
+
function atGroup(place, err) {
|
|
6
|
+
if (!(err instanceof TypeError))
|
|
7
|
+
throw err;
|
|
8
|
+
const problem = err.message.replace(/^use\(\): /, "");
|
|
9
|
+
const name = /^(?:invalid |unknown )?(\w+)/.exec(problem)?.[1];
|
|
10
|
+
const known = name !== undefined &&
|
|
11
|
+
(name === "viewport" ||
|
|
12
|
+
USE_OPTION_NAMES.includes(name) ||
|
|
13
|
+
isFixtureUseKey(name));
|
|
14
|
+
throw new TypeError(`${place}${known ? `.${name}` : ""}: ${problem}`);
|
|
15
|
+
}
|
|
16
|
+
/** Checks one `use` of a group or an environment; throws `<place>.locale: what is wrong` */
|
|
17
|
+
function validateUseAt(place, use) {
|
|
18
|
+
try {
|
|
19
|
+
const isObject = typeof use === "object" && use !== null && !Array.isArray(use);
|
|
20
|
+
const { viewport, ...options } = (isObject ? use : {});
|
|
21
|
+
// Anything but an object is refused here, with the message `test.use()` gives
|
|
22
|
+
validateUseOptions(isObject ? options : use);
|
|
23
|
+
if (viewport !== undefined &&
|
|
24
|
+
viewport !== null &&
|
|
25
|
+
!(isSize(viewport.width) && isSize(viewport.height)))
|
|
26
|
+
throw new TypeError(`viewport must be { width, height } with whole numbers above 0, or null, got ${JSON.stringify(viewport)}`);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
atGroup(place, err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Checks the `use` of every group of the config: the options of config `use` (`test.use()` ones, and `viewport`),
|
|
34
|
+
* and the fixture keys. Throws `config.groups[1].use.locale: what is wrong`.
|
|
35
|
+
*/
|
|
36
|
+
export function validateGroupUse(config) {
|
|
37
|
+
config.groups?.forEach((group, index) => {
|
|
38
|
+
if (group.use !== undefined)
|
|
39
|
+
validateUseAt(`config.groups[${index}].use`, group.use);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Checks the `use` of every environment the same way: `config.environments.production.use.locale: what is wrong` */
|
|
43
|
+
export function validateEnvironmentUse(config) {
|
|
44
|
+
for (const [name, environment] of Object.entries(config.environments ?? {}))
|
|
45
|
+
if (environment?.use !== undefined)
|
|
46
|
+
validateUseAt(`config.environments.${name}.use`, environment.use);
|
|
47
|
+
}
|
|
48
|
+
/** The `use` of the group called `name`; nothing when there is no such group or it has none */
|
|
49
|
+
export function groupUseOf(groups, name) {
|
|
50
|
+
return groups?.find((group) => group.name === name)?.use ?? {};
|
|
51
|
+
}
|
|
@@ -45,6 +45,8 @@ export interface ResolvedConfig {
|
|
|
45
45
|
workers?: number | string;
|
|
46
46
|
/** The workers a run without `--workers` and `SAMURAI_WORKERS` uses on this machine */
|
|
47
47
|
workersResolved: number;
|
|
48
|
+
/** The config's `fullyParallel` as written; absent when not set */
|
|
49
|
+
fullyParallel?: boolean;
|
|
48
50
|
retries: number;
|
|
49
51
|
forbidOnly: boolean;
|
|
50
52
|
headless: boolean;
|
|
@@ -62,6 +62,9 @@ export function resolveConfig(config, options) {
|
|
|
62
62
|
bidiCommandTimeout: config.bidi?.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT,
|
|
63
63
|
...(config.workers !== undefined && { workers: config.workers }),
|
|
64
64
|
workersResolved: resolveWorkers({ config: config.workers }),
|
|
65
|
+
...(config.fullyParallel !== undefined && {
|
|
66
|
+
fullyParallel: config.fullyParallel,
|
|
67
|
+
}),
|
|
65
68
|
// Matches the runner's `resolveRetries` without the command line flag and the environment (config/ cannot import runner/)
|
|
66
69
|
retries: config.retries ?? DEFAULT_RETRIES,
|
|
67
70
|
forbidOnly: forbidsOnly(config, env),
|