@itmetsam/samurai-framework 0.21.4 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +8 -1
- 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 +11 -2
- package/dist/browser/browser.js +31 -8
- package/dist/browser/cdp.d.ts +11 -0
- package/dist/browser/cdp.js +28 -0
- package/dist/browser/chrome-launch.d.ts +4 -0
- package/dist/browser/chrome-launch.js +11 -3
- 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 +8 -1
- package/dist/browser/index.js +5 -1
- package/dist/browser/keyboard.d.ts +35 -0
- package/dist/browser/keyboard.js +83 -0
- package/dist/browser/launch-firefox.js +1 -1
- package/dist/browser/launch-flags.d.ts +4 -1
- package/dist/browser/launch-flags.js +18 -5
- package/dist/browser/mouse.d.ts +42 -0
- package/dist/browser/mouse.js +66 -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.d.ts +20 -6
- package/dist/browser/page.js +24 -8
- package/dist/browser/session-capabilities.d.ts +10 -0
- package/dist/browser/session-capabilities.js +11 -0
- package/dist/browser/session.d.ts +2 -1
- package/dist/browser/session.js +3 -3
- package/dist/browser/storage-state.d.ts +4 -0
- package/dist/browser/storage-state.js +17 -0
- package/dist/browser/use-applier.d.ts +38 -0
- package/dist/browser/use-applier.js +121 -0
- package/dist/browser/use-commands.d.ts +36 -0
- package/dist/browser/use-commands.js +120 -0
- package/dist/browser/viewport.d.ts +3 -2
- package/dist/browser/viewport.js +15 -2
- 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 +8 -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 +8 -0
- package/dist/config/group-use.js +41 -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 +18 -0
- package/dist/config/use-options.js +184 -0
- 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/logger/warn-once.d.ts +4 -0
- package/dist/logger/warn-once.js +13 -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/attachments.d.ts +16 -0
- package/dist/runner/attachments.js +60 -0
- package/dist/runner/fixture-args.d.ts +15 -0
- package/dist/runner/fixture-args.js +95 -0
- package/dist/runner/fixture-defs.d.ts +22 -0
- package/dist/runner/fixture-defs.js +104 -0
- package/dist/runner/fixture-scope.d.ts +33 -0
- package/dist/runner/fixture-scope.js +156 -0
- package/dist/runner/fixtures.d.ts +37 -0
- package/dist/runner/fixtures.js +40 -0
- package/dist/runner/global-hooks.d.ts +13 -0
- package/dist/runner/global-hooks.js +66 -0
- package/dist/runner/hooks.d.ts +6 -1
- package/dist/runner/hooks.js +15 -3
- 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/registry.d.ts +3 -39
- package/dist/runner/registry.js +99 -21
- package/dist/runner/reporter.d.ts +17 -1
- package/dist/runner/reporter.js +15 -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 +1 -3
- package/dist/runner/run-test.js +98 -35
- package/dist/runner/run.d.ts +1 -0
- package/dist/runner/run.js +1 -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/steps.d.ts +13 -0
- package/dist/runner/steps.js +67 -0
- package/dist/runner/suite.d.ts +8 -3
- package/dist/runner/suite.js +12 -3
- package/dist/runner/test-annotations.d.ts +16 -0
- package/dist/runner/test-annotations.js +47 -0
- package/dist/runner/test-function.d.ts +115 -0
- package/dist/runner/test-function.js +1 -0
- package/dist/runner/test-info.d.ts +104 -0
- package/dist/runner/test-info.js +170 -0
- package/dist/runner/test-runner.d.ts +1 -0
- package/dist/runner/test-runner.js +64 -19
- package/dist/runner/test-timeout.d.ts +22 -0
- package/dist/runner/test-timeout.js +45 -0
- package/dist/runner/use-options.d.ts +7 -0
- package/dist/runner/use-options.js +25 -0
- 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 +5 -2
- package/dist/runner/worker.d.ts +2 -0
- package/dist/runner/worker.js +7 -1
- package/dist/script/element-handle.d.ts +1 -1
- package/dist/script/element-handle.js +1 -1
- package/dist/types/config.d.ts +86 -2
- package/dist/types/extend.d.ts +30 -0
- package/dist/types/test.d.ts +138 -0
- package/dist/types/use.d.ts +85 -0
- package/package.json +1 -1
- package/dist/browser/emulation.d.ts +0 -61
- package/dist/browser/emulation.js +0 -180
|
@@ -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
|
+
}
|
|
@@ -9,6 +9,7 @@ export interface Viewport {
|
|
|
9
9
|
export declare function validateViewport({ width, height, devicePixelRatio, }: Viewport): void;
|
|
10
10
|
/**
|
|
11
11
|
* Gives the context this size (and optionally this device pixel ratio); `null` puts the browser's own size back
|
|
12
|
-
* and waits until the page has really got it.
|
|
12
|
+
* and waits until the page has really got it. A `mobile` page (Chrome's phone layout, `use.isMobile`) is sized
|
|
13
|
+
* through CDP, as BiDi's `setViewport` would turn the phone layout off again.
|
|
13
14
|
*/
|
|
14
|
-
export declare function applyViewport(connector: BiDiConnector, context: string, viewport: Viewport | null): Promise<void>;
|
|
15
|
+
export declare function applyViewport(connector: BiDiConnector, context: string, viewport: Viewport | null, mobile?: boolean): Promise<void>;
|
package/dist/browser/viewport.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { callFunction } from "../script/call-function.js";
|
|
2
|
+
import { cdpSend } from "./cdp.js";
|
|
2
3
|
/** Time (ms) without a `resize` event after which a viewport reset counts as done */
|
|
3
4
|
const RESIZE_QUIET_TIME = 50;
|
|
4
5
|
/** Time (ms) a viewport reset waits for a `resize` event that may never come (the size did not change) */
|
|
@@ -20,9 +21,21 @@ export function validateViewport({ width, height, devicePixelRatio, }) {
|
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* Gives the context this size (and optionally this device pixel ratio); `null` puts the browser's own size back
|
|
23
|
-
* and waits until the page has really got it.
|
|
24
|
+
* and waits until the page has really got it. A `mobile` page (Chrome's phone layout, `use.isMobile`) is sized
|
|
25
|
+
* through CDP, as BiDi's `setViewport` would turn the phone layout off again.
|
|
24
26
|
*/
|
|
25
|
-
export async function applyViewport(connector, context, viewport) {
|
|
27
|
+
export async function applyViewport(connector, context, viewport, mobile = false) {
|
|
28
|
+
if (mobile) {
|
|
29
|
+
if (viewport)
|
|
30
|
+
validateViewport(viewport);
|
|
31
|
+
await cdpSend(connector, context, "Emulation.setDeviceMetricsOverride", {
|
|
32
|
+
width: viewport?.width ?? 0,
|
|
33
|
+
height: viewport?.height ?? 0,
|
|
34
|
+
deviceScaleFactor: viewport ? (viewport.devicePixelRatio ?? 1) : 0,
|
|
35
|
+
mobile: true,
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
26
39
|
if (viewport === null) {
|
|
27
40
|
await untilResized(connector, context, () => connector.send("browsingContext.setViewport", {
|
|
28
41
|
context,
|
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,9 +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 { validateGroupUse } from "./group-use.js";
|
|
6
|
+
import { validateUseConfig } from "./use-options.js";
|
|
5
7
|
import { validateForbidOnlyConfig } from "./forbid-only.js";
|
|
6
8
|
import { validateRetriesConfig } from "./retries.js";
|
|
9
|
+
import { validateFullyParallelConfig } from "./fully-parallel.js";
|
|
7
10
|
import { validateWorkersConfig } from "./workers.js";
|
|
11
|
+
import { validateWebServerConfig } from "./web-server-options.js";
|
|
8
12
|
let activeProject;
|
|
9
13
|
/** Makes `project` the one config, secrets, reports and downloads are resolved for; `undefined` ends it */
|
|
10
14
|
export function setActiveProject(project) {
|
|
@@ -30,6 +34,10 @@ export function validateConfig(config) {
|
|
|
30
34
|
validateWorkersConfig(config);
|
|
31
35
|
validateForbidOnlyConfig(config);
|
|
32
36
|
validateRetriesConfig(config);
|
|
37
|
+
validateFullyParallelConfig(config);
|
|
38
|
+
validateUseConfig(config);
|
|
39
|
+
validateGroupUse(config);
|
|
40
|
+
validateWebServerConfig(config);
|
|
33
41
|
}
|
|
34
42
|
/** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
|
|
35
43
|
export async function loadRawConfig() {
|
|
@@ -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,8 @@
|
|
|
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
|
+
* Throws `config.groups[1].use.locale: what is wrong`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateGroupUse(config: SamuraiTestConfig): void;
|
|
7
|
+
/** The `use` of the group called `name`; nothing when there is no such group or it has none */
|
|
8
|
+
export declare function groupUseOf(groups: readonly SamuraiGroup[] | undefined, name: string): GroupUse;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { USE_OPTION_NAMES, validateUseOptions } from "./use-options.js";
|
|
2
|
+
const isSize = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
3
|
+
/** Rewrites `use(): locale must ...` as `config.groups[1].use.locale: locale must ...` */
|
|
4
|
+
function atGroup(place, err) {
|
|
5
|
+
if (!(err instanceof TypeError))
|
|
6
|
+
throw err;
|
|
7
|
+
const problem = err.message.replace(/^use\(\): /, "");
|
|
8
|
+
const name = /^(?:invalid |unknown )?(\w+)/.exec(problem)?.[1];
|
|
9
|
+
const known = name !== undefined &&
|
|
10
|
+
(name === "viewport" || USE_OPTION_NAMES.includes(name));
|
|
11
|
+
throw new TypeError(`${place}${known ? `.${name}` : ""}: ${problem}`);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Checks the `use` of every group of the config: the options of config `use` (`test.use()` ones, and `viewport`).
|
|
15
|
+
* Throws `config.groups[1].use.locale: what is wrong`.
|
|
16
|
+
*/
|
|
17
|
+
export function validateGroupUse(config) {
|
|
18
|
+
config.groups?.forEach((group, index) => {
|
|
19
|
+
if (group.use === undefined)
|
|
20
|
+
return;
|
|
21
|
+
const place = `config.groups[${index}].use`;
|
|
22
|
+
try {
|
|
23
|
+
const raw = group.use;
|
|
24
|
+
const isObject = typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
25
|
+
const { viewport, ...options } = (isObject ? raw : {});
|
|
26
|
+
// Anything but an object is refused here, with the message `test.use()` gives
|
|
27
|
+
validateUseOptions(isObject ? options : raw);
|
|
28
|
+
if (viewport !== undefined &&
|
|
29
|
+
viewport !== null &&
|
|
30
|
+
!(isSize(viewport.width) && isSize(viewport.height)))
|
|
31
|
+
throw new TypeError(`viewport must be { width, height } with whole numbers above 0, or null, got ${JSON.stringify(viewport)}`);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
atGroup(place, err);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The `use` of the group called `name`; nothing when there is no such group or it has none */
|
|
39
|
+
export function groupUseOf(groups, name) {
|
|
40
|
+
return groups?.find((group) => group.name === name)?.use ?? {};
|
|
41
|
+
}
|
|
@@ -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),
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { StorageState } from "../types/use.js";
|
|
2
|
+
/** The state itself, or the JSON file (relative to `baseDir`) that holds it. Throws `<who>: what is wrong` (`use()` unless told otherwise). */
|
|
3
|
+
export declare function loadStorageState(source: string | StorageState, baseDir: string, who?: string): StorageState;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { checkStorageStateObject } from "./use-options.js";
|
|
4
|
+
/** The state itself, or the JSON file (relative to `baseDir`) that holds it. Throws `<who>: what is wrong` (`use()` unless told otherwise). */
|
|
5
|
+
export function loadStorageState(source, baseDir, who = "use()") {
|
|
6
|
+
if (typeof source !== "string")
|
|
7
|
+
return source;
|
|
8
|
+
const file = path.resolve(baseDir, source);
|
|
9
|
+
let state;
|
|
10
|
+
try {
|
|
11
|
+
state = JSON.parse(readFileSync(file, "utf8"));
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
throw new Error(`${who}: storageState file ${JSON.stringify(source)} could not be read: ${err instanceof Error ? err.message : String(err)}`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof state !== "object" || state === null || Array.isArray(state))
|
|
17
|
+
throw new Error(`${who}: storageState file ${JSON.stringify(source)} must hold an object`);
|
|
18
|
+
checkStorageStateObject(state);
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SamuraiTestConfig } from "../types/config.js";
|
|
2
|
+
import type { UseOptions } from "../types/use.js";
|
|
3
|
+
/** The options of `use`: config, group, `test.use()` and (those that can change at run time) `page.use()` */
|
|
4
|
+
export declare const USE_OPTION_NAMES: readonly string[];
|
|
5
|
+
/**
|
|
6
|
+
* Checks the options of `test.use()` or of the config's `use` and returns them. The config's `use` also holds
|
|
7
|
+
* options that are not these (`viewport`, `headless`, ...), so it passes `allowOthers`; `test.use()` refuses
|
|
8
|
+
* a key it does not know. Throws `use(): what is wrong`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateUseOptions(options: unknown, allowOthers?: boolean): UseOptions;
|
|
11
|
+
/** Checks a storage state object, whether it came from the options or from a file */
|
|
12
|
+
export declare function checkStorageStateObject(state: Record<string, unknown>): void;
|
|
13
|
+
/** The options after `layers` are applied in order: a later layer wins, key by key */
|
|
14
|
+
export declare function mergeUse<T extends UseOptions>(layers: readonly T[]): T;
|
|
15
|
+
/** Checks the new options of a config's `use` */
|
|
16
|
+
export declare function validateUseConfig(config: SamuraiTestConfig): void;
|
|
17
|
+
/** The options of `test.use()` that a config's `use` holds, without the rest (`viewport`, `headless`, ...) */
|
|
18
|
+
export declare function useOptionsOf(configUse: object | undefined): UseOptions;
|