@itmetsam/samurai-framework 0.12.1 → 0.15.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/README.md +29 -29
- package/dist/api.d.ts +2 -2
- package/dist/api.js +1 -1
- package/dist/cli/console-reporter.js +6 -2
- package/dist/cli/main.js +1 -1
- package/dist/config/config.js +3 -0
- package/dist/config/forbid-only.d.ts +5 -0
- package/dist/config/forbid-only.js +9 -0
- package/dist/flows/run/project.js +22 -9
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +47 -6
- 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/pooled-run.d.ts +7 -1
- package/dist/runner/pooled-run.js +38 -10
- 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 +8 -0
- package/dist/runner/reporter.js +22 -1
- package/dist/runner/run-test.d.ts +6 -1
- package/dist/runner/run-test.js +27 -1
- package/dist/runner/run.d.ts +4 -2
- package/dist/runner/run.js +5 -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 +43 -18
- package/dist/runner/worker-pool.d.ts +8 -1
- package/dist/runner/worker-pool.js +34 -2
- package/dist/runner/worker-session.js +11 -1
- package/dist/steps/parse.js +16 -0
- package/dist/types/config.d.ts +8 -1
- package/dist/types/test.d.ts +43 -1
- package/package.json +4 -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";
|
|
@@ -32,12 +32,15 @@ export class ConsoleReporter {
|
|
|
32
32
|
const took = "duration" in result
|
|
33
33
|
? ` ${this.paint(90, `(${seconds(result.duration)})`)}`
|
|
34
34
|
: "";
|
|
35
|
+
if (result.status === "skipped")
|
|
36
|
+
return this.paint(90, `- ${name}${this.tag(browser)} (${result.annotation === "fixme" ? "fixme" : "skipped"}${result.reason ? `: ${result.reason}` : ""})`);
|
|
35
37
|
return result.status === "success"
|
|
36
38
|
? `${this.paint(32, "✔")} ${name}${this.tag(browser)}${took}`
|
|
37
39
|
: `${this.paint(31, "✖")} ${name}${this.tag(browser)}${took}`;
|
|
38
40
|
}
|
|
39
41
|
summary(summary) {
|
|
40
|
-
const
|
|
42
|
+
const skipped = summary.tests.filter((test) => test.status === "skipped");
|
|
43
|
+
const failed = summary.tests.filter((test) => test.status !== "success" && test.status !== "skipped");
|
|
41
44
|
for (const test of failed) {
|
|
42
45
|
if (!("name" in test))
|
|
43
46
|
continue;
|
|
@@ -50,10 +53,11 @@ export class ConsoleReporter {
|
|
|
50
53
|
if ("file" in test)
|
|
51
54
|
this.write(` ${this.paint(90, test.file)}\n`);
|
|
52
55
|
}
|
|
53
|
-
const passed = summary.tests.length - failed.length;
|
|
56
|
+
const passed = summary.tests.length - failed.length - skipped.length;
|
|
54
57
|
const parts = [
|
|
55
58
|
failed.length > 0 && this.paint(31, `${failed.length} failed`),
|
|
56
59
|
this.paint(32, `${passed} passed`),
|
|
60
|
+
skipped.length > 0 && this.paint(90, `${skipped.length} skipped`),
|
|
57
61
|
].filter(Boolean);
|
|
58
62
|
this.write(`\n${parts.join(", ")} ${this.paint(90, `(${seconds(summary.duration)})`)}\n`);
|
|
59
63
|
}
|
package/dist/cli/main.js
CHANGED
|
@@ -59,7 +59,7 @@ export async function main(argv, io) {
|
|
|
59
59
|
io.out(`${JSON.stringify(tests, null, 2)}\n`);
|
|
60
60
|
else
|
|
61
61
|
for (const found of tests)
|
|
62
|
-
io.out(`${found.name}\n ${found.file}\n`);
|
|
62
|
+
io.out(`${found.name}${found.modifier ? ` [${found.modifier}]` : ""}\n ${found.file}\n`);
|
|
63
63
|
return 0;
|
|
64
64
|
}
|
|
65
65
|
const human = values.json
|
package/dist/config/config.js
CHANGED
|
@@ -2,6 +2,7 @@ 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";
|
|
5
6
|
import { validateWorkersConfig } from "./workers.js";
|
|
6
7
|
let activeProject;
|
|
7
8
|
/** Makes `project` the one config, secrets, reports and downloads are resolved for; `undefined` ends it */
|
|
@@ -27,6 +28,7 @@ export async function loadConfig() {
|
|
|
27
28
|
if (activeProject?.config) {
|
|
28
29
|
validateBrowserConfig(activeProject.config);
|
|
29
30
|
validateWorkersConfig(activeProject.config);
|
|
31
|
+
validateForbidOnlyConfig(activeProject.config);
|
|
30
32
|
return activeProject.config;
|
|
31
33
|
}
|
|
32
34
|
const configUrl = path.join(projectDir(), "samurai.config.ts");
|
|
@@ -37,6 +39,7 @@ export async function loadConfig() {
|
|
|
37
39
|
}
|
|
38
40
|
validateBrowserConfig(config.default);
|
|
39
41
|
validateWorkersConfig(config.default);
|
|
42
|
+
validateForbidOnlyConfig(config.default);
|
|
40
43
|
return config.default;
|
|
41
44
|
}
|
|
42
45
|
/** 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
|
+
}
|
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { projectConfig } from "../../config/config.js";
|
|
3
3
|
import { checkFlow, withKeys, } from "../core/index.js";
|
|
4
4
|
import { DEFAULT_SRC_DIR, realPath, resolveGroups, testId, toPosix, } from "../../runner/groups.js";
|
|
5
|
-
import logger from "../../logger/index.js";
|
|
5
|
+
import logger, { keepLogFiles } from "../../logger/index.js";
|
|
6
6
|
import { resolveWorkers } from "../../runner/workers-option.js";
|
|
7
7
|
import { withProject } from "../../runner/project.js";
|
|
8
8
|
import { browsersFor } from "../../runner/browsers.js";
|
|
@@ -56,7 +56,10 @@ async function runOnPool(pool, items, onEvent, signal) {
|
|
|
56
56
|
});
|
|
57
57
|
if (signal.aborted)
|
|
58
58
|
return "cancelled";
|
|
59
|
-
|
|
59
|
+
// A skipped test is not a failure
|
|
60
|
+
return results.every((r) => r.status === "success" || r.status === "skipped")
|
|
61
|
+
? "passed"
|
|
62
|
+
: "failed";
|
|
60
63
|
}
|
|
61
64
|
/** The tests, and the browsers, of each Test and Group node of a flow; a Test node uses the project's browsers */
|
|
62
65
|
export function nodeTestsOf(flow, tests, groups) {
|
|
@@ -152,13 +155,7 @@ export async function checkFlowInProject(options) {
|
|
|
152
155
|
const { flow, problems } = await prepare(options);
|
|
153
156
|
return { flow, problems };
|
|
154
157
|
}
|
|
155
|
-
|
|
156
|
-
* Checks and runs a flow of a project: loads its config, lists its tests and groups, refuses to start
|
|
157
|
-
* when `checkFlow` finds errors (`FlowCheckError`), then walks the flow; each Test and Group node runs
|
|
158
|
-
* its tests through `runTests`, one after the other, or on one shared worker pool when `workers` is above 1.
|
|
159
|
-
* One at a time per process.
|
|
160
|
-
*/
|
|
161
|
-
export async function runFlowInProject(options) {
|
|
158
|
+
async function walkFlowInProject(options) {
|
|
162
159
|
const { projectDir, flow, unsupported, config, steps, name, environment, tests, groups, problems, } = await prepare(options);
|
|
163
160
|
const errors = problems.filter((p) => p.level === "error");
|
|
164
161
|
if (errors.length)
|
|
@@ -250,3 +247,19 @@ export async function runFlowInProject(options) {
|
|
|
250
247
|
await pool?.dispose();
|
|
251
248
|
}
|
|
252
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Checks and runs a flow of a project: loads its config, lists its tests and groups, refuses to start
|
|
252
|
+
* when `checkFlow` finds errors (`FlowCheckError`), then walks the flow; each Test and Group node runs
|
|
253
|
+
* its tests through `runTests`, one after the other, or on one shared worker pool when `workers` is above 1.
|
|
254
|
+
* One at a time per process.
|
|
255
|
+
*/
|
|
256
|
+
export async function runFlowInProject(options) {
|
|
257
|
+
// One log for the whole run, across the project sessions it opens one after the other
|
|
258
|
+
const releaseLogs = keepLogFiles(path.resolve(options.projectDir ?? process.cwd()));
|
|
259
|
+
try {
|
|
260
|
+
return await walkFlowInProject(options);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
releaseLogs();
|
|
264
|
+
}
|
|
265
|
+
}
|
package/dist/logger/index.d.ts
CHANGED
|
@@ -5,4 +5,10 @@ export declare function consoleLine(entry: {
|
|
|
5
5
|
meta?: Record<string, unknown> | undefined;
|
|
6
6
|
}): string;
|
|
7
7
|
declare const logger: import("winston").Logger;
|
|
8
|
+
/**
|
|
9
|
+
* Writes the log to `error.log` and `combined.log` in `<projectDir>/logs/` until the returned function is called.
|
|
10
|
+
* Nothing is written to disk before a project is in use, so a command that has none (`samurai --version`) leaves
|
|
11
|
+
* no folder behind. Holders of the same project share the files; the files close with the last one.
|
|
12
|
+
*/
|
|
13
|
+
export declare function keepLogFiles(projectDir: string): () => void;
|
|
8
14
|
export default logger;
|
package/dist/logger/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { createLogger, format, transports } from "winston";
|
|
2
3
|
import { maskFormat } from "../config/mask.js";
|
|
3
4
|
/** Metadata every line carries (`defaultMeta`): not worth printing on each console line */
|
|
@@ -15,7 +16,7 @@ const logger = createLogger({
|
|
|
15
16
|
}), format.errors({ stack: true }), format.splat(), format.metadata({ key: "meta" }), maskFormat(), format.json({ space: 2 })),
|
|
16
17
|
defaultMeta: { service: "samurai-framework" },
|
|
17
18
|
transports: [
|
|
18
|
-
// The console shows the log; `logs
|
|
19
|
+
// The console shows the log; the files of a project's `logs/` folder are added while a project is in use (`keepLogFiles`)
|
|
19
20
|
new transports.Console({
|
|
20
21
|
// The `samurai` command quiets this to "warn"; the log files keep everything
|
|
21
22
|
level: process.env.SAMURAI_LOG_LEVEL ?? "debug",
|
|
@@ -27,11 +28,51 @@ const logger = createLogger({
|
|
|
27
28
|
meta: meta,
|
|
28
29
|
}))),
|
|
29
30
|
}),
|
|
30
|
-
new transports.File({
|
|
31
|
-
filename: `logs/error.log`,
|
|
32
|
-
level: "error",
|
|
33
|
-
}),
|
|
34
|
-
new transports.File({ filename: `logs/combined.log` }),
|
|
35
31
|
],
|
|
36
32
|
});
|
|
33
|
+
/** The files the log is written to now: `error.log` keeps errors, `combined.log` everything */
|
|
34
|
+
let files;
|
|
35
|
+
/**
|
|
36
|
+
* Writes the log to `error.log` and `combined.log` in `<projectDir>/logs/` until the returned function is called.
|
|
37
|
+
* Nothing is written to disk before a project is in use, so a command that has none (`samurai --version`) leaves
|
|
38
|
+
* no folder behind. Holders of the same project share the files; the files close with the last one.
|
|
39
|
+
*/
|
|
40
|
+
export function keepLogFiles(projectDir) {
|
|
41
|
+
const dir = path.join(projectDir, "logs");
|
|
42
|
+
if (files && files.dir !== dir)
|
|
43
|
+
closeFiles();
|
|
44
|
+
files ??= {
|
|
45
|
+
dir,
|
|
46
|
+
transports: [
|
|
47
|
+
new transports.File({
|
|
48
|
+
filename: path.join(dir, "error.log"),
|
|
49
|
+
level: "error",
|
|
50
|
+
}),
|
|
51
|
+
new transports.File({ filename: path.join(dir, "combined.log") }),
|
|
52
|
+
],
|
|
53
|
+
holders: 0,
|
|
54
|
+
};
|
|
55
|
+
const mine = files;
|
|
56
|
+
if (mine.holders === 0)
|
|
57
|
+
for (const t of mine.transports)
|
|
58
|
+
logger.add(t);
|
|
59
|
+
mine.holders++;
|
|
60
|
+
let released = false;
|
|
61
|
+
return () => {
|
|
62
|
+
if (released)
|
|
63
|
+
return;
|
|
64
|
+
released = true;
|
|
65
|
+
if (--mine.holders === 0 && files === mine)
|
|
66
|
+
closeFiles();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function closeFiles() {
|
|
70
|
+
if (!files)
|
|
71
|
+
return;
|
|
72
|
+
for (const t of files.transports) {
|
|
73
|
+
logger.remove(t);
|
|
74
|
+
t.close?.();
|
|
75
|
+
}
|
|
76
|
+
files = undefined;
|
|
77
|
+
}
|
|
37
78
|
export default logger;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SupportedBrowser } from "../types/browser.js";
|
|
2
|
+
import type { AllHookFixtures, RegisteredTestCase } from "../types/test.js";
|
|
3
|
+
import { type HookError } from "./hooks.js";
|
|
4
|
+
export interface AllHooksOptions {
|
|
5
|
+
/** The limit of each beforeAll / afterAll hook, in ms */
|
|
6
|
+
timeout: number;
|
|
7
|
+
/** The fixtures the hooks of a browser get */
|
|
8
|
+
fixtures(browser: SupportedBrowser): AllHookFixtures;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The `beforeAll` and `afterAll` hooks of one worker: each runs once per suite and browser, `beforeAll` when the
|
|
12
|
+
* first test of the suite arrives and `afterAll` when the worker is done
|
|
13
|
+
*/
|
|
14
|
+
export declare class AllHooks {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly scopes;
|
|
17
|
+
/** In the order the suites were entered */
|
|
18
|
+
private readonly entered;
|
|
19
|
+
constructor(options: AllHooksOptions);
|
|
20
|
+
/** Runs the `beforeAll` hooks of the suites around `test` that this worker has not run for `browser`. Rejects with the `HookError` of one that failed, now or earlier */
|
|
21
|
+
ensure(test: RegisteredTestCase, browser: SupportedBrowser): Promise<void>;
|
|
22
|
+
/** Runs the `afterAll` hooks of every suite entered, last entered first, and returns the errors; a second call has nothing to do */
|
|
23
|
+
closeAll(): Promise<HookError[]>;
|
|
24
|
+
private enter;
|
|
25
|
+
private runBefore;
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { runHook } from "./hooks.js";
|
|
2
|
+
import { suiteChain } from "./suite.js";
|
|
3
|
+
/**
|
|
4
|
+
* The `beforeAll` and `afterAll` hooks of one worker: each runs once per suite and browser, `beforeAll` when the
|
|
5
|
+
* first test of the suite arrives and `afterAll` when the worker is done
|
|
6
|
+
*/
|
|
7
|
+
export class AllHooks {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.scopes = new Map();
|
|
11
|
+
/** In the order the suites were entered */
|
|
12
|
+
this.entered = [];
|
|
13
|
+
}
|
|
14
|
+
/** Runs the `beforeAll` hooks of the suites around `test` that this worker has not run for `browser`. Rejects with the `HookError` of one that failed, now or earlier */
|
|
15
|
+
async ensure(test, browser) {
|
|
16
|
+
for (const suite of suiteChain(test.suite)) {
|
|
17
|
+
if (!suite.beforeAll?.length && !suite.afterAll?.length)
|
|
18
|
+
continue;
|
|
19
|
+
await this.enter(suite, browser).ready;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Runs the `afterAll` hooks of every suite entered, last entered first, and returns the errors; a second call has nothing to do */
|
|
23
|
+
async closeAll() {
|
|
24
|
+
const errors = [];
|
|
25
|
+
const entered = this.entered.splice(0).reverse();
|
|
26
|
+
this.scopes.clear();
|
|
27
|
+
for (const { suite, browser, ready } of entered) {
|
|
28
|
+
await ready.catch(() => undefined);
|
|
29
|
+
for (const hook of suite.afterAll ?? []) {
|
|
30
|
+
try {
|
|
31
|
+
await runHook("afterAll", hook, this.options.fixtures(browser), this.options.timeout);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
errors.push(error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return errors;
|
|
39
|
+
}
|
|
40
|
+
enter(suite, browser) {
|
|
41
|
+
const ofSuite = this.scopes.get(suite) ?? new Map();
|
|
42
|
+
this.scopes.set(suite, ofSuite);
|
|
43
|
+
const known = ofSuite.get(browser);
|
|
44
|
+
if (known)
|
|
45
|
+
return known;
|
|
46
|
+
const scope = {
|
|
47
|
+
suite,
|
|
48
|
+
browser,
|
|
49
|
+
ready: this.runBefore(suite, browser),
|
|
50
|
+
};
|
|
51
|
+
// Rejections are handled by the callers of `ensure`; one nobody waits for is not an error of the process
|
|
52
|
+
scope.ready.catch(() => undefined);
|
|
53
|
+
ofSuite.set(browser, scope);
|
|
54
|
+
this.entered.push(scope);
|
|
55
|
+
return scope;
|
|
56
|
+
}
|
|
57
|
+
async runBefore(suite, browser) {
|
|
58
|
+
for (const hook of suite.beforeAll ?? [])
|
|
59
|
+
await runHook("beforeAll", hook, this.options.fixtures(browser), this.options.timeout);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RegisteredTestCase } from "../types/test.js";
|
|
2
|
+
/** `test.only` is in the specs and the config refuses it: one error that lists every focused test */
|
|
3
|
+
export declare function assertNoOnly(all: readonly RegisteredTestCase[], forbid: boolean, srcDir: string): void;
|
|
4
|
+
/** When any of the selected tests is focused, only the focused ones; the others are not run and not reported */
|
|
5
|
+
export declare function narrowToOnly(selected: RegisteredTestCase[]): RegisteredTestCase[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { toPosix } from "./groups.js";
|
|
3
|
+
/** `test.only` is in the specs and the config refuses it: one error that lists every focused test */
|
|
4
|
+
export function assertNoOnly(all, forbid, srcDir) {
|
|
5
|
+
if (!forbid)
|
|
6
|
+
return;
|
|
7
|
+
const focused = all.filter((test) => test.modifier === "only");
|
|
8
|
+
if (focused.length === 0)
|
|
9
|
+
return;
|
|
10
|
+
throw new Error(focused
|
|
11
|
+
.map((test) => `test.only is not allowed (forbidOnly): ${toPosix(path.relative(srcDir, test.file))}: ${test.name}`)
|
|
12
|
+
.join("\n"));
|
|
13
|
+
}
|
|
14
|
+
/** When any of the selected tests is focused, only the focused ones; the others are not run and not reported */
|
|
15
|
+
export function narrowToOnly(selected) {
|
|
16
|
+
return selected.some((test) => test.modifier === "only")
|
|
17
|
+
? selected.filter((test) => test.modifier === "only")
|
|
18
|
+
: selected;
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Suite, TestFixtures } from "../types/test.js";
|
|
2
|
+
import { type HookKind } from "./suite.js";
|
|
3
|
+
/** A hook threw or ran out of time: the message names the hook (`beforeEach hook failed: <message>`) */
|
|
4
|
+
export declare class HookError extends Error {
|
|
5
|
+
readonly kind: HookKind;
|
|
6
|
+
readonly cause: unknown;
|
|
7
|
+
constructor(kind: HookKind, cause: unknown);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Runs one hook with its fixtures and waits for it. Anything it throws, and a hook that is still running after
|
|
11
|
+
* `limitMs` (when given), becomes a `HookError` of that kind.
|
|
12
|
+
*/
|
|
13
|
+
export declare function runHook<F>(kind: HookKind, hook: (fixtures: F) => Promise<void> | void, fixtures: F, limitMs?: number): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Runs a test body between the `beforeEach` and `afterEach` hooks of its suites.
|
|
16
|
+
*
|
|
17
|
+
* - A failing `beforeEach` stops the other `beforeEach` hooks and the body; the `afterEach` hooks still run.
|
|
18
|
+
* - The `afterEach` hooks run when the body ended by itself (it passed, threw or skipped), not when `signal` says
|
|
19
|
+
* the test was cut short (timeout, abort, closed browser): the time or the page is gone. They all run, even
|
|
20
|
+
* after one fails.
|
|
21
|
+
* - What it throws: the `beforeEach` failure, else the body's error, else the first `afterEach` failure. A skip is
|
|
22
|
+
* overruled by a failing `afterEach`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runWithEachHooks(suite: Suite, fixtures: TestFixtures, signal: AbortSignal, body: () => Promise<void>): Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isSkipSignal } from "./skip.js";
|
|
2
|
+
import { afterEachOf, beforeEachOf } from "./suite.js";
|
|
3
|
+
/** A hook threw or ran out of time: the message names the hook (`beforeEach hook failed: <message>`) */
|
|
4
|
+
export class HookError extends Error {
|
|
5
|
+
constructor(kind, cause) {
|
|
6
|
+
super(`${kind} hook failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
7
|
+
this.kind = kind;
|
|
8
|
+
this.cause = cause;
|
|
9
|
+
this.name = "HookError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Runs one hook with its fixtures and waits for it. Anything it throws, and a hook that is still running after
|
|
14
|
+
* `limitMs` (when given), becomes a `HookError` of that kind.
|
|
15
|
+
*/
|
|
16
|
+
export async function runHook(kind, hook, fixtures, limitMs) {
|
|
17
|
+
let timer;
|
|
18
|
+
try {
|
|
19
|
+
const running = Promise.resolve().then(() => hook(fixtures));
|
|
20
|
+
if (limitMs === undefined)
|
|
21
|
+
return await running;
|
|
22
|
+
await Promise.race([
|
|
23
|
+
running,
|
|
24
|
+
new Promise((_, reject) => {
|
|
25
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${limitMs / 1000} seconds`)), limitMs);
|
|
26
|
+
}),
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// `test.skip(condition)` in a hook skips the test: it is not a failure of the hook
|
|
31
|
+
if (isSkipSignal(error))
|
|
32
|
+
throw error;
|
|
33
|
+
throw new HookError(kind, error);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Runs a test body between the `beforeEach` and `afterEach` hooks of its suites.
|
|
41
|
+
*
|
|
42
|
+
* - A failing `beforeEach` stops the other `beforeEach` hooks and the body; the `afterEach` hooks still run.
|
|
43
|
+
* - The `afterEach` hooks run when the body ended by itself (it passed, threw or skipped), not when `signal` says
|
|
44
|
+
* the test was cut short (timeout, abort, closed browser): the time or the page is gone. They all run, even
|
|
45
|
+
* after one fails.
|
|
46
|
+
* - What it throws: the `beforeEach` failure, else the body's error, else the first `afterEach` failure. A skip is
|
|
47
|
+
* overruled by a failing `afterEach`.
|
|
48
|
+
*/
|
|
49
|
+
export async function runWithEachHooks(suite, fixtures, signal, body) {
|
|
50
|
+
let primary;
|
|
51
|
+
let failed = false;
|
|
52
|
+
try {
|
|
53
|
+
for (const hook of beforeEachOf(suite))
|
|
54
|
+
await runHook("beforeEach", hook, fixtures);
|
|
55
|
+
await body();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
primary = error;
|
|
59
|
+
failed = true;
|
|
60
|
+
}
|
|
61
|
+
let afterError;
|
|
62
|
+
let afterFailed = false;
|
|
63
|
+
if (!signal.aborted) {
|
|
64
|
+
for (const hook of afterEachOf(suite)) {
|
|
65
|
+
try {
|
|
66
|
+
await runHook("afterEach", hook, fixtures);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!afterFailed)
|
|
70
|
+
afterError = error;
|
|
71
|
+
afterFailed = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (afterFailed && (!failed || isSkipSignal(primary)))
|
|
76
|
+
throw afterError;
|
|
77
|
+
if (failed)
|
|
78
|
+
throw primary;
|
|
79
|
+
}
|
|
@@ -27,6 +27,11 @@ export declare function planItems(selected: RegisteredTestCase[], browsers: Supp
|
|
|
27
27
|
* and `configGroups` are the same list, resolved and as written.
|
|
28
28
|
*/
|
|
29
29
|
export declare function serialKeyOf(groups: ResolvedGroup[], configGroups: SamuraiGroup[] | undefined, srcDir: string): (test: RegisteredTestCase) => string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* How many items can run at the same time: each item alone counts one, and the items of one serial key together
|
|
32
|
+
* count one. More workers than this would only sit idle.
|
|
33
|
+
*/
|
|
34
|
+
export declare function concurrency(items: WorkItem[]): number;
|
|
30
35
|
/**
|
|
31
36
|
* How a run goes: through the pool, in this process, or in this process because the workers could not read the
|
|
32
37
|
* config (it was given as an object; a worker reads `samurai.config.ts` from the project folder)
|
|
@@ -37,7 +42,8 @@ export declare function poolMode(input: {
|
|
|
37
42
|
inlineConfig: boolean;
|
|
38
43
|
}): "pool" | "in-process" | "inline-config";
|
|
39
44
|
/**
|
|
40
|
-
* Runs the planned items on the pool and reports them as they happen.
|
|
45
|
+
* Runs the planned items on the pool and reports them as they happen. Tests that do not run (skip, fixme) are
|
|
46
|
+
* reported first, here, and never reach a worker. An item the pool answers without events
|
|
41
47
|
* (a queued one cut short by an abort, or one whose worker died before it began) is reported here at the end.
|
|
42
48
|
*/
|
|
43
49
|
export declare function runPooled(pool: PooledRunner, planned: PlannedTest[], items: WorkItem[], reporter: TestReporter, signal?: AbortSignal): Promise<void>;
|