@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.
Files changed (69) hide show
  1. package/README.md +29 -29
  2. package/dist/api.d.ts +2 -2
  3. package/dist/api.js +1 -1
  4. package/dist/cli/args.d.ts +4 -1
  5. package/dist/cli/args.js +7 -2
  6. package/dist/cli/console-reporter.js +17 -4
  7. package/dist/cli/flow.js +5 -0
  8. package/dist/cli/main.js +2 -1
  9. package/dist/cli/usage.d.ts +1 -1
  10. package/dist/cli/usage.js +2 -0
  11. package/dist/config/config.js +6 -0
  12. package/dist/config/forbid-only.d.ts +5 -0
  13. package/dist/config/forbid-only.js +9 -0
  14. package/dist/config/retries.d.ts +8 -0
  15. package/dist/config/retries.js +19 -0
  16. package/dist/flows/run/node-pool.d.ts +9 -0
  17. package/dist/flows/run/node-pool.js +27 -0
  18. package/dist/flows/run/project-options.d.ts +44 -0
  19. package/dist/flows/run/project-options.js +8 -0
  20. package/dist/flows/run/project.d.ts +4 -40
  21. package/dist/flows/run/project.js +30 -38
  22. package/dist/logger/index.d.ts +6 -0
  23. package/dist/logger/index.js +47 -6
  24. package/dist/recorder/candidates.d.ts +2 -1
  25. package/dist/recorder/candidates.js +44 -1
  26. package/dist/recorder/capture.d.ts +4 -2
  27. package/dist/recorder/capture.js +12 -4
  28. package/dist/recorder/page-messages.d.ts +2 -0
  29. package/dist/recorder/ranking.d.ts +2 -0
  30. package/dist/recorder/ranking.js +8 -1
  31. package/dist/recorder/recorder.js +5 -2
  32. package/dist/runner/all-hooks.d.ts +26 -0
  33. package/dist/runner/all-hooks.js +61 -0
  34. package/dist/runner/focus.d.ts +5 -0
  35. package/dist/runner/focus.js +19 -0
  36. package/dist/runner/hooks.d.ts +24 -0
  37. package/dist/runner/hooks.js +79 -0
  38. package/dist/runner/options.d.ts +6 -0
  39. package/dist/runner/pooled-run.d.ts +7 -1
  40. package/dist/runner/pooled-run.js +43 -14
  41. package/dist/runner/project.js +5 -0
  42. package/dist/runner/registry.d.ts +35 -4
  43. package/dist/runner/registry.js +66 -11
  44. package/dist/runner/reporter.d.ts +14 -4
  45. package/dist/runner/reporter.js +29 -2
  46. package/dist/runner/retries-option.d.ts +10 -0
  47. package/dist/runner/retries-option.js +12 -0
  48. package/dist/runner/retry.d.ts +13 -0
  49. package/dist/runner/retry.js +43 -0
  50. package/dist/runner/run-test.d.ts +8 -1
  51. package/dist/runner/run-test.js +34 -2
  52. package/dist/runner/run.d.ts +5 -2
  53. package/dist/runner/run.js +6 -1
  54. package/dist/runner/skip.d.ts +10 -0
  55. package/dist/runner/skip.js +18 -0
  56. package/dist/runner/suite.d.ts +24 -0
  57. package/dist/runner/suite.js +73 -0
  58. package/dist/runner/test-error.js +4 -0
  59. package/dist/runner/test-runner.js +54 -20
  60. package/dist/runner/worker-pool.d.ts +9 -1
  61. package/dist/runner/worker-pool.js +35 -2
  62. package/dist/runner/worker-protocol.d.ts +3 -0
  63. package/dist/runner/worker-session.js +17 -4
  64. package/dist/runner/worker.js +3 -0
  65. package/dist/steps/model.d.ts +3 -0
  66. package/dist/steps/parse.js +32 -7
  67. package/dist/types/config.d.ts +15 -1
  68. package/dist/types/test.d.ts +55 -3
  69. 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](docs/running-and-reports.md#parallel-runs)).
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](docs/flows.md)).
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](docs/recorder.md)). The Electron app that ties them together lives in a separate repository (see the [roadmap](docs/roadmap.md)).
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](docs/roadmap.md).
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](docs/cli.md).
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](docs/pages-and-browsers.md#which-browser-runs)). The Firefox installed on the machine is not used.
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](docs/cli.md).
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](docs/configuration.md#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/`.
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](docs/flows.md).
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](docs/environments-and-secrets.md).
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 | What it covers |
127
- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
128
- | [Writing tests](docs/writing-tests.md) | `test`, `describe`, fixtures, how a test passes or fails |
129
- | [Locators](docs/locators.md) | Finding elements, chaining, fallbacks, actions and their auto-waiting |
130
- | [Assertions](docs/assertions.md) | `expect` for elements and for plain values |
131
- | [Pages and browsers](docs/pages-and-browsers.md) | Navigation, waits, screenshots, PDF, viewport, emulation, dialogs, downloads, contexts, cookies |
132
- | [Network](docs/network.md) | Waiting for traffic, mocking, blocking, response bodies |
133
- | [Configuration](docs/configuration.md) | Every option in `samurai.config.ts`, CLI flags and precedence |
134
- | [Environments and secrets](docs/environments-and-secrets.md) | Per-environment settings, variables, secrets and masking |
135
- | [Command line](docs/cli.md) | `samurai run`, `list`, `record` and `init`, options, output, exit codes, `--json` |
136
- | [Flows](docs/flows.md) | `samurai flow list`, `check` and `run`: graphs of tests, groups and steps, reports, CI |
137
- | [Writing a step](docs/steps.md) | `defineStep`: flow steps of your own, the built-in steps |
138
- | [Running tests and reports](docs/running-and-reports.md) | What a run does, `report.json`, page logs, timeouts |
139
- | [Steps and the recorder](docs/recorder.md) | The step codec, the recording engine and the recorder toolbar |
140
- | [Embedding the framework](docs/embedding.md) | 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](docs/roadmap.md) | What is built and what is still open |
143
-
144
- The [documentation index](docs/README.md) lists every guide by topic.
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";
@@ -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 failed = summary.tests.filter((test) => test.status !== "success");
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
@@ -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)
@@ -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 { FlowEvent, FlowSummary, NodeTest } from "./events.js";
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
- /** The flow has errors; nothing was run */
10
- export declare class FlowCheckError extends Error {
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 {};