@itmetsam/samurai-framework 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/browser.js +2 -1
- package/dist/browser/page-network.js +3 -2
- package/dist/browser/page-settings.js +2 -5
- package/dist/browser/page.js +2 -1
- package/dist/browser/session.d.ts +1 -1
- package/dist/browser/session.js +1 -1
- package/dist/browser/viewport.d.ts +0 -5
- package/dist/browser/viewport.js +0 -2
- package/dist/capabilities.d.ts +32 -0
- package/dist/capabilities.js +30 -0
- package/dist/{runner/browsers.js → config/browsers-for.js} +1 -2
- package/dist/config/config.d.ts +3 -1
- package/dist/config/config.js +12 -11
- package/dist/config/defaults.d.ts +41 -0
- package/dist/config/defaults.js +38 -0
- package/dist/config/resolved-config.d.ts +106 -0
- package/dist/config/resolved-config.js +112 -0
- package/dist/config/run-settings.d.ts +2 -0
- package/dist/config/run-settings.js +2 -3
- package/dist/config/timeouts.js +1 -2
- package/dist/{runner → config}/workers-option.js +1 -1
- package/dist/flows/run/project.js +5 -4
- package/dist/network/data-collector.js +1 -2
- package/dist/network/router.js +1 -2
- package/dist/project/files-store.js +2 -1
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +1 -0
- package/dist/recorder/session-browser.js +1 -1
- package/dist/recorder/session.d.ts +5 -2
- package/dist/recorder/session.js +18 -3
- package/dist/runner/groups.d.ts +4 -5
- package/dist/runner/groups.js +2 -5
- package/dist/runner/page-logs-report.js +6 -3
- package/dist/runner/project.js +2 -2
- package/dist/runner/retries-option.js +2 -1
- package/dist/runner/run.d.ts +12 -2
- package/dist/runner/run.js +21 -7
- package/dist/runner/select.js +4 -4
- package/dist/runner/test-dir.js +2 -1
- package/dist/runner/test-runner.d.ts +8 -2
- package/dist/runner/test-runner.js +11 -7
- package/dist/steps/edit.d.ts +10 -0
- package/dist/steps/edit.js +4 -0
- package/dist/steps/index.d.ts +2 -2
- package/dist/steps/index.js +1 -1
- package/dist/steps/model.d.ts +17 -0
- package/dist/steps/parse.d.ts +6 -1
- package/dist/steps/parse.js +54 -10
- package/dist/transport/bidi-connection.d.ts +0 -2
- package/dist/transport/bidi-connection.js +1 -2
- package/package.json +6 -2
- /package/dist/{runner/browsers.d.ts → config/browsers-for.d.ts} +0 -0
- /package/dist/{runner → config}/workers-option.d.ts +0 -0
|
@@ -2,6 +2,7 @@ import { constants, createReadStream } from "node:fs";
|
|
|
2
2
|
import { copyFile, mkdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { DEFAULT_FILES_DIR } from "../config/defaults.js";
|
|
5
6
|
/** Files bigger than this aren't stored while recording */
|
|
6
7
|
export const RECORD_LIMIT = 50 * 1024 * 1024;
|
|
7
8
|
const NAME_LIMIT = 120;
|
|
@@ -20,7 +21,7 @@ export function cleanFileName(name) {
|
|
|
20
21
|
return cleaned === "" ? "file" : cleaned;
|
|
21
22
|
}
|
|
22
23
|
/** The files folder: absolute, and relative to the project with forward slashes */
|
|
23
|
-
export function filesFolder(projectDir, filesDir =
|
|
24
|
+
export function filesFolder(projectDir, filesDir = DEFAULT_FILES_DIR) {
|
|
24
25
|
const absolute = path.resolve(projectDir, filesDir);
|
|
25
26
|
return {
|
|
26
27
|
absolute,
|
package/dist/project/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { ConfigEditError, editConfig, hasConfigPath, propertyText, type ConfigVa
|
|
|
2
2
|
export { addVariable, ExistsError, VARIABLE_NAME } from "./variables.js";
|
|
3
3
|
export { isGitIgnored, SECRET_NAME, setSecret } from "./secrets-file.js";
|
|
4
4
|
export { cleanFileName, filesFolder, isFolderIgnored, RECORD_LIMIT, storeFile, } from "./files-store.js";
|
|
5
|
+
export { loadConfig, type ResolvedConfig, type ResolvedEnvironment, type ResolvedIgnoreError, } from "../config/resolved-config.js";
|
package/dist/project/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { ConfigEditError, editConfig, hasConfigPath, propertyText, } from "./con
|
|
|
2
2
|
export { addVariable, ExistsError, VARIABLE_NAME } from "./variables.js";
|
|
3
3
|
export { isGitIgnored, SECRET_NAME, setSecret } from "./secrets-file.js";
|
|
4
4
|
export { cleanFileName, filesFolder, isFolderIgnored, RECORD_LIMIT, storeFile, } from "./files-store.js";
|
|
5
|
+
export { loadConfig, } from "../config/resolved-config.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Browser } from "../browser/browser.js";
|
|
2
2
|
import { ensureWarm } from "../browser/warm-up.js";
|
|
3
3
|
import { readConfig } from "../config/config.js";
|
|
4
|
-
import { browsersFor } from "../
|
|
4
|
+
import { browsersFor } from "../config/browsers-for.js";
|
|
5
5
|
import { DEFAULT_PORT } from "../runner/ports.js";
|
|
6
6
|
/**
|
|
7
7
|
* The browser a recording uses: the one asked for, else the first of the project's (recording in two browsers at
|
|
@@ -31,8 +31,10 @@ export interface RecordSpecOptions extends ProjectOptions {
|
|
|
31
31
|
/** Called after each step is written to the file, with the file's new content */
|
|
32
32
|
onEvent?: (event: RecorderEvent, source: string) => void;
|
|
33
33
|
/**
|
|
34
|
-
* Called
|
|
35
|
-
*
|
|
34
|
+
* Called once with the recording's first status right after `onStarted` resolves (`{ state: "recording" }`,
|
|
35
|
+
* and `toolbar: false` if the toolbar had already failed to load), then whenever the recording is paused
|
|
36
|
+
* or resumed, or the toolbar fails (or comes back) on a page. Statuses that arrive while `onStarted` runs are held
|
|
37
|
+
* and sent once it resolves, after the first status.
|
|
36
38
|
*/
|
|
37
39
|
onStatus?: (status: {
|
|
38
40
|
state: "recording" | "paused";
|
|
@@ -46,6 +48,7 @@ export interface RecordSpecOptions extends ProjectOptions {
|
|
|
46
48
|
browser: Browser;
|
|
47
49
|
}) => void | Promise<void>;
|
|
48
50
|
}
|
|
51
|
+
/** What a recording returns; every field is always present, empty when there is nothing to report. */
|
|
49
52
|
export interface RecordResult {
|
|
50
53
|
file: string;
|
|
51
54
|
/** Full name of the test the steps went into */
|
package/dist/recorder/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { loadRawConfig, projectDir } from "../config/config.js";
|
|
4
4
|
import { loadSecrets } from "../config/secrets.js";
|
|
5
5
|
import { filesFolder, isFolderIgnored } from "../project/index.js";
|
|
6
6
|
import { withProject } from "../runner/project.js";
|
|
@@ -30,11 +30,21 @@ export async function recordSpec(options) {
|
|
|
30
30
|
...options,
|
|
31
31
|
headless: options.headless ?? settings.headless,
|
|
32
32
|
});
|
|
33
|
-
const loaded = await
|
|
33
|
+
const loaded = await loadRawConfig();
|
|
34
34
|
const folder = filesFolder(projectDir(), loaded.filesDir);
|
|
35
35
|
const hadFolder = existsSync(folder.absolute);
|
|
36
36
|
let recorder;
|
|
37
37
|
const addedSecrets = new Set();
|
|
38
|
+
// Statuses that come while `onStarted` runs wait behind the first one, which is always the first
|
|
39
|
+
let waiting = [];
|
|
40
|
+
let toolbarLoaded = true;
|
|
41
|
+
const onStatus = (status) => {
|
|
42
|
+
toolbarLoaded = status.toolbar;
|
|
43
|
+
if (waiting)
|
|
44
|
+
waiting.push(status);
|
|
45
|
+
else
|
|
46
|
+
options.onStatus?.(status);
|
|
47
|
+
};
|
|
38
48
|
let stopRequested;
|
|
39
49
|
const stopped = new Promise((resolve) => (stopRequested = resolve));
|
|
40
50
|
try {
|
|
@@ -50,7 +60,7 @@ export async function recordSpec(options) {
|
|
|
50
60
|
toolbar: {
|
|
51
61
|
...(await toolbarOptions(run, spec.source, projectDir(), loaded, testIndex)),
|
|
52
62
|
projectDir: projectDir(),
|
|
53
|
-
...(options.onStatus && { onStatus
|
|
63
|
+
...(options.onStatus && { onStatus }),
|
|
54
64
|
onSecretAdded: (name) => addedSecrets.add(name),
|
|
55
65
|
onStop: () => stopRequested(),
|
|
56
66
|
},
|
|
@@ -65,6 +75,11 @@ export async function recordSpec(options) {
|
|
|
65
75
|
},
|
|
66
76
|
});
|
|
67
77
|
await options.onStarted?.({ page, browser });
|
|
78
|
+
const queued = waiting ?? [];
|
|
79
|
+
waiting = undefined;
|
|
80
|
+
options.onStatus?.({ state: "recording", toolbar: toolbarLoaded });
|
|
81
|
+
for (const status of queued)
|
|
82
|
+
options.onStatus?.(status);
|
|
68
83
|
let windowClosed = false;
|
|
69
84
|
void browser.exited.then(() => (windowClosed = true));
|
|
70
85
|
await new Promise((resolve) => {
|
package/dist/runner/groups.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { SupportedBrowser } from "../types/browser.js";
|
|
2
2
|
import type { SamuraiGroup } from "../types/config.js";
|
|
3
|
-
/** The test folder when the config names none */
|
|
4
|
-
export declare const DEFAULT_SRC_DIR = "./src";
|
|
5
|
-
/** The spec pattern when a group names none */
|
|
6
|
-
export declare const DEFAULT_TEST_MATCH = "**/*.spec.ts";
|
|
7
3
|
/** A test as groups see it: its spec file (relative to `srcDir`, or any path inside it) and full name */
|
|
8
4
|
export interface GroupTest {
|
|
9
5
|
file: string;
|
|
@@ -16,7 +12,10 @@ export interface ResolvedGroup {
|
|
|
16
12
|
kind: "picked" | "pattern";
|
|
17
13
|
/** What selects the tests of a pattern group, for display */
|
|
18
14
|
pattern?: string;
|
|
19
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Ids of the found tests in the group: the spec file relative to `srcDir` with `/` separators, `::`, then the
|
|
17
|
+
* full name (`FoundTest.id` is the same)
|
|
18
|
+
*/
|
|
20
19
|
testIds: string[];
|
|
21
20
|
/** Picked tests that no longer exist */
|
|
22
21
|
missing: {
|
package/dist/runner/groups.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { matchesGlob, relative, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export const DEFAULT_SRC_DIR = "./src";
|
|
6
|
-
/** The spec pattern when a group names none */
|
|
7
|
-
export const DEFAULT_TEST_MATCH = "**/*.spec.ts";
|
|
3
|
+
import { DEFAULT_TEST_MATCH } from "../config/defaults.js";
|
|
4
|
+
import { browsersFor } from "../config/browsers-for.js";
|
|
8
5
|
/**
|
|
9
6
|
* The real path of `p` (symlinks resolved), or `p` itself when it does not exist. Registered test files come
|
|
10
7
|
* from stack traces, which resolve symlinks, so paths compared with them must be real too
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_LOGS_CAPTURE } from "../config/defaults.js";
|
|
1
2
|
import { toTestError } from "./test-error.js";
|
|
2
3
|
/** The page threw exceptions nobody caught, and config `logs.failOnPageError` says that fails the test */
|
|
3
4
|
export class PageErrorsError extends Error {
|
|
@@ -16,15 +17,17 @@ export class RouteErrorsError extends Error {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
function isIgnored(error, ignore) {
|
|
19
|
-
return ignore.some((pattern) => typeof pattern === "string"
|
|
20
|
+
return ignore.some((pattern) => typeof pattern === "string"
|
|
21
|
+
? error.message.includes(pattern)
|
|
22
|
+
: pattern.test(error.message));
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Decides, once a test body is done, whether page errors fail it and which log entries go in the report:
|
|
23
26
|
* `capture` "failures" (default) attaches them to failed tests, "all" always, "off" never.
|
|
24
27
|
*/
|
|
25
28
|
export function judgePageLogs(input) {
|
|
26
|
-
const { config, entries, errors, dropped, testFailed, allowPageErrors, routeErrors = [] } = input;
|
|
27
|
-
const capture = config?.capture ??
|
|
29
|
+
const { config, entries, errors, dropped, testFailed, allowPageErrors, routeErrors = [], } = input;
|
|
30
|
+
const capture = config?.capture ?? DEFAULT_LOGS_CAPTURE;
|
|
28
31
|
let error;
|
|
29
32
|
if (routeErrors.length > 0 && !testFailed) {
|
|
30
33
|
error = toTestError(new RouteErrorsError(routeErrors));
|
package/dist/runner/project.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { removeGeneratedFiles } from "../browser/generated-files.js";
|
|
3
|
-
import {
|
|
3
|
+
import { loadRawConfig, projectDir, setActiveProject, } from "../config/config.js";
|
|
4
4
|
import { resetSecrets } from "../config/mask.js";
|
|
5
5
|
import { setRunSettings } from "../config/run-settings.js";
|
|
6
6
|
import { keepLogFiles } from "../logger/index.js";
|
|
@@ -20,7 +20,7 @@ export async function withProject(options, work) {
|
|
|
20
20
|
const releaseLogs = keepLogFiles(path.resolve(dir ?? previousDir));
|
|
21
21
|
try {
|
|
22
22
|
setActiveProject({ dir: dir ?? previousDir, config, dataDir });
|
|
23
|
-
const loaded = config ?? (await
|
|
23
|
+
const loaded = config ?? (await loadRawConfig());
|
|
24
24
|
setActiveProject({ dir: dir ?? previousDir, config: loaded, dataDir });
|
|
25
25
|
resetSecrets();
|
|
26
26
|
const run = prepareRun(loaded, {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_RETRIES } from "../config/defaults.js";
|
|
1
2
|
import { parseRetries } from "../config/retries.js";
|
|
2
3
|
/** How many times a failing test is run again: the flag, else `SAMURAI_RETRIES`, else the config's `retries`, else 0 */
|
|
3
4
|
export function resolveRetries({ flag, env, config }) {
|
|
@@ -8,5 +9,5 @@ export function resolveRetries({ flag, env, config }) {
|
|
|
8
9
|
return parseRetries(fromEnv, "SAMURAI_RETRIES");
|
|
9
10
|
if (config !== undefined)
|
|
10
11
|
return parseRetries(config, "retries");
|
|
11
|
-
return
|
|
12
|
+
return DEFAULT_RETRIES;
|
|
12
13
|
}
|
package/dist/runner/run.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Modifier, TestSummary } from "../types/test.js";
|
|
2
2
|
import { type ProjectOptions } from "./project.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type ResolvedGroup } from "./groups.js";
|
|
4
4
|
import type { RunnerOptions } from "./options.js";
|
|
5
5
|
export interface RunTestsOptions extends RunnerOptions, ProjectOptions {
|
|
6
6
|
}
|
|
@@ -16,15 +16,25 @@ export interface RunTestsOptions extends RunnerOptions, ProjectOptions {
|
|
|
16
16
|
export declare function runTests(options?: RunTestsOptions): Promise<TestSummary>;
|
|
17
17
|
/** A test found in a spec file */
|
|
18
18
|
export interface FoundTest {
|
|
19
|
+
/**
|
|
20
|
+
* The id the groups use for this test (`ResolvedGroup.testIds`): its spec file relative to `srcDir` with `/`
|
|
21
|
+
* separators, then `::` and its full name, e.g. `checkout/cart.spec.ts::Cart > adds`
|
|
22
|
+
*/
|
|
23
|
+
id: string;
|
|
19
24
|
/** Full name: `describe` titles and the test title joined with " > " */
|
|
20
25
|
name: string;
|
|
21
26
|
file: string;
|
|
27
|
+
/** The spec file relative to `srcDir`, with `/` separators */
|
|
28
|
+
relativeFile: string;
|
|
22
29
|
/** `skip` or `fixme` (not run), or `only` (focused); absent for a plain test. A modifier on a `describe` counts. */
|
|
23
30
|
modifier?: Modifier;
|
|
24
31
|
/** The `{ reason }` given with the `skip` or `fixme` that applies */
|
|
25
32
|
reason?: string;
|
|
26
33
|
}
|
|
27
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Imports the project's specs without running anything and lists the tests the options select, each with the id
|
|
36
|
+
* its group uses and its spec file relative to `srcDir`
|
|
37
|
+
*/
|
|
28
38
|
export declare function listTests(options?: RunTestsOptions): Promise<FoundTest[]>;
|
|
29
39
|
/** Imports the project's specs without running anything and lists the config's groups with the tests each holds */
|
|
30
40
|
export declare function listGroups(options?: RunTestsOptions): Promise<ResolvedGroup[]>;
|
package/dist/runner/run.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import path from "path";
|
|
1
2
|
import { withProject } from "./project.js";
|
|
3
|
+
import { testId, toPosix } from "./groups.js";
|
|
2
4
|
import TestRunner from "./test-runner.js";
|
|
3
5
|
/**
|
|
4
6
|
* Sets the project up (config, environment, secrets), builds a runner from the rest of the options and hands both to
|
|
@@ -33,14 +35,26 @@ function inProject(options, work) {
|
|
|
33
35
|
export async function runTests(options = {}) {
|
|
34
36
|
return inProject(options, (runner) => runner.start());
|
|
35
37
|
}
|
|
36
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Imports the project's specs without running anything and lists the tests the options select, each with the id
|
|
40
|
+
* its group uses and its spec file relative to `srcDir`
|
|
41
|
+
*/
|
|
37
42
|
export async function listTests(options = {}) {
|
|
38
|
-
return inProject(options, async (runner) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
return inProject(options, async (runner) => {
|
|
44
|
+
const { tests, srcDir } = await runner.list();
|
|
45
|
+
return tests.map((test) => {
|
|
46
|
+
const { name, file, modifier, reason } = test;
|
|
47
|
+
const relativeFile = toPosix(path.relative(srcDir, file));
|
|
48
|
+
return {
|
|
49
|
+
id: testId(relativeFile, name),
|
|
50
|
+
name,
|
|
51
|
+
file,
|
|
52
|
+
relativeFile,
|
|
53
|
+
...(modifier && { modifier }),
|
|
54
|
+
...(reason !== undefined && { reason }),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
});
|
|
44
58
|
}
|
|
45
59
|
/** Imports the project's specs without running anything and lists the config's groups with the tests each holds */
|
|
46
60
|
export async function listGroups(options = {}) {
|
package/dist/runner/select.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { pathToFileURL } from "url";
|
|
3
|
-
import {
|
|
3
|
+
import { loadRawConfig } from "../config/config.js";
|
|
4
4
|
import logger from "../logger/index.js";
|
|
5
|
-
import { browsersFor } from "
|
|
5
|
+
import { browsersFor } from "../config/browsers-for.js";
|
|
6
6
|
import { findGroup, resolveGroups, testId, toPosix, } from "./groups.js";
|
|
7
7
|
import { assertModuleProject } from "./module-type.js";
|
|
8
8
|
import { clearRegistry, registeredTests } from "./registry.js";
|
|
@@ -40,7 +40,7 @@ export function idOf(test, srcDir) {
|
|
|
40
40
|
}
|
|
41
41
|
/** The config's groups, resolved against the tests the specs register */
|
|
42
42
|
async function resolveAll(tests) {
|
|
43
|
-
const config = await
|
|
43
|
+
const config = await loadRawConfig();
|
|
44
44
|
const { dir, srcDir } = realDirs(config.srcDir);
|
|
45
45
|
const groups = resolveGroups(config, tests.map(({ file, name }) => ({
|
|
46
46
|
file: toPosix(path.relative(srcDir, file)),
|
|
@@ -69,7 +69,7 @@ export function selectTests({ all, groups, srcDir }, options) {
|
|
|
69
69
|
*/
|
|
70
70
|
export async function browsersOfRun(groups, options) {
|
|
71
71
|
const { browsers: flag, group } = options;
|
|
72
|
-
const config = await
|
|
72
|
+
const config = await loadRawConfig();
|
|
73
73
|
const ofGroup = group !== undefined && !(flag && flag.length > 0)
|
|
74
74
|
? findGroup(groups, group).browsers
|
|
75
75
|
: undefined;
|
package/dist/runner/test-dir.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { projectDir } from "../config/config.js";
|
|
3
3
|
import { cleanName } from "../browser/evidence.js";
|
|
4
|
-
import { DEFAULT_SRC_DIR
|
|
4
|
+
import { DEFAULT_SRC_DIR } from "../config/defaults.js";
|
|
5
|
+
import { realPath, toPosix } from "./groups.js";
|
|
5
6
|
/** The project and source folders as real paths: registered test files come from stack traces, which resolve symlinks */
|
|
6
7
|
export function realDirs(configuredSrcDir) {
|
|
7
8
|
const dir = realPath(projectDir());
|
|
@@ -26,8 +26,14 @@ export default class TestRunner {
|
|
|
26
26
|
static init(run: PreparedRun, options?: RunnerOptions, project?: ProjectInfo): Promise<TestRunner>;
|
|
27
27
|
/** The groups of the config with the tests each holds; see `listGroups` */
|
|
28
28
|
groups(): Promise<ResolvedGroup[]>;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Imports the spec files and returns the tests the options select (by `group`, `testNames` and `grep`)
|
|
31
|
+
* with the real `srcDir` their ids are relative to
|
|
32
|
+
*/
|
|
33
|
+
list(): Promise<{
|
|
34
|
+
tests: RegisteredTestCase[];
|
|
35
|
+
srcDir: string;
|
|
36
|
+
}>;
|
|
31
37
|
/**
|
|
32
38
|
* Runs the selected tests in every browser of the run, one browser after the other, and returns the summary that
|
|
33
39
|
* was also written to the report. With more than one worker the tests run at the same time in worker processes.
|
|
@@ -2,12 +2,12 @@ import { glob } from "node:fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { ensureWarm } from "../browser/warm-up.js";
|
|
4
4
|
import { projectBrowser } from "../browsers/resolve.js";
|
|
5
|
-
import {
|
|
5
|
+
import { loadRawConfig, projectDir, readConfig } from "../config/config.js";
|
|
6
6
|
import { forbidsOnly } from "../config/forbid-only.js";
|
|
7
7
|
import logger from "../logger/index.js";
|
|
8
8
|
import { AllHooks } from "./all-hooks.js";
|
|
9
9
|
import { assertNoOnly, narrowToOnly } from "./focus.js";
|
|
10
|
-
import { DEFAULT_SRC_DIR, DEFAULT_TEST_MATCH } from "
|
|
10
|
+
import { DEFAULT_SRC_DIR, DEFAULT_TEST_MATCH } from "../config/defaults.js";
|
|
11
11
|
import { DEFAULT_PORT } from "./ports.js";
|
|
12
12
|
import TestReporter from "./reporter.js";
|
|
13
13
|
import { concurrency, planItems, poolMode, runPooled, serialKeyOf, } from "./pooled-run.js";
|
|
@@ -16,7 +16,7 @@ import { staticSkip } from "./skip.js";
|
|
|
16
16
|
import { browsersOfRun, loadSpecs, selectTests } from "./select.js";
|
|
17
17
|
import { resolveRetries } from "./retries-option.js";
|
|
18
18
|
import { runWithRetries } from "./retry.js";
|
|
19
|
-
import { resolveWorkers } from "
|
|
19
|
+
import { resolveWorkers } from "../config/workers-option.js";
|
|
20
20
|
import { WorkerPool } from "./worker-pool.js";
|
|
21
21
|
import { processWorkerFactory } from "./worker-process.js";
|
|
22
22
|
/** Finds the spec files, picks the tests and browsers of a run and runs each test in each browser */
|
|
@@ -48,9 +48,13 @@ export default class TestRunner {
|
|
|
48
48
|
async groups() {
|
|
49
49
|
return (await loadSpecs(this.testFiles)).groups;
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Imports the spec files and returns the tests the options select (by `group`, `testNames` and `grep`)
|
|
53
|
+
* with the real `srcDir` their ids are relative to
|
|
54
|
+
*/
|
|
55
|
+
async list() {
|
|
56
|
+
const loaded = await loadSpecs(this.testFiles);
|
|
57
|
+
return { tests: selectTests(loaded, this.options), srcDir: loaded.srcDir };
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
56
60
|
* Runs the selected tests in every browser of the run, one browser after the other, and returns the summary that
|
|
@@ -60,7 +64,7 @@ export default class TestRunner {
|
|
|
60
64
|
const { reportPath, onEvent, signal } = this.options;
|
|
61
65
|
// The specs are imported once: their top-level code runs once per run
|
|
62
66
|
const loaded = await loadSpecs(this.testFiles);
|
|
63
|
-
const config = await
|
|
67
|
+
const config = await loadRawConfig();
|
|
64
68
|
// A focused test the config refuses stops the run before anything starts; else only the focused ones run
|
|
65
69
|
assertNoOnly(loaded.all, forbidsOnly(config), loaded.srcDir);
|
|
66
70
|
const selected = narrowToOnly(selectTests(loaded, this.options));
|
package/dist/steps/edit.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export type StepEdit = {
|
|
|
22
22
|
test: number;
|
|
23
23
|
from: number;
|
|
24
24
|
to: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Removes the whole test, with the comments and the blank line above it. For several tests, apply from the last index to the first.
|
|
28
|
+
* It removes all leading trivia from the end of the previous statement, so comments above the test go with it: a
|
|
29
|
+
* file-header comment when the test is the first statement, and a trailing `// comment` on the previous test's line.
|
|
30
|
+
* Throws for a test that isn't `removable`
|
|
31
|
+
*/
|
|
32
|
+
| {
|
|
33
|
+
op: "remove-test";
|
|
34
|
+
test: number;
|
|
25
35
|
};
|
|
26
36
|
/**
|
|
27
37
|
* Applies one edit and returns the new source. Only the touched statement changes, plus the test's parameters
|
package/dist/steps/edit.js
CHANGED
|
@@ -125,6 +125,10 @@ export function applyEdit(source, edit) {
|
|
|
125
125
|
}
|
|
126
126
|
case "remove":
|
|
127
127
|
return removeStep(source, parsed, edit.index);
|
|
128
|
+
case "remove-test":
|
|
129
|
+
if (!parsed.removable)
|
|
130
|
+
throw new Error(`Cannot remove test "${parsed.name}": it is not a statement of its own`);
|
|
131
|
+
return splice(source, parsed.start, parsed.end, "");
|
|
128
132
|
case "move": {
|
|
129
133
|
const { start, end } = pickStep(parsed, edit.from);
|
|
130
134
|
if (edit.to < 0 || edit.to >= parsed.steps.length)
|
package/dist/steps/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { parseSpec } from "./parse.js";
|
|
1
|
+
export { parseSpec, parseSpecFile } from "./parse.js";
|
|
2
2
|
export { applyEdit, type StepEdit } from "./edit.js";
|
|
3
3
|
export { stepToSource, locatorToSource, callToSource, filesToSource, valueToSource, } from "./emit.js";
|
|
4
4
|
export { formatSize, humanSize, parseSize, sizeFromSource } from "./files.js";
|
|
5
5
|
export { locatorFromSpec } from "./build.js";
|
|
6
|
-
export type { Expectation, FileEntry, GeneratedFileSpec, LocatorCall, LocatorSpec, ParsedStep, ParsedTest, Step, TextMatcher, TextValue, } from "./model.js";
|
|
6
|
+
export type { Expectation, FileEntry, GeneratedFileSpec, LocatorCall, LocatorSpec, ParsedStep, ParsedTest, Step, TextMatcher, TextValue, UnparsedTest, } from "./model.js";
|
package/dist/steps/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { parseSpec } from "./parse.js";
|
|
1
|
+
export { parseSpec, parseSpecFile } from "./parse.js";
|
|
2
2
|
export { applyEdit } from "./edit.js";
|
|
3
3
|
export { stepToSource, locatorToSource, callToSource, filesToSource, valueToSource, } from "./emit.js";
|
|
4
4
|
export { formatSize, humanSize, parseSize, sizeFromSource } from "./files.js";
|
package/dist/steps/model.d.ts
CHANGED
|
@@ -146,6 +146,15 @@ export interface ParsedTest {
|
|
|
146
146
|
/** Offset just after the `{` of the test body, and of its closing `}` */
|
|
147
147
|
bodyStart: number;
|
|
148
148
|
bodyEnd: number;
|
|
149
|
+
/** Where the whole test starts: the full start of its statement, so the comments and blank line above are included */
|
|
150
|
+
start: number;
|
|
151
|
+
/** Where the whole test ends: the end of its statement, after the `;` */
|
|
152
|
+
end: number;
|
|
153
|
+
/**
|
|
154
|
+
* False when the test call isn't a statement of its own (a loop or `if` body without braces, an arrow's
|
|
155
|
+
* expression body), so it can't be removed without breaking the file
|
|
156
|
+
*/
|
|
157
|
+
removable: boolean;
|
|
149
158
|
/**
|
|
150
159
|
* The fixtures the test's callback destructures, and the offset where another one goes: after the last one
|
|
151
160
|
* (`list`), inside an empty `{}` (`empty`) or inside the empty `()` of a callback without parameters (`none`).
|
|
@@ -161,3 +170,11 @@ export interface ParsedTest {
|
|
|
161
170
|
/** The `{ reason }` written with the `skip` or `fixme` that applies, when it is a string literal */
|
|
162
171
|
reason?: string;
|
|
163
172
|
}
|
|
173
|
+
/** A `test` or `describe` call the codec can't read: its title isn't a plain string, or its function isn't written inline */
|
|
174
|
+
export interface UnparsedTest {
|
|
175
|
+
kind: "test" | "describe";
|
|
176
|
+
/** The statement's range, as for `ParsedTest` */
|
|
177
|
+
start: number;
|
|
178
|
+
end: number;
|
|
179
|
+
reason: "dynamic-title" | "not-inline";
|
|
180
|
+
}
|
package/dist/steps/parse.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
import type { ParsedTest } from "./model.js";
|
|
1
|
+
import type { ParsedTest, UnparsedTest } from "./model.js";
|
|
2
|
+
/** The tests in a spec file, with their steps and where each statement sits, and the tests and describes it can't read */
|
|
3
|
+
export declare function parseSpecFile(source: string): {
|
|
4
|
+
tests: ParsedTest[];
|
|
5
|
+
unparsed: UnparsedTest[];
|
|
6
|
+
};
|
|
2
7
|
/** The tests in a spec file, with their steps and where each statement sits */
|
|
3
8
|
export declare function parseSpec(source: string): ParsedTest[];
|
package/dist/steps/parse.js
CHANGED
|
@@ -85,24 +85,64 @@ function effectiveMark(own, describes) {
|
|
|
85
85
|
};
|
|
86
86
|
return chain.some((m) => m.modifier === "only") ? { modifier: "only" } : {};
|
|
87
87
|
}
|
|
88
|
-
/** The
|
|
89
|
-
|
|
88
|
+
/** The range of a whole test: its statement with the comments above it, or the call alone when it isn't a statement */
|
|
89
|
+
function wholeRange(call) {
|
|
90
|
+
const statement = ts.isExpressionStatement(call.parent) ? call.parent : call;
|
|
91
|
+
return { start: statement.getFullStart(), end: statement.end };
|
|
92
|
+
}
|
|
93
|
+
/** Whether the call is a statement directly inside a block, a module block or the file, so removing its range leaves valid code */
|
|
94
|
+
function isRemovable(call) {
|
|
95
|
+
const statement = call.parent;
|
|
96
|
+
if (!ts.isExpressionStatement(statement))
|
|
97
|
+
return false;
|
|
98
|
+
const container = statement.parent;
|
|
99
|
+
return (ts.isBlock(container) ||
|
|
100
|
+
ts.isSourceFile(container) ||
|
|
101
|
+
ts.isModuleBlock(container));
|
|
102
|
+
}
|
|
103
|
+
/** A test or describe the codec can't read, with where its statement sits */
|
|
104
|
+
function unreadable(node, kind, reason) {
|
|
105
|
+
return { kind, reason, ...wholeRange(node) };
|
|
106
|
+
}
|
|
107
|
+
/** `test.skip(condition, reason)` inside a body: it takes a condition where a test takes a title, so it is neither a test nor unparsed */
|
|
108
|
+
function isConditionForm(node, callee) {
|
|
109
|
+
const [first] = node.arguments;
|
|
110
|
+
return ((callee.modifier === "skip" || callee.modifier === "fixme") &&
|
|
111
|
+
!(first &&
|
|
112
|
+
(ts.isStringLiteral(first) ||
|
|
113
|
+
ts.isNoSubstitutionTemplateLiteral(first) ||
|
|
114
|
+
ts.isTemplateExpression(first))));
|
|
115
|
+
}
|
|
116
|
+
/** The tests in a spec file, with their steps and where each statement sits, and the tests and describes it can't read */
|
|
117
|
+
export function parseSpecFile(source) {
|
|
90
118
|
const sourceFile = ts.createSourceFile("spec.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
91
119
|
const tests = [];
|
|
120
|
+
const unparsed = [];
|
|
92
121
|
const visit = (node, titles, describes) => {
|
|
93
122
|
const callee = ts.isCallExpression(node)
|
|
94
123
|
? calleeOf(node.expression)
|
|
95
124
|
: undefined;
|
|
96
|
-
if (ts.isCallExpression(node) &&
|
|
125
|
+
if (ts.isCallExpression(node) &&
|
|
126
|
+
callee &&
|
|
127
|
+
(callee.name === "test" || callee.name === "describe") &&
|
|
128
|
+
!(callee.name === "test" && isConditionForm(node, callee))) {
|
|
129
|
+
const kind = callee.name;
|
|
97
130
|
const title = stringOf(node.arguments[0]);
|
|
98
131
|
const { callback, reason } = callbackOf(node, callee.modifier);
|
|
99
132
|
const body = callbackBody(callback);
|
|
133
|
+
if (title === undefined) {
|
|
134
|
+
unparsed.push(unreadable(node, kind, "dynamic-title"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!body) {
|
|
138
|
+
unparsed.push(unreadable(node, kind, "not-inline"));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
100
141
|
const mark = {
|
|
101
142
|
...(callee.modifier && { modifier: callee.modifier }),
|
|
102
143
|
...(reason !== undefined && { reason }),
|
|
103
144
|
};
|
|
104
|
-
if (
|
|
105
|
-
// `test.skip(condition, reason)` has no title and no body, so it stays a step of the test it is in
|
|
145
|
+
if (kind === "test") {
|
|
106
146
|
const titlePath = [...titles, title];
|
|
107
147
|
tests.push({
|
|
108
148
|
titlePath,
|
|
@@ -110,18 +150,22 @@ export function parseSpec(source) {
|
|
|
110
150
|
steps: body.statements.map((statement) => parseStatement(source, statement, sourceFile)),
|
|
111
151
|
bodyStart: body.getStart(sourceFile) + 1,
|
|
112
152
|
bodyEnd: body.end - 1,
|
|
153
|
+
...wholeRange(node),
|
|
154
|
+
removable: isRemovable(node),
|
|
113
155
|
fixtures: callbackFixtures(callback, sourceFile),
|
|
114
156
|
...effectiveMark(mark, describes),
|
|
115
157
|
});
|
|
116
158
|
return;
|
|
117
159
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
160
|
+
ts.forEachChild(body, (child) => visit(child, [...titles, title], [...describes, mark]));
|
|
161
|
+
return;
|
|
122
162
|
}
|
|
123
163
|
ts.forEachChild(node, (child) => visit(child, titles, describes));
|
|
124
164
|
};
|
|
125
165
|
visit(sourceFile, [], []);
|
|
126
|
-
return tests;
|
|
166
|
+
return { tests, unparsed };
|
|
167
|
+
}
|
|
168
|
+
/** The tests in a spec file, with their steps and where each statement sits */
|
|
169
|
+
export function parseSpec(source) {
|
|
170
|
+
return parseSpecFile(source).tests;
|
|
127
171
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { BiDiCommands, BiDiEvents } from "../types/bidi.js";
|
|
2
2
|
import { type EventName, type Subscription, type SubscribeOptions } from "./subscriptions.js";
|
|
3
|
-
/** Time (ms) a command may wait for its reply before it fails with code "timeout" */
|
|
4
|
-
export declare const DEFAULT_COMMAND_TIMEOUT = 30000;
|
|
5
3
|
export interface ConnectorOptions {
|
|
6
4
|
/** @default DEFAULT_COMMAND_TIMEOUT */
|
|
7
5
|
commandTimeout?: number;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
|
+
import { DEFAULT_COMMAND_TIMEOUT } from "../config/defaults.js";
|
|
2
3
|
import logger from "../logger/index.js";
|
|
3
4
|
import { WaitTimeoutError } from "../wait/wait-until.js";
|
|
4
5
|
import { BiDiError } from "./bidi-error.js";
|
|
5
6
|
import { forLog } from "./log-redaction.js";
|
|
6
7
|
import { SubscriptionManager, } from "./subscriptions.js";
|
|
7
|
-
/** Time (ms) a command may wait for its reply before it fails with code "timeout" */
|
|
8
|
-
export const DEFAULT_COMMAND_TIMEOUT = 30000;
|
|
9
8
|
/**
|
|
10
9
|
* The connection to the browser is gone (the browser exited or was closed, or the socket dropped). Commands that
|
|
11
10
|
* were waiting fail with it, and so does every command sent afterwards: a closed socket would swallow them silently.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itmetsam/samurai-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
|
|
5
5
|
"main": "dist/api.js",
|
|
6
6
|
"exports": {
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
"types": "./dist/project/index.d.ts",
|
|
49
49
|
"default": "./dist/project/index.js"
|
|
50
50
|
},
|
|
51
|
+
"./capabilities": {
|
|
52
|
+
"types": "./dist/capabilities.d.ts",
|
|
53
|
+
"default": "./dist/capabilities.js"
|
|
54
|
+
},
|
|
51
55
|
"./package.json": "./package.json"
|
|
52
56
|
},
|
|
53
57
|
"scripts": {
|
|
@@ -55,7 +59,7 @@
|
|
|
55
59
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/copy-types.mjs",
|
|
56
60
|
"prepack": "npm run build",
|
|
57
61
|
"test": "SAMURAI_LOG_LEVEL=${SAMURAI_LOG_LEVEL:-warn} tsx --import ./scripts/test-tmp.mjs --test --test-timeout=10000 \"src/**/*.test.ts\"",
|
|
58
|
-
"test:browser": "SAMURAI_LOG_LEVEL=${SAMURAI_LOG_LEVEL:-warn} tsx --import ./scripts/test-tmp.mjs --test --test-concurrency=1 --test-timeout=60000 \"src/**/*.browser-test.ts\"",
|
|
62
|
+
"test:browser": "SAMURAI_LOG_LEVEL=${SAMURAI_LOG_LEVEL:-warn} tsx --import ./scripts/test-tmp.mjs --test --test-concurrency=1 --test-timeout=60000 ${SAMURAI_TEST_SHARD:+--test-shard=$SAMURAI_TEST_SHARD} \"src/**/*.browser-test.ts\"",
|
|
59
63
|
"test:browser:chrome": "SAMURAI_TEST_BROWSER=chrome npm run test:browser",
|
|
60
64
|
"test:cli": "tsx --import ./scripts/test-tmp.mjs --test --test-concurrency=1 \"src/**/*.cli-test.ts\""
|
|
61
65
|
},
|
|
File without changes
|
|
File without changes
|