@itmetsam/samurai-framework 0.12.1 → 0.15.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/README.md +29 -29
- package/dist/api.d.ts +2 -2
- package/dist/api.js +1 -1
- package/dist/cli/args.d.ts +4 -1
- package/dist/cli/args.js +7 -2
- package/dist/cli/console-reporter.js +17 -4
- package/dist/cli/flow.js +5 -0
- package/dist/cli/main.js +2 -1
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +2 -0
- package/dist/config/config.js +6 -0
- package/dist/config/forbid-only.d.ts +5 -0
- package/dist/config/forbid-only.js +9 -0
- package/dist/config/retries.d.ts +8 -0
- package/dist/config/retries.js +19 -0
- package/dist/flows/run/node-pool.d.ts +9 -0
- package/dist/flows/run/node-pool.js +27 -0
- package/dist/flows/run/project-options.d.ts +44 -0
- package/dist/flows/run/project-options.js +8 -0
- package/dist/flows/run/project.d.ts +4 -40
- package/dist/flows/run/project.js +30 -38
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +47 -6
- package/dist/recorder/candidates.d.ts +2 -1
- package/dist/recorder/candidates.js +44 -1
- package/dist/recorder/capture.d.ts +4 -2
- package/dist/recorder/capture.js +12 -4
- package/dist/recorder/page-messages.d.ts +2 -0
- package/dist/recorder/ranking.d.ts +2 -0
- package/dist/recorder/ranking.js +8 -1
- package/dist/recorder/recorder.js +5 -2
- package/dist/runner/all-hooks.d.ts +26 -0
- package/dist/runner/all-hooks.js +61 -0
- package/dist/runner/focus.d.ts +5 -0
- package/dist/runner/focus.js +19 -0
- package/dist/runner/hooks.d.ts +24 -0
- package/dist/runner/hooks.js +79 -0
- package/dist/runner/options.d.ts +6 -0
- package/dist/runner/pooled-run.d.ts +7 -1
- package/dist/runner/pooled-run.js +43 -14
- package/dist/runner/project.js +5 -0
- package/dist/runner/registry.d.ts +35 -4
- package/dist/runner/registry.js +66 -11
- package/dist/runner/reporter.d.ts +14 -4
- package/dist/runner/reporter.js +29 -2
- package/dist/runner/retries-option.d.ts +10 -0
- package/dist/runner/retries-option.js +12 -0
- package/dist/runner/retry.d.ts +13 -0
- package/dist/runner/retry.js +43 -0
- package/dist/runner/run-test.d.ts +8 -1
- package/dist/runner/run-test.js +34 -2
- package/dist/runner/run.d.ts +5 -2
- package/dist/runner/run.js +6 -1
- package/dist/runner/skip.d.ts +10 -0
- package/dist/runner/skip.js +18 -0
- package/dist/runner/suite.d.ts +24 -0
- package/dist/runner/suite.js +73 -0
- package/dist/runner/test-error.js +4 -0
- package/dist/runner/test-runner.js +54 -20
- package/dist/runner/worker-pool.d.ts +9 -1
- package/dist/runner/worker-pool.js +35 -2
- package/dist/runner/worker-protocol.d.ts +3 -0
- package/dist/runner/worker-session.js +17 -4
- package/dist/runner/worker.js +3 -0
- package/dist/steps/model.d.ts +3 -0
- package/dist/steps/parse.js +32 -7
- package/dist/types/config.d.ts +15 -1
- package/dist/types/test.d.ts +55 -3
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -19,12 +19,12 @@ test("Visitor can sign in", async ({ page, env, secrets }) => {
|
|
|
19
19
|
- **Auto-waiting.** Actions wait until the element is attached, visible, stable, enabled and not covered. Assertions retry until they pass or time out.
|
|
20
20
|
- **Semantic locators.** By role, label, text, test id, CSS or XPath, with ordered fallbacks.
|
|
21
21
|
- **Environments and secrets.** One test file runs against `dev`, `staging` or `production`; secrets come from the environment or a `.env.<name>` file and are masked in logs and reports.
|
|
22
|
-
- **Parallel runs.** Tests run side by side in worker processes (`--workers`, `workers` in the config; half the CPUs by default), and a flow's Parallel node runs its branches at the same time (see [Running tests and reports](
|
|
22
|
+
- **Parallel runs.** Tests run side by side in worker processes (`--workers`, `workers` in the config; half the CPUs by default), and a flow's Parallel node runs its branches at the same time (see [Running tests and reports](https://github.com/degrootsam/samurai-framework/wiki/Running-tests-and-reports#parallel-runs)).
|
|
23
23
|
- **Network control.** Mock, block or modify requests; wait for requests, responses and network idle; read response bodies.
|
|
24
|
-
- **Flows.** Chain tests and groups into a graph with conditions, variables, waits and HTTP requests, and run it from the command line or CI (see [Flows](
|
|
25
|
-
- **Recorder building blocks.** Specs are parsed into steps and edited back with minimal diffs, and an engine records what a person does in the browser. A toolbar in the recorded page adds assertions, variables and secrets, waits, screenshots and pause (see [Steps and the recorder](
|
|
24
|
+
- **Flows.** Chain tests and groups into a graph with conditions, variables, waits and HTTP requests, and run it from the command line or CI (see [Flows](https://github.com/degrootsam/samurai-framework/wiki/Flows)).
|
|
25
|
+
- **Recorder building blocks.** Specs are parsed into steps and edited back with minimal diffs, and an engine records what a person does in the browser. A toolbar in the recorded page adds assertions, variables and secrets, waits, screenshots and pause (see [Steps and the recorder](https://github.com/degrootsam/samurai-framework/wiki/Steps-and-the-recorder)). The Electron app that ties them together lives in a separate repository (see the [roadmap](https://github.com/degrootsam/samurai-framework/wiki/Roadmap)).
|
|
26
26
|
|
|
27
|
-
> **Status:** early. Firefox only. See [what is still open](
|
|
27
|
+
> **Status:** early. Firefox only. See [what is still open](https://github.com/degrootsam/samurai-framework/wiki/Roadmap).
|
|
28
28
|
|
|
29
29
|
## Getting started
|
|
30
30
|
|
|
@@ -39,14 +39,14 @@ npx samurai browsers install firefox # once per machine
|
|
|
39
39
|
npx samurai run
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Specs and the config then import from `@itmetsam/samurai-framework` instead of the relative paths used below. See [Command line](
|
|
42
|
+
Specs and the config then import from `@itmetsam/samurai-framework` instead of the relative paths used below. See [Command line](https://github.com/degrootsam/samurai-framework/wiki/Command-line).
|
|
43
43
|
|
|
44
44
|
The rest of this section works in a checkout of this repository.
|
|
45
45
|
|
|
46
46
|
### 1. Requirements
|
|
47
47
|
|
|
48
48
|
- [Node.js](https://nodejs.org) 24 or newer (`engines` in `package.json`; CI uses 26) and, to work on the framework, [Bun](https://bun.sh) 1.3 or newer
|
|
49
|
-
- Firefox, downloaded by the framework: run `bun run dev browsers install firefox` once in this checkout (`npx samurai browsers install firefox` in a project; see [Which browser runs](
|
|
49
|
+
- Firefox, downloaded by the framework: run `bun run dev browsers install firefox` once in this checkout (`npx samurai browsers install firefox` in a project; see [Which browser runs](https://github.com/degrootsam/samurai-framework/wiki/Pages-and-browsers#which-browser-runs)). The Firefox installed on the machine is not used.
|
|
50
50
|
|
|
51
51
|
### 2. Install
|
|
52
52
|
|
|
@@ -103,13 +103,13 @@ bun run dev --env production # another environment (or SAMURAI_ENV=production)
|
|
|
103
103
|
bun run dev --timeout 60000 # per-test timeout in ms; also --expect-timeout
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
`bun run dev` is `samurai run` straight from the sources, so it takes the same options (`--grep`, `--file`, `--group`, `--headless`, `--json`, …) and the other commands too (`bun run dev list`, `bun run dev browsers install firefox`). See [Command line](
|
|
106
|
+
`bun run dev` is `samurai run` straight from the sources, so it takes the same options (`--grep`, `--file`, `--group`, `--headless`, `--json`, …) and the other commands too (`bun run dev list`, `bun run dev browsers install firefox`). See [Command line](https://github.com/degrootsam/samurai-framework/wiki/Command-line).
|
|
107
107
|
|
|
108
|
-
A browser window opens for each test and closes when the test ends (`--headless` hides it; on CI it is the default, see [Configuration](
|
|
108
|
+
A browser window opens for each test and closes when the test ends (`--headless` hides it; on CI it is the default, see [Configuration](https://github.com/degrootsam/samurai-framework/wiki/Configuration#headless)). Each test's result is printed as it finishes, followed by a summary, and the full report is written to `result/report.json`. The process exits with code 0 when every test passed, 1 otherwise and 2 for a usage or configuration error. Framework logs go to stderr and `logs/`.
|
|
109
109
|
|
|
110
110
|
### Flows (optional)
|
|
111
111
|
|
|
112
|
-
A project can also keep flows in `flows/*.flow.json` (edited in the Samurai app) that chain tests and groups with conditions and variables. `samurai flow check` validates them and `samurai flow run <flow>` runs them, with a report in `result/flows/`. See [Flows](
|
|
112
|
+
A project can also keep flows in `flows/*.flow.json` (edited in the Samurai app) that chain tests and groups with conditions and variables. `samurai flow check` validates them and `samurai flow run <flow>` runs them, with a report in `result/flows/`. See [Flows](https://github.com/degrootsam/samurai-framework/wiki/Flows).
|
|
113
113
|
|
|
114
114
|
### 6. Secrets (optional)
|
|
115
115
|
|
|
@@ -119,29 +119,29 @@ cp .env.example .env.staging
|
|
|
119
119
|
# SAMURAI_SECRET_TEST_PASSWORD=...
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
Read it in a test as `secrets.TEST_PASSWORD`. Real environment variables win over the file, and `.env.*` files are git-ignored. See [Environments and secrets](
|
|
122
|
+
Read it in a test as `secrets.TEST_PASSWORD`. Real environment variables win over the file, and `.env.*` files are git-ignored. See [Environments and secrets](https://github.com/degrootsam/samurai-framework/wiki/Environments-and-secrets).
|
|
123
123
|
|
|
124
124
|
## Documentation
|
|
125
125
|
|
|
126
|
-
| Guide
|
|
127
|
-
|
|
|
128
|
-
| [Writing tests](
|
|
129
|
-
| [Locators](
|
|
130
|
-
| [Assertions](
|
|
131
|
-
| [Pages and browsers](
|
|
132
|
-
| [Network](
|
|
133
|
-
| [Configuration](
|
|
134
|
-
| [Environments and secrets](
|
|
135
|
-
| [Command line](
|
|
136
|
-
| [Flows](
|
|
137
|
-
| [Writing a step](
|
|
138
|
-
| [Running tests and reports](
|
|
139
|
-
| [Steps and the recorder](
|
|
140
|
-
| [Embedding the framework](
|
|
141
|
-
| [Contributing](docs/contributing.md)
|
|
142
|
-
| [Roadmap](
|
|
143
|
-
|
|
144
|
-
The [documentation index](
|
|
126
|
+
| Guide | What it covers |
|
|
127
|
+
| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
128
|
+
| [Writing tests](https://github.com/degrootsam/samurai-framework/wiki/Writing-tests) | `test`, `describe`, fixtures, how a test passes or fails |
|
|
129
|
+
| [Locators](https://github.com/degrootsam/samurai-framework/wiki/Locators) | Finding elements, chaining, fallbacks, actions and their auto-waiting |
|
|
130
|
+
| [Assertions](https://github.com/degrootsam/samurai-framework/wiki/Assertions) | `expect` for elements and for plain values |
|
|
131
|
+
| [Pages and browsers](https://github.com/degrootsam/samurai-framework/wiki/Pages-and-browsers) | Navigation, waits, screenshots, PDF, viewport, emulation, dialogs, downloads, contexts, cookies |
|
|
132
|
+
| [Network](https://github.com/degrootsam/samurai-framework/wiki/Network) | Waiting for traffic, mocking, blocking, response bodies |
|
|
133
|
+
| [Configuration](https://github.com/degrootsam/samurai-framework/wiki/Configuration) | Every option in `samurai.config.ts`, CLI flags and precedence |
|
|
134
|
+
| [Environments and secrets](https://github.com/degrootsam/samurai-framework/wiki/Environments-and-secrets) | Per-environment settings, variables, secrets and masking |
|
|
135
|
+
| [Command line](https://github.com/degrootsam/samurai-framework/wiki/Command-line) | `samurai run`, `list`, `record` and `init`, options, output, exit codes, `--json` |
|
|
136
|
+
| [Flows](https://github.com/degrootsam/samurai-framework/wiki/Flows) | `samurai flow list`, `check` and `run`: graphs of tests, groups and steps, reports, CI |
|
|
137
|
+
| [Writing a step](https://github.com/degrootsam/samurai-framework/wiki/Writing-a-step) | `defineStep`: flow steps of your own, the built-in steps |
|
|
138
|
+
| [Running tests and reports](https://github.com/degrootsam/samurai-framework/wiki/Running-tests-and-reports) | What a run does, `report.json`, page logs, timeouts |
|
|
139
|
+
| [Steps and the recorder](https://github.com/degrootsam/samurai-framework/wiki/Steps-and-the-recorder) | The step codec, the recording engine and the recorder toolbar |
|
|
140
|
+
| [Embedding the framework](https://github.com/degrootsam/samurai-framework/wiki/Embedding-the-framework) | Using the package from another app (such as the Electron UI): build, entry points, `runTests`, events |
|
|
141
|
+
| [Contributing](docs/contributing.md) | Project layout, running the framework's own tests, conventions |
|
|
142
|
+
| [Roadmap](https://github.com/degrootsam/samurai-framework/wiki/Roadmap) | What is built and what is still open |
|
|
143
|
+
|
|
144
|
+
The [documentation index](https://github.com/degrootsam/samurai-framework/wiki/Home) lists every guide by topic.
|
|
145
145
|
|
|
146
146
|
## Scripts
|
|
147
147
|
|
package/dist/api.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { test, describe } from "./runner/registry.js";
|
|
1
|
+
export { test, describe, beforeEach, afterEach, beforeAll, afterAll, } from "./runner/registry.js";
|
|
2
2
|
export { expect } from "./assert/expect.js";
|
|
3
3
|
export { defineConfig } from "./config/config.js";
|
|
4
4
|
export { defineStep, type StepDefinition, type StepResult, type StepRunInput, } from "./flows/steps/index.js";
|
|
5
|
-
export type { TestFixtures } from "./types/test.js";
|
|
5
|
+
export type { AllHookFixtures, TestFixtures } from "./types/test.js";
|
|
6
6
|
export { generatedFile, type GeneratedFile, type FileInput, } from "./browser/generated-files.js";
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { test, describe } from "./runner/registry.js";
|
|
1
|
+
export { test, describe, beforeEach, afterEach, beforeAll, afterAll, } from "./runner/registry.js";
|
|
2
2
|
export { expect } from "./assert/expect.js";
|
|
3
3
|
export { defineConfig } from "./config/config.js";
|
|
4
4
|
export { defineStep, } from "./flows/steps/index.js";
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare function parseCommandLine(argv: string[]): {
|
|
|
12
12
|
headless?: boolean | undefined;
|
|
13
13
|
port?: string | undefined;
|
|
14
14
|
workers?: string | undefined;
|
|
15
|
+
retries?: string | undefined;
|
|
15
16
|
grep?: string | undefined;
|
|
16
17
|
group?: string | undefined;
|
|
17
18
|
browser?: string[] | undefined;
|
|
@@ -40,13 +41,15 @@ export interface RunnerFlags {
|
|
|
40
41
|
port?: number;
|
|
41
42
|
/** `--workers`: a whole number or a percentage like `50%`, as typed */
|
|
42
43
|
workers?: string;
|
|
44
|
+
/** `--retries`: a whole number from 0, as typed */
|
|
45
|
+
retries?: string;
|
|
43
46
|
grep?: string;
|
|
44
47
|
group?: string;
|
|
45
48
|
files?: string[];
|
|
46
49
|
/** `--browser`, repeated for more than one; replaces the config's `browsers` for this run */
|
|
47
50
|
browsers?: SupportedBrowser[];
|
|
48
51
|
}
|
|
49
|
-
/** Reads `--headless`, `--port <n>`, `--workers <n|n%>`, `--grep <text>`, `--group <name>`, `--file <spec>` and `--browser <name>` (both repeatable) from `argv`, or from values that were parsed already */
|
|
52
|
+
/** Reads `--headless`, `--port <n>`, `--workers <n|n%>`, `--retries <n>`, `--grep <text>`, `--group <name>`, `--file <spec>` and `--browser <name>` (both repeatable) from `argv`, or from values that were parsed already */
|
|
50
53
|
export declare function parseRunnerFlags(source: string[] | CommandValues): RunnerFlags;
|
|
51
54
|
/** Reads `--env`, `--timeout` and `--expect-timeout` from `argv` (or values parsed already); the environment falls back to SAMURAI_ENV */
|
|
52
55
|
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, parseWorkers, } from "../runner/run.js";
|
|
2
|
+
import { checkBrowserList, parseRetries, parseWorkers, } from "../runner/run.js";
|
|
3
3
|
const FLAGS = {
|
|
4
4
|
env: { type: "string" },
|
|
5
5
|
timeout: { type: "string" },
|
|
@@ -7,6 +7,7 @@ const FLAGS = {
|
|
|
7
7
|
headless: { type: "boolean" },
|
|
8
8
|
port: { type: "string" },
|
|
9
9
|
workers: { type: "string" },
|
|
10
|
+
retries: { type: "string" },
|
|
10
11
|
grep: { type: "string" },
|
|
11
12
|
group: { type: "string" },
|
|
12
13
|
browser: { type: "string", multiple: true },
|
|
@@ -34,7 +35,7 @@ export function parseCommandLine(argv) {
|
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
const valuesOf = (source) => Array.isArray(source) ? parseCommandLine(source).values : source;
|
|
37
|
-
/** Reads `--headless`, `--port <n>`, `--workers <n|n%>`, `--grep <text>`, `--group <name>`, `--file <spec>` and `--browser <name>` (both repeatable) from `argv`, or from values that were parsed already */
|
|
38
|
+
/** Reads `--headless`, `--port <n>`, `--workers <n|n%>`, `--retries <n>`, `--grep <text>`, `--group <name>`, `--file <spec>` and `--browser <name>` (both repeatable) from `argv`, or from values that were parsed already */
|
|
38
39
|
export function parseRunnerFlags(source) {
|
|
39
40
|
const values = valuesOf(source);
|
|
40
41
|
const flags = {};
|
|
@@ -51,6 +52,10 @@ export function parseRunnerFlags(source) {
|
|
|
51
52
|
parseWorkers(values.workers, "--workers");
|
|
52
53
|
flags.workers = values.workers;
|
|
53
54
|
}
|
|
55
|
+
if (values.retries !== undefined) {
|
|
56
|
+
parseRetries(values.retries, "--retries");
|
|
57
|
+
flags.retries = values.retries;
|
|
58
|
+
}
|
|
54
59
|
if (values.grep !== undefined)
|
|
55
60
|
flags.grep = values.grep;
|
|
56
61
|
if (values.group !== undefined)
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { painter } from "./paint.js";
|
|
2
2
|
const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
|
|
3
|
+
/** What a result says about its retries: `passed on retry 2`, `failed after 2 retries`, or nothing */
|
|
4
|
+
function retried(result) {
|
|
5
|
+
if (!("retry" in result) || !result.retry)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (result.status === "success")
|
|
8
|
+
return `passed on retry ${result.retry}`;
|
|
9
|
+
return `failed after ${result.retry} ${result.retry === 1 ? "retry" : "retries"}`;
|
|
10
|
+
}
|
|
3
11
|
/** Prints a run for a person: one line per test as it ends, then every failure in full and a count */
|
|
4
12
|
export class ConsoleReporter {
|
|
5
13
|
constructor({ write, color }) {
|
|
@@ -30,14 +38,17 @@ export class ConsoleReporter {
|
|
|
30
38
|
}
|
|
31
39
|
line(name, result, browser) {
|
|
32
40
|
const took = "duration" in result
|
|
33
|
-
? ` ${this.paint(90, `(${seconds(result.duration)})`)}`
|
|
41
|
+
? ` ${this.paint(90, `(${[seconds(result.duration), retried(result)].filter(Boolean).join(", ")})`)}`
|
|
34
42
|
: "";
|
|
43
|
+
if (result.status === "skipped")
|
|
44
|
+
return this.paint(90, `- ${name}${this.tag(browser)} (${result.annotation === "fixme" ? "fixme" : "skipped"}${result.reason ? `: ${result.reason}` : ""})`);
|
|
35
45
|
return result.status === "success"
|
|
36
46
|
? `${this.paint(32, "✔")} ${name}${this.tag(browser)}${took}`
|
|
37
47
|
: `${this.paint(31, "✖")} ${name}${this.tag(browser)}${took}`;
|
|
38
48
|
}
|
|
39
49
|
summary(summary) {
|
|
40
|
-
const
|
|
50
|
+
const skipped = summary.tests.filter((test) => test.status === "skipped");
|
|
51
|
+
const failed = summary.tests.filter((test) => test.status !== "success" && test.status !== "skipped");
|
|
41
52
|
for (const test of failed) {
|
|
42
53
|
if (!("name" in test))
|
|
43
54
|
continue;
|
|
@@ -50,10 +61,12 @@ export class ConsoleReporter {
|
|
|
50
61
|
if ("file" in test)
|
|
51
62
|
this.write(` ${this.paint(90, test.file)}\n`);
|
|
52
63
|
}
|
|
53
|
-
const passed = summary.tests.length - failed.length;
|
|
64
|
+
const passed = summary.tests.length - failed.length - skipped.length;
|
|
65
|
+
const flaky = summary.tests.filter((test) => "flaky" in test && test.flaky).length;
|
|
54
66
|
const parts = [
|
|
55
67
|
failed.length > 0 && this.paint(31, `${failed.length} failed`),
|
|
56
|
-
this.paint(32, `${passed} passed`),
|
|
68
|
+
this.paint(32, `${passed} passed${flaky > 0 ? ` (${flaky} flaky)` : ""}`),
|
|
69
|
+
skipped.length > 0 && this.paint(90, `${skipped.length} skipped`),
|
|
57
70
|
].filter(Boolean);
|
|
58
71
|
this.write(`\n${parts.join(", ")} ${this.paint(90, `(${seconds(summary.duration)})`)}\n`);
|
|
59
72
|
}
|
package/dist/cli/flow.js
CHANGED
|
@@ -25,6 +25,7 @@ Options for run:
|
|
|
25
25
|
--headless, --no-headless Run the browser without a window, or with one
|
|
26
26
|
--port <n> Browser debugging port (default ${DEFAULT_PORT})
|
|
27
27
|
--workers <n|n%> Worker processes the tests share (or SAMURAI_WORKERS; default half the CPUs)
|
|
28
|
+
--retries <n> Run a failing test again up to n times, each with a fresh browser (or SAMURAI_RETRIES; default 0)
|
|
28
29
|
|
|
29
30
|
Each run writes result/flows/<id>.json.
|
|
30
31
|
Exit code: 0 when every node that should run passed, 1 when a node failed or the run was cancelled,
|
|
@@ -228,6 +229,10 @@ export async function flowCommand(argv, io) {
|
|
|
228
229
|
...(io.env.SAMURAI_WORKERS && {
|
|
229
230
|
workersEnv: io.env.SAMURAI_WORKERS,
|
|
230
231
|
}),
|
|
232
|
+
...(flags.retries !== undefined && { retries: flags.retries }),
|
|
233
|
+
...(io.env.SAMURAI_RETRIES && {
|
|
234
|
+
retriesEnv: io.env.SAMURAI_RETRIES,
|
|
235
|
+
}),
|
|
231
236
|
},
|
|
232
237
|
});
|
|
233
238
|
}
|
package/dist/cli/main.js
CHANGED
|
@@ -48,6 +48,7 @@ export async function main(argv, io) {
|
|
|
48
48
|
...(flags.headless !== undefined && { headless: flags.headless }),
|
|
49
49
|
...(flags.port !== undefined && { port: flags.port }),
|
|
50
50
|
...(flags.workers !== undefined && { workers: flags.workers }),
|
|
51
|
+
...(flags.retries !== undefined && { retries: flags.retries }),
|
|
51
52
|
...(flags.grep !== undefined && { grep: flags.grep }),
|
|
52
53
|
...(flags.group !== undefined && { group: flags.group }),
|
|
53
54
|
...(flags.files && { files: flags.files }),
|
|
@@ -59,7 +60,7 @@ export async function main(argv, io) {
|
|
|
59
60
|
io.out(`${JSON.stringify(tests, null, 2)}\n`);
|
|
60
61
|
else
|
|
61
62
|
for (const found of tests)
|
|
62
|
-
io.out(`${found.name}\n ${found.file}\n`);
|
|
63
|
+
io.out(`${found.name}${found.modifier ? ` [${found.modifier}]` : ""}\n ${found.file}\n`);
|
|
63
64
|
return 0;
|
|
64
65
|
}
|
|
65
66
|
const human = values.json
|
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\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\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";
|
package/dist/cli/usage.js
CHANGED
|
@@ -27,6 +27,8 @@ Options for run:
|
|
|
27
27
|
--workers <n|n%> Tests that run at the same time, each in a worker process: a number
|
|
28
28
|
from 1, or a share of the CPUs like 50%. Default: the config's
|
|
29
29
|
workers (or SAMURAI_WORKERS), else half the CPUs
|
|
30
|
+
--retries <n> Run a failing test again up to n times, each with a fresh browser
|
|
31
|
+
(or SAMURAI_RETRIES; default 0)
|
|
30
32
|
|
|
31
33
|
Options for record (also --env, --port, --json, --headless, --no-headless):
|
|
32
34
|
--browser <name> Record in this browser (default: the first of the config's browsers)
|
package/dist/config/config.js
CHANGED
|
@@ -2,6 +2,8 @@ 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 { validateForbidOnlyConfig } from "./forbid-only.js";
|
|
6
|
+
import { validateRetriesConfig } from "./retries.js";
|
|
5
7
|
import { validateWorkersConfig } from "./workers.js";
|
|
6
8
|
let activeProject;
|
|
7
9
|
/** Makes `project` the one config, secrets, reports and downloads are resolved for; `undefined` ends it */
|
|
@@ -27,6 +29,8 @@ export async function loadConfig() {
|
|
|
27
29
|
if (activeProject?.config) {
|
|
28
30
|
validateBrowserConfig(activeProject.config);
|
|
29
31
|
validateWorkersConfig(activeProject.config);
|
|
32
|
+
validateForbidOnlyConfig(activeProject.config);
|
|
33
|
+
validateRetriesConfig(activeProject.config);
|
|
30
34
|
return activeProject.config;
|
|
31
35
|
}
|
|
32
36
|
const configUrl = path.join(projectDir(), "samurai.config.ts");
|
|
@@ -37,6 +41,8 @@ export async function loadConfig() {
|
|
|
37
41
|
}
|
|
38
42
|
validateBrowserConfig(config.default);
|
|
39
43
|
validateWorkersConfig(config.default);
|
|
44
|
+
validateForbidOnlyConfig(config.default);
|
|
45
|
+
validateRetriesConfig(config.default);
|
|
40
46
|
return config.default;
|
|
41
47
|
}
|
|
42
48
|
/** A project's config without keeping the project active (`withProject` is one at a time): the given one, else its samurai.config.ts */
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SamuraiTestConfig } from "../types/config.js";
|
|
2
|
+
/** Checks the `forbidOnly` key of a config. Throws an Error that names the key */
|
|
3
|
+
export declare function validateForbidOnlyConfig(config: SamuraiTestConfig): void;
|
|
4
|
+
/** Whether `test.only` is refused: the config's `forbidOnly`, else on when `CI` is set */
|
|
5
|
+
export declare function forbidsOnly(config: SamuraiTestConfig, env?: Partial<NodeJS.ProcessEnv>): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Checks the `forbidOnly` key of a config. Throws an Error that names the key */
|
|
2
|
+
export function validateForbidOnlyConfig(config) {
|
|
3
|
+
if (config.forbidOnly !== undefined && typeof config.forbidOnly !== "boolean")
|
|
4
|
+
throw new Error(`forbidOnly must be true or false, got "${String(config.forbidOnly)}"`);
|
|
5
|
+
}
|
|
6
|
+
/** Whether `test.only` is refused: the config's `forbidOnly`, else on when `CI` is set */
|
|
7
|
+
export function forbidsOnly(config, env = process.env) {
|
|
8
|
+
return config.forbidOnly ?? Boolean(env.CI);
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SamuraiTestConfig } from "../types/config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads a `retries` value: a whole number from 0, as a number or as text. `where` names the source (`--retries`,
|
|
4
|
+
* `SAMURAI_RETRIES`, a config key) in the error.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseRetries(value: unknown, where: string): number;
|
|
7
|
+
/** Checks the `retries` key of a config. Throws an Error that names the key */
|
|
8
|
+
export declare function validateRetriesConfig(config: SamuraiTestConfig): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a `retries` value: a whole number from 0, as a number or as text. `where` names the source (`--retries`,
|
|
3
|
+
* `SAMURAI_RETRIES`, a config key) in the error.
|
|
4
|
+
*/
|
|
5
|
+
export function parseRetries(value, where) {
|
|
6
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0)
|
|
7
|
+
return value;
|
|
8
|
+
if (typeof value === "string" && /^\s*\d+\s*$/.test(value))
|
|
9
|
+
return Number(value.trim());
|
|
10
|
+
throw new Error(`${where} must be a whole number from 0, got "${String(value)}"`);
|
|
11
|
+
}
|
|
12
|
+
/** Checks the `retries` key of a config. Throws an Error that names the key */
|
|
13
|
+
export function validateRetriesConfig(config) {
|
|
14
|
+
if (config.retries === undefined)
|
|
15
|
+
return;
|
|
16
|
+
if (typeof config.retries !== "number")
|
|
17
|
+
throw new Error(`retries must be a whole number from 0, got "${String(config.retries)}"`);
|
|
18
|
+
parseRetries(config.retries, "retries");
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunEvent } from "../../runner/reporter.js";
|
|
2
|
+
import type { WorkerPool } from "../../runner/worker-pool.js";
|
|
3
|
+
import type { WorkItem } from "../../runner/worker-protocol.js";
|
|
4
|
+
import type { SupportedBrowser } from "../../types/browser.js";
|
|
5
|
+
import type { NodeTest } from "./events.js";
|
|
6
|
+
/** The work items of a node: each test in each browser, the browsers one after the other */
|
|
7
|
+
export declare function itemsOf(tests: NodeTest[], browsers: readonly SupportedBrowser[]): WorkItem[];
|
|
8
|
+
/** Runs the tests of a node on the pool and reports them as a run would */
|
|
9
|
+
export declare function runOnPool(pool: Pick<WorkerPool, "run">, items: WorkItem[], onEvent: (event: RunEvent) => void, signal: AbortSignal): Promise<"passed" | "failed" | "cancelled">;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The work items of a node: each test in each browser, the browsers one after the other */
|
|
2
|
+
export function itemsOf(tests, browsers) {
|
|
3
|
+
return browsers
|
|
4
|
+
.flatMap((browser) => tests.map((t) => ({ file: t.file, testName: t.name, browser })))
|
|
5
|
+
.map((item, index) => ({ ...item, index }));
|
|
6
|
+
}
|
|
7
|
+
/** Runs the tests of a node on the pool and reports them as a run would */
|
|
8
|
+
export async function runOnPool(pool, items, onEvent, signal) {
|
|
9
|
+
const results = await pool.run(items, {
|
|
10
|
+
signal,
|
|
11
|
+
onEvent: (event) => onEvent({
|
|
12
|
+
type: event.type,
|
|
13
|
+
name: event.item.testName,
|
|
14
|
+
file: event.item.file,
|
|
15
|
+
browser: event.item.browser,
|
|
16
|
+
...(event.type === "test-start" &&
|
|
17
|
+
event.retry && { retry: event.retry }),
|
|
18
|
+
...(event.type === "test-end" && { result: event.result }),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
if (signal.aborted)
|
|
22
|
+
return "cancelled";
|
|
23
|
+
// A skipped test is not a failure
|
|
24
|
+
return results.every((r) => r.status === "success" || r.status === "skipped")
|
|
25
|
+
? "passed"
|
|
26
|
+
: "failed";
|
|
27
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ProjectOptions } from "../../runner/project.js";
|
|
2
|
+
import type { PoolOptions, WorkerPool } from "../../runner/worker-pool.js";
|
|
3
|
+
import type { runTests } from "../../runner/run.js";
|
|
4
|
+
import type { FlowFile, FlowProblem } from "../core/index.js";
|
|
5
|
+
import type { FlowEvent } from "./events.js";
|
|
6
|
+
/** The flow has errors; nothing was run */
|
|
7
|
+
export declare class FlowCheckError extends Error {
|
|
8
|
+
readonly problems: FlowProblem[];
|
|
9
|
+
constructor(problems: FlowProblem[]);
|
|
10
|
+
}
|
|
11
|
+
export interface RunFlowInProjectOptions extends ProjectOptions {
|
|
12
|
+
/** The flow to run... */
|
|
13
|
+
flow?: FlowFile;
|
|
14
|
+
/** ...or the id of one in `<project>/flows` */
|
|
15
|
+
flowId?: string;
|
|
16
|
+
/** Beats the flow's `envDefault`, the config's `defaultEnvironment` and the first environment */
|
|
17
|
+
environment?: string;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
onEvent?: (event: FlowEvent) => void;
|
|
20
|
+
/** `"fail"` (default): a flow with a node that can't run is refused. `"skip"`: those nodes are skipped. */
|
|
21
|
+
unsupported?: "fail" | "skip";
|
|
22
|
+
port?: number;
|
|
23
|
+
/**
|
|
24
|
+
* How many worker processes the tests of the whole run share: a number, or a percentage like `"50%"`. Unset
|
|
25
|
+
* takes `SAMURAI_WORKERS` (`workersEnv`), then the config's `workers`, then half the CPUs. 1 runs each Test
|
|
26
|
+
* and Group node through `runTests` in this process; more starts one pool that every node, and every branch
|
|
27
|
+
* of a Parallel node, takes its workers from
|
|
28
|
+
*/
|
|
29
|
+
workers?: number | string;
|
|
30
|
+
/** The value of `SAMURAI_WORKERS`, when set */
|
|
31
|
+
workersEnv?: string;
|
|
32
|
+
/** How many times a failing test is run again: a number, or numeric text like the command line's `--retries`. Unset takes `SAMURAI_RETRIES` (`retriesEnv`), then the config's `retries`, then 0 */
|
|
33
|
+
retries?: number | string;
|
|
34
|
+
/** The value of `SAMURAI_RETRIES`, when set */
|
|
35
|
+
retriesEnv?: string;
|
|
36
|
+
/** @internal Replaces the worker pool (tests) */
|
|
37
|
+
createPool?: (options: PoolOptions) => Pick<WorkerPool, "run" | "dispose">;
|
|
38
|
+
/** @internal Replaces the runner of each Test and Group node (tests) */
|
|
39
|
+
testRunner?: typeof runTests;
|
|
40
|
+
/** @internal */
|
|
41
|
+
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/** What `runFlowInProject` takes when the flow itself is given separately: everything but `flow` and `flowId` */
|
|
44
|
+
export type FlowRunOptions = Omit<RunFlowInProjectOptions, "flow" | "flowId">;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The flow has errors; nothing was run */
|
|
2
|
+
export class FlowCheckError extends Error {
|
|
3
|
+
constructor(problems) {
|
|
4
|
+
super(`This flow can't run:\n${problems.map((p) => ` - ${p.message}`).join("\n")}`);
|
|
5
|
+
this.problems = problems;
|
|
6
|
+
this.name = "FlowCheckError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -1,46 +1,11 @@
|
|
|
1
1
|
import { type FlowFile, type FlowProblem } from "../core/index.js";
|
|
2
2
|
import { type ResolvedGroup } from "../../runner/groups.js";
|
|
3
|
-
import { type ProjectOptions } from "../../runner/project.js";
|
|
4
|
-
import { runTests } from "../../runner/run.js";
|
|
5
|
-
import { WorkerPool, type PoolOptions } from "../../runner/worker-pool.js";
|
|
6
3
|
import type { SamuraiTestConfig } from "../../types/config.js";
|
|
7
|
-
import type {
|
|
4
|
+
import type { FlowSummary, NodeTest } from "./events.js";
|
|
5
|
+
import { FlowCheckError, type FlowRunOptions, type RunFlowInProjectOptions } from "./project-options.js";
|
|
8
6
|
import type { SupportedBrowser } from "../../types/browser.js";
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
readonly problems: FlowProblem[];
|
|
12
|
-
constructor(problems: FlowProblem[]);
|
|
13
|
-
}
|
|
14
|
-
export interface RunFlowInProjectOptions extends ProjectOptions {
|
|
15
|
-
/** The flow to run... */
|
|
16
|
-
flow?: FlowFile;
|
|
17
|
-
/** ...or the id of one in `<project>/flows` */
|
|
18
|
-
flowId?: string;
|
|
19
|
-
/** Beats the flow's `envDefault`, the config's `defaultEnvironment` and the first environment */
|
|
20
|
-
environment?: string;
|
|
21
|
-
signal?: AbortSignal;
|
|
22
|
-
onEvent?: (event: FlowEvent) => void;
|
|
23
|
-
/** `"fail"` (default): a flow with a node that can't run is refused. `"skip"`: those nodes are skipped. */
|
|
24
|
-
unsupported?: "fail" | "skip";
|
|
25
|
-
port?: number;
|
|
26
|
-
/**
|
|
27
|
-
* How many worker processes the tests of the whole run share: a number, or a percentage like `"50%"`. Unset
|
|
28
|
-
* takes `SAMURAI_WORKERS` (`workersEnv`), then the config's `workers`, then half the CPUs. 1 runs each Test
|
|
29
|
-
* and Group node through `runTests` in this process; more starts one pool that every node, and every branch
|
|
30
|
-
* of a Parallel node, takes its workers from
|
|
31
|
-
*/
|
|
32
|
-
workers?: number | string;
|
|
33
|
-
/** The value of `SAMURAI_WORKERS`, when set */
|
|
34
|
-
workersEnv?: string;
|
|
35
|
-
/** @internal Replaces the worker pool (tests) */
|
|
36
|
-
createPool?: (options: PoolOptions) => Pick<WorkerPool, "run" | "dispose">;
|
|
37
|
-
/** @internal Replaces the runner of each Test and Group node (tests) */
|
|
38
|
-
testRunner?: typeof runTests;
|
|
39
|
-
/** @internal */
|
|
40
|
-
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
41
|
-
}
|
|
42
|
-
/** What `runFlowInProject` takes when the flow itself is given separately: everything but `flow` and `flowId` */
|
|
43
|
-
export type FlowRunOptions = Omit<RunFlowInProjectOptions, "flow" | "flowId">;
|
|
7
|
+
export { FlowCheckError };
|
|
8
|
+
export type { FlowRunOptions, RunFlowInProjectOptions };
|
|
44
9
|
/** The environment: the option, else the flow's `envDefault` (when the project has it), else the config's default, else the first, else "default" */
|
|
45
10
|
export declare function chooseEnvironment(config: SamuraiTestConfig, flow: FlowFile, option?: string): string;
|
|
46
11
|
/** A test of the project as flows see it */
|
|
@@ -71,4 +36,3 @@ export declare function checkFlowInProject(options: Pick<RunFlowInProjectOptions
|
|
|
71
36
|
* One at a time per process.
|
|
72
37
|
*/
|
|
73
38
|
export declare function runFlowInProject(options: RunFlowInProjectOptions): Promise<FlowSummary>;
|
|
74
|
-
export {};
|