@itmetsam/samurai-framework 0.4.0 → 0.6.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 +2 -2
- package/dist/api.d.ts +1 -0
- package/dist/api.js +1 -0
- package/dist/browser/browser-finder.d.ts +1 -0
- package/dist/browser/browser-finder.js +1 -0
- package/dist/browser/browser.d.ts +8 -0
- package/dist/browser/browser.js +33 -12
- package/dist/browser/warm-up.d.ts +14 -0
- package/dist/browser/warm-up.js +37 -0
- package/dist/browsers/cache-dir.d.ts +2 -0
- package/dist/browsers/cache-dir.js +15 -0
- package/dist/browsers/downloader.d.ts +23 -0
- package/dist/browsers/downloader.js +52 -0
- package/dist/browsers/index.d.ts +6 -0
- package/dist/browsers/index.js +5 -0
- package/dist/browsers/overrides.d.ts +17 -0
- package/dist/browsers/overrides.js +15 -0
- package/dist/browsers/pins.d.ts +16 -0
- package/dist/browsers/pins.js +14 -0
- package/dist/browsers/resolve.d.ts +26 -0
- package/dist/browsers/resolve.js +56 -0
- package/dist/browsers/store.d.ts +52 -0
- package/dist/browsers/store.js +170 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +2 -0
- package/dist/cli/browsers.d.ts +6 -0
- package/dist/cli/browsers.js +119 -0
- package/dist/cli/flow-reporter.d.ts +5 -2
- package/dist/cli/flow-reporter.js +3 -2
- package/dist/cli/flow.d.ts +1 -1
- package/dist/cli/flow.js +4 -2
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +11 -1
- package/dist/config/config.d.ts +5 -0
- package/dist/config/config.js +13 -0
- package/dist/flows/core/steps.js +14 -3
- package/dist/flows/run/index.d.ts +1 -0
- package/dist/flows/run/index.js +1 -0
- package/dist/flows/run/project.js +18 -21
- package/dist/flows/steps/api.js +28 -5
- package/dist/flows/steps/catalog.d.ts +10 -4
- package/dist/flows/steps/catalog.js +23 -16
- package/dist/flows/steps/define.d.ts +2 -0
- package/dist/flows/steps/define.js +15 -4
- package/dist/flows/steps/handler.js +16 -4
- package/dist/flows/steps/index.d.ts +3 -2
- package/dist/flows/steps/index.js +2 -1
- package/dist/flows/steps/set-variable.js +14 -3
- package/dist/flows/steps/set.d.ts +26 -0
- package/dist/flows/steps/set.js +43 -0
- package/dist/flows/steps/wait.js +8 -1
- package/dist/recorder/session.js +4 -0
- package/dist/runner/groups.d.ts +5 -0
- package/dist/runner/groups.js +13 -0
- package/dist/runner/test-runner.js +9 -3
- package/dist/transport/bidi-connection.d.ts +2 -0
- package/dist/transport/bidi-connection.js +1 -1
- package/dist/types/config.d.ts +12 -0
- package/package.json +6 -1
package/dist/flows/steps/api.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { defineStep } from "./define.js";
|
|
2
|
-
const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"].map((m) => ({
|
|
2
|
+
const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"].map((m) => ({
|
|
3
|
+
label: m,
|
|
4
|
+
value: m,
|
|
5
|
+
}));
|
|
3
6
|
const isJson = (text) => {
|
|
4
7
|
try {
|
|
5
8
|
JSON.parse(text);
|
|
@@ -16,9 +19,27 @@ export const api = defineStep({
|
|
|
16
19
|
icon: "globe",
|
|
17
20
|
section: "integrations",
|
|
18
21
|
fields: [
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
{
|
|
23
|
+
path: "config.method",
|
|
24
|
+
type: "select",
|
|
25
|
+
label: "Method",
|
|
26
|
+
options: METHODS,
|
|
27
|
+
default: "GET",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: "config.url",
|
|
31
|
+
type: "expression",
|
|
32
|
+
label: "URL",
|
|
33
|
+
placeholder: 'env.baseUrl + "/health"',
|
|
34
|
+
default: "",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: "config.body",
|
|
38
|
+
type: "code",
|
|
39
|
+
label: "Body",
|
|
40
|
+
placeholder: '{ "name": "test" }',
|
|
41
|
+
default: "",
|
|
42
|
+
},
|
|
22
43
|
],
|
|
23
44
|
summary: "{config.method} {config.url}",
|
|
24
45
|
outputs: [
|
|
@@ -32,7 +53,9 @@ export const api = defineStep({
|
|
|
32
53
|
signal,
|
|
33
54
|
...(body !== undefined && {
|
|
34
55
|
body,
|
|
35
|
-
headers: {
|
|
56
|
+
headers: {
|
|
57
|
+
"content-type": isJson(body) ? "application/json" : "text/plain",
|
|
58
|
+
},
|
|
36
59
|
}),
|
|
37
60
|
});
|
|
38
61
|
return { outputs: { statusCode: res.status, body: await res.text() } };
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import type { AnyNodeDefinition, FlowFile, FlowProblem, StepInfo } from "../core/index.js";
|
|
2
2
|
import type { Handler } from "../run/handlers/support.js";
|
|
3
3
|
import { type StepDefinition } from "./define.js";
|
|
4
|
+
import type { SamuraiTestConfig } from "../../types/config.js";
|
|
4
5
|
/** The built-in steps, in library order */
|
|
5
6
|
export declare const STEPS: readonly StepDefinition<any>[];
|
|
6
7
|
export declare const STEP_NODES: readonly AnyNodeDefinition[];
|
|
7
8
|
export declare const STEP_HANDLERS: Readonly<Record<string, Handler>>;
|
|
8
9
|
/** The steps as data, for the app */
|
|
9
10
|
export declare const stepCatalog: (steps?: readonly StepDefinition<any>[]) => StepInfo[];
|
|
10
|
-
/** The project's steps
|
|
11
|
-
export declare function listSteps(
|
|
12
|
-
|
|
11
|
+
/** The project's steps as data: the built-ins, then its config's. `projectDir` defaults to the working directory. */
|
|
12
|
+
export declare function listSteps(options?: {
|
|
13
|
+
projectDir?: string;
|
|
14
|
+
config?: SamuraiTestConfig;
|
|
15
|
+
}): Promise<StepInfo[]>;
|
|
16
|
+
/** Only the steps' own `validate` problems (the project's steps included): the app checks everything else itself */
|
|
13
17
|
export declare function validateSteps(flow: FlowFile, options: {
|
|
14
18
|
tests?: string[];
|
|
15
19
|
groups?: string[];
|
|
16
|
-
|
|
20
|
+
projectDir?: string;
|
|
21
|
+
config?: SamuraiTestConfig;
|
|
22
|
+
}): Promise<FlowProblem[]>;
|
|
@@ -1,29 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { stepHandler } from "./handler.js";
|
|
4
|
-
import { setVariable } from "./set-variable.js";
|
|
5
|
-
import { wait } from "./wait.js";
|
|
1
|
+
import { infoOf } from "./define.js";
|
|
2
|
+
import { BUILTIN_SET, BUILTIN_STEPS, projectSteps } from "./set.js";
|
|
6
3
|
/** The built-in steps, in library order */
|
|
7
4
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
-
export const STEPS =
|
|
9
|
-
export const STEP_NODES =
|
|
10
|
-
export const STEP_HANDLERS =
|
|
5
|
+
export const STEPS = BUILTIN_STEPS;
|
|
6
|
+
export const STEP_NODES = BUILTIN_SET.nodes;
|
|
7
|
+
export const STEP_HANDLERS = BUILTIN_SET.handlers;
|
|
11
8
|
/** The steps as data, for the app */
|
|
12
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
10
|
export const stepCatalog = (steps = STEPS) => steps.map(infoOf);
|
|
14
|
-
/** The project's steps
|
|
15
|
-
export async function listSteps() {
|
|
16
|
-
return
|
|
11
|
+
/** The project's steps as data: the built-ins, then its config's. `projectDir` defaults to the working directory. */
|
|
12
|
+
export async function listSteps(options = {}) {
|
|
13
|
+
return [...(await projectSteps(options)).catalog];
|
|
17
14
|
}
|
|
18
|
-
/** Only the steps' own `validate` problems: the app checks everything else itself */
|
|
19
|
-
export function validateSteps(flow, options) {
|
|
15
|
+
/** Only the steps' own `validate` problems (the project's steps included): the app checks everything else itself */
|
|
16
|
+
export async function validateSteps(flow, options) {
|
|
17
|
+
const set = await projectSteps(options);
|
|
20
18
|
const tests = options.tests ? new Set(options.tests) : undefined;
|
|
21
19
|
const groups = options.groups ? new Set(options.groups) : undefined;
|
|
22
20
|
return flow.nodes.flatMap((n) => {
|
|
23
|
-
const step =
|
|
21
|
+
const step = set.steps.find((s) => s.kind === n.kind);
|
|
24
22
|
if (!step?.validate)
|
|
25
23
|
return [];
|
|
26
|
-
const def =
|
|
27
|
-
return step.validate({
|
|
24
|
+
const def = set.nodes.find((d) => d.kind === n.kind);
|
|
25
|
+
return step.validate({
|
|
26
|
+
...n,
|
|
27
|
+
config: {
|
|
28
|
+
...Object.fromEntries(step.fields.map((f) => [
|
|
29
|
+
f.path.slice(7),
|
|
30
|
+
"default" in f ? f.default : undefined,
|
|
31
|
+
])),
|
|
32
|
+
...n.config,
|
|
33
|
+
},
|
|
34
|
+
}, { label: def.label, tests, groups });
|
|
28
35
|
});
|
|
29
36
|
}
|
|
@@ -38,6 +38,8 @@ export interface StepDefinition<C = Record<string, unknown>> extends StepInfo {
|
|
|
38
38
|
/** What the step does in a run: return to pass, throw to fail */
|
|
39
39
|
run: (input: StepRunInput<C>) => Promise<StepResult | void>;
|
|
40
40
|
}
|
|
41
|
+
/** The checks every step passes: in `defineStep`, and again when a project lists it. Throws for a step that can't work. */
|
|
42
|
+
export declare function checkStep(def: StepDefinition<any>, where?: string): void;
|
|
41
43
|
/** Declares a flow step: one way in, one way out. Throws for a definition that can't work. */
|
|
42
44
|
export declare function defineStep<const F extends readonly StepField[]>(def: Omit<StepDefinition<RunConfigOf<F>>, "fields"> & {
|
|
43
45
|
fields: F;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { CLOSED_KINDS, RESERVED_OUTPUTS, STEP_KIND, stepNodeOf, } from "../core/index.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/** The checks every step passes: in `defineStep`, and again when a project lists it. Throws for a step that can't work. */
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export function checkStep(def, where = "") {
|
|
5
|
+
const name = `Step "${def.kind}"${where}`;
|
|
5
6
|
if (CLOSED_KINDS.includes(def.kind))
|
|
6
7
|
throw new Error(`${name}: "${def.kind}" is a closed kind; steps can't replace it.`);
|
|
7
8
|
if (!STEP_KIND.test(def.kind))
|
|
8
9
|
throw new Error(`${name}: "${def.kind}" isn't a valid step kind. Use lowercase letters, digits and -.`);
|
|
10
|
+
if (!Array.isArray(def.fields))
|
|
11
|
+
throw new Error(`${name}: fields must be a list.`);
|
|
9
12
|
for (const f of def.fields) {
|
|
10
13
|
if (!f.path.startsWith("config."))
|
|
11
14
|
throw new Error(`${name}: field "${f.path}" must be a config. path.`);
|
|
@@ -24,8 +27,16 @@ export function defineStep(def) {
|
|
|
24
27
|
if (outputs.indexOf(o) !== outputs.lastIndexOf(o))
|
|
25
28
|
throw new Error(`${name}: output "${o}" is declared twice.`);
|
|
26
29
|
}
|
|
27
|
-
if (def.writesVar !== undefined &&
|
|
30
|
+
if (def.writesVar !== undefined &&
|
|
31
|
+
!def.fields.some((f) => f.path === def.writesVar))
|
|
28
32
|
throw new Error(`${name}: writesVar "${def.writesVar}" isn't one of its fields.`);
|
|
33
|
+
if (typeof def.run !== "function")
|
|
34
|
+
throw new Error(`${name}: run must be a function.`);
|
|
35
|
+
}
|
|
36
|
+
/** Declares a flow step: one way in, one way out. Throws for a definition that can't work. */
|
|
37
|
+
export function defineStep(def) {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
checkStep(def);
|
|
29
40
|
return def;
|
|
30
41
|
}
|
|
31
42
|
/** The step as data, without its functions: what the catalog holds */
|
|
@@ -27,7 +27,9 @@ export function stepHandler(step) {
|
|
|
27
27
|
const stopped = new Promise((resolve) => {
|
|
28
28
|
if (run.signal.aborted)
|
|
29
29
|
resolve("stopped");
|
|
30
|
-
run.signal.addEventListener("abort", () => resolve("stopped"), {
|
|
30
|
+
run.signal.addEventListener("abort", () => resolve("stopped"), {
|
|
31
|
+
once: true,
|
|
32
|
+
});
|
|
31
33
|
});
|
|
32
34
|
let result;
|
|
33
35
|
try {
|
|
@@ -36,7 +38,11 @@ export function stepHandler(step) {
|
|
|
36
38
|
config,
|
|
37
39
|
env: { ...run.context.env },
|
|
38
40
|
vars: { ...run.context.vars },
|
|
39
|
-
node: {
|
|
41
|
+
node: {
|
|
42
|
+
id: node.id,
|
|
43
|
+
...(node.key && { key: node.key }),
|
|
44
|
+
...(node.title && { title: node.title }),
|
|
45
|
+
},
|
|
40
46
|
signal: run.signal,
|
|
41
47
|
sleep: (ms) => run.sleep(ms, run.signal),
|
|
42
48
|
log: (level, text) => run.log(node.id, level, text),
|
|
@@ -54,11 +60,17 @@ export function stepHandler(step) {
|
|
|
54
60
|
const declared = new Set((step.outputs ?? []).map((o) => o.name));
|
|
55
61
|
const extra = keysOf(result?.outputs).filter((k) => !declared.has(k));
|
|
56
62
|
if (extra.length)
|
|
57
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
status: "failed",
|
|
65
|
+
error: `${step.label} returned outputs it doesn't declare: ${extra.join(", ")}.`,
|
|
66
|
+
};
|
|
58
67
|
const allowed = step.writesVar ? getPath(typed, step.writesVar) : undefined;
|
|
59
68
|
const wrongVars = keysOf(result?.vars).filter((k) => k !== allowed);
|
|
60
69
|
if (wrongVars.length)
|
|
61
|
-
return {
|
|
70
|
+
return {
|
|
71
|
+
status: "failed",
|
|
72
|
+
error: `${step.label} wrote variables it doesn't declare: ${wrongVars.join(", ")}.`,
|
|
73
|
+
};
|
|
62
74
|
Object.assign(run.context.vars, result?.vars);
|
|
63
75
|
return {
|
|
64
76
|
status: "passed",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Flow steps: define your own, and the built-in ones. Node only. */
|
|
2
|
-
export { defineStep, stepNode, type RunConfigOf, type StepDefinition, type StepResult, type StepRunInput, } from "./define.js";
|
|
2
|
+
export { checkStep, defineStep, stepNode, type RunConfigOf, type StepDefinition, type StepResult, type StepRunInput, } from "./define.js";
|
|
3
3
|
export { stepHandler } from "./handler.js";
|
|
4
|
+
export { BUILTIN_SET, BUILTIN_STEPS, projectSteps, stepSet, type StepSet, } from "./set.js";
|
|
4
5
|
export { STEPS, STEP_HANDLERS, STEP_NODES, listSteps, stepCatalog, validateSteps, } from "./catalog.js";
|
|
5
|
-
export type { StepField, StepInfo, StepOutput, StepSection } from "../core/index.js";
|
|
6
|
+
export type { StepField, StepInfo, StepOutput, StepSection, } from "../core/index.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Flow steps: define your own, and the built-in ones. Node only. */
|
|
2
|
-
export { defineStep, stepNode, } from "./define.js";
|
|
2
|
+
export { checkStep, defineStep, stepNode, } from "./define.js";
|
|
3
3
|
export { stepHandler } from "./handler.js";
|
|
4
|
+
export { BUILTIN_SET, BUILTIN_STEPS, projectSteps, stepSet, } from "./set.js";
|
|
4
5
|
export { STEPS, STEP_HANDLERS, STEP_NODES, listSteps, stepCatalog, validateSteps, } from "./catalog.js";
|
|
@@ -6,11 +6,22 @@ export const setVariable = defineStep({
|
|
|
6
6
|
section: "control",
|
|
7
7
|
fields: [
|
|
8
8
|
{
|
|
9
|
-
path: "config.name",
|
|
10
|
-
|
|
9
|
+
path: "config.name",
|
|
10
|
+
type: "text",
|
|
11
|
+
label: "Name",
|
|
12
|
+
placeholder: "orderId",
|
|
13
|
+
default: "",
|
|
14
|
+
required: true,
|
|
15
|
+
pattern: "^[a-z][a-z0-9_]*$",
|
|
11
16
|
patternMessage: "use lowercase letters, digits and _ for the name.",
|
|
12
17
|
},
|
|
13
|
-
{
|
|
18
|
+
{
|
|
19
|
+
path: "config.value",
|
|
20
|
+
type: "expression",
|
|
21
|
+
label: "Value",
|
|
22
|
+
placeholder: '"order-" + run.environment',
|
|
23
|
+
default: "",
|
|
24
|
+
},
|
|
14
25
|
],
|
|
15
26
|
summary: "vars.{config.name} = {config.value}",
|
|
16
27
|
writesVar: "config.name",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SamuraiTestConfig } from "../../types/config.js";
|
|
2
|
+
import type { AnyNodeDefinition, StepInfo } from "../core/index.js";
|
|
3
|
+
import type { Handler } from "../run/handlers/support.js";
|
|
4
|
+
import { type StepDefinition } from "./define.js";
|
|
5
|
+
type AnyStep = StepDefinition<any>;
|
|
6
|
+
/** The built-in steps, in library order */
|
|
7
|
+
export declare const BUILTIN_STEPS: readonly AnyStep[];
|
|
8
|
+
/** Everything a project's steps give the walker, the check and the app */
|
|
9
|
+
export interface StepSet {
|
|
10
|
+
/** The built-ins, then the project's in config order */
|
|
11
|
+
steps: readonly AnyStep[];
|
|
12
|
+
nodes: readonly AnyNodeDefinition[];
|
|
13
|
+
handlers: Readonly<Record<string, Handler>>;
|
|
14
|
+
/** The steps as data, for the app */
|
|
15
|
+
catalog: StepInfo[];
|
|
16
|
+
}
|
|
17
|
+
/** The built-in steps plus a project's (its config's `steps`). Throws for a list that can't work. */
|
|
18
|
+
export declare function stepSet(projectSteps?: unknown): StepSet;
|
|
19
|
+
/** The built-in steps alone */
|
|
20
|
+
export declare const BUILTIN_SET: StepSet;
|
|
21
|
+
/** The project's steps: its config's `steps` after the built-ins. `projectDir` defaults to the working directory. */
|
|
22
|
+
export declare function projectSteps(options?: {
|
|
23
|
+
projectDir?: string;
|
|
24
|
+
config?: SamuraiTestConfig;
|
|
25
|
+
}): Promise<StepSet>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { projectConfig } from "../../config/config.js";
|
|
2
|
+
import { api } from "./api.js";
|
|
3
|
+
import { checkStep, infoOf, stepNode } from "./define.js";
|
|
4
|
+
import { stepHandler } from "./handler.js";
|
|
5
|
+
import { setVariable } from "./set-variable.js";
|
|
6
|
+
import { wait } from "./wait.js";
|
|
7
|
+
/** The built-in steps, in library order */
|
|
8
|
+
export const BUILTIN_STEPS = [wait, setVariable, api];
|
|
9
|
+
const WHERE = " (samurai.config.ts)";
|
|
10
|
+
/** The built-in steps plus a project's (its config's `steps`). Throws for a list that can't work. */
|
|
11
|
+
export function stepSet(projectSteps = []) {
|
|
12
|
+
if (!Array.isArray(projectSteps))
|
|
13
|
+
throw new Error("samurai.config.ts: steps must be a list of steps.");
|
|
14
|
+
const builtin = new Set(BUILTIN_STEPS.map((s) => s.kind));
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
projectSteps.forEach((entry, i) => {
|
|
17
|
+
const kind = entry && typeof entry === "object"
|
|
18
|
+
? entry.kind
|
|
19
|
+
: undefined;
|
|
20
|
+
if (typeof kind !== "string" || kind === "")
|
|
21
|
+
throw new Error(`Step #${i + 1}${WHERE}: it has no kind; make it with defineStep.`);
|
|
22
|
+
checkStep(entry, WHERE);
|
|
23
|
+
if (builtin.has(kind))
|
|
24
|
+
throw new Error(`Step "${kind}"${WHERE}: a built-in step already uses this kind.`);
|
|
25
|
+
if (seen.has(kind))
|
|
26
|
+
throw new Error(`Step "${kind}"${WHERE}: declared twice.`);
|
|
27
|
+
seen.add(kind);
|
|
28
|
+
});
|
|
29
|
+
const steps = [...BUILTIN_STEPS, ...projectSteps];
|
|
30
|
+
return {
|
|
31
|
+
steps,
|
|
32
|
+
nodes: steps.map(stepNode),
|
|
33
|
+
handlers: Object.fromEntries(steps.map((s) => [s.kind, stepHandler(s)])),
|
|
34
|
+
catalog: steps.map(infoOf),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** The built-in steps alone */
|
|
38
|
+
export const BUILTIN_SET = stepSet();
|
|
39
|
+
/** The project's steps: its config's `steps` after the built-ins. `projectDir` defaults to the working directory. */
|
|
40
|
+
export async function projectSteps(options = {}) {
|
|
41
|
+
const config = await projectConfig(options);
|
|
42
|
+
return config.steps === undefined ? BUILTIN_SET : stepSet(config.steps);
|
|
43
|
+
}
|
package/dist/flows/steps/wait.js
CHANGED
|
@@ -5,7 +5,14 @@ export const wait = defineStep({
|
|
|
5
5
|
icon: "timer",
|
|
6
6
|
section: "control",
|
|
7
7
|
fields: [
|
|
8
|
-
{
|
|
8
|
+
{
|
|
9
|
+
path: "config.ms",
|
|
10
|
+
type: "number",
|
|
11
|
+
label: "Duration",
|
|
12
|
+
unit: "ms",
|
|
13
|
+
default: 1000,
|
|
14
|
+
min: 0,
|
|
15
|
+
},
|
|
9
16
|
],
|
|
10
17
|
summary: "{config.ms} ms",
|
|
11
18
|
async run({ config, sleep }) {
|
package/dist/recorder/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Browser } from "../browser/browser.js";
|
|
4
|
+
import { ensureWarm } from "../browser/warm-up.js";
|
|
4
5
|
import { projectDir } from "../config/config.js";
|
|
5
6
|
import { withProject } from "../runner/project.js";
|
|
6
7
|
import { parseSpec } from "../steps/parse.js";
|
|
@@ -52,6 +53,9 @@ export async function recordSpec(options) {
|
|
|
52
53
|
if (source === undefined && options.create === undefined) {
|
|
53
54
|
throw new Error(`${options.file} does not exist; give a title with --new to create it`);
|
|
54
55
|
}
|
|
56
|
+
// Before the spec is touched: a recording without its browser changes nothing
|
|
57
|
+
await ensureWarm("firefox", { port: options.port ?? 9223 }, options.signal);
|
|
58
|
+
options.signal?.throwIfAborted();
|
|
55
59
|
source ??= "";
|
|
56
60
|
if (options.create !== undefined &&
|
|
57
61
|
!parseSpec(source).some((t) => t.name === options.create)) {
|
package/dist/runner/groups.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface ResolvedGroup {
|
|
|
23
23
|
title: string;
|
|
24
24
|
}[];
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The real path of `p` (symlinks resolved), or `p` itself when it does not exist. Registered test files come
|
|
28
|
+
* from stack traces, which resolve symlinks, so paths compared with them must be real too
|
|
29
|
+
*/
|
|
30
|
+
export declare function realPath(p: string): string;
|
|
26
31
|
/** Separators of any platform become `/` */
|
|
27
32
|
export declare const toPosix: (p: string) => string;
|
|
28
33
|
/** The id of a test: its spec file relative to `srcDir` with `/` separators, then `::` and its full name */
|
package/dist/runner/groups.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
1
2
|
import { matchesGlob, relative, resolve } from "node:path";
|
|
2
3
|
/** The test folder when the config names none */
|
|
3
4
|
export const DEFAULT_SRC_DIR = "./src";
|
|
4
5
|
/** The spec pattern when a group names none */
|
|
5
6
|
export const DEFAULT_TEST_MATCH = "**/*.spec.ts";
|
|
7
|
+
/**
|
|
8
|
+
* The real path of `p` (symlinks resolved), or `p` itself when it does not exist. Registered test files come
|
|
9
|
+
* from stack traces, which resolve symlinks, so paths compared with them must be real too
|
|
10
|
+
*/
|
|
11
|
+
export function realPath(p) {
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync(p);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
6
19
|
/** Separators of any platform become `/` */
|
|
7
20
|
export const toPosix = (p) => p.replace(/\\/g, "/");
|
|
8
21
|
/** The id of a test: its spec file relative to `srcDir` with `/` separators, then `::` and its full name */
|
|
@@ -3,6 +3,7 @@ import { glob } from "node:fs/promises";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { pathToFileURL } from "url";
|
|
5
5
|
import { Browser } from "../browser/browser.js";
|
|
6
|
+
import { ensureWarm } from "../browser/warm-up.js";
|
|
6
7
|
import { loadConfig, projectDir, readConfig } from "../config/config.js";
|
|
7
8
|
import logger from "../logger/index.js";
|
|
8
9
|
import { assertModuleProject } from "./module-type.js";
|
|
@@ -14,7 +15,7 @@ import Page from "../browser/page.js";
|
|
|
14
15
|
import { takePendingAssertions } from "../assert/expect.js";
|
|
15
16
|
import { toTestError } from "./test-error.js";
|
|
16
17
|
import { ConnectionClosedError } from "../transport/bidi-connection.js";
|
|
17
|
-
import { DEFAULT_SRC_DIR, findGroup, resolveGroups, testId, toPosix, } from "./groups.js";
|
|
18
|
+
import { DEFAULT_SRC_DIR, findGroup, realPath, resolveGroups, testId, toPosix, } from "./groups.js";
|
|
18
19
|
import { judgePageLogs } from "./page-logs-report.js";
|
|
19
20
|
/** Whether a test with this full name is selected by the options */
|
|
20
21
|
export function isSelected(name, { grep, testNames }) {
|
|
@@ -81,8 +82,9 @@ export default class TestRunner {
|
|
|
81
82
|
/** The config's groups, resolved against the tests the specs register */
|
|
82
83
|
async resolveAll(tests) {
|
|
83
84
|
const config = await loadConfig();
|
|
84
|
-
|
|
85
|
-
const
|
|
85
|
+
// Real paths: registered test files come from stack traces, which resolve symlinks
|
|
86
|
+
const dir = realPath(projectDir());
|
|
87
|
+
const srcDir = realPath(path.resolve(dir, config.srcDir ?? DEFAULT_SRC_DIR));
|
|
86
88
|
const groups = resolveGroups(config, tests.map(({ file, name }) => ({
|
|
87
89
|
file: toPosix(path.relative(srcDir, file)),
|
|
88
90
|
name,
|
|
@@ -114,6 +116,10 @@ export default class TestRunner {
|
|
|
114
116
|
...(onEvent && { onEvent }),
|
|
115
117
|
});
|
|
116
118
|
const selected = await this.register();
|
|
119
|
+
// A missing browser is one clear error before the run, not a failure per test; a fresh build's slow
|
|
120
|
+
// first start happens here, outside any test's timeout
|
|
121
|
+
if (selected.length > 0)
|
|
122
|
+
await ensureWarm((await readConfig("browser")) ?? "firefox", { port: this.options.port ?? 9223 }, signal);
|
|
117
123
|
reporter.onStart(selected.length);
|
|
118
124
|
for (const test of selected) {
|
|
119
125
|
if (signal?.aborted) {
|
|
@@ -1,5 +1,7 @@
|
|
|
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;
|
|
3
5
|
export interface ConnectorOptions {
|
|
4
6
|
/** @default 30000 */
|
|
5
7
|
commandTimeout?: number;
|
|
@@ -4,7 +4,7 @@ import { WaitTimeoutError } from "../wait/wait-until.js";
|
|
|
4
4
|
import { BiDiError } from "./bidi-error.js";
|
|
5
5
|
import { SubscriptionManager, } from "./subscriptions.js";
|
|
6
6
|
/** Time (ms) a command may wait for its reply before it fails with code "timeout" */
|
|
7
|
-
const DEFAULT_COMMAND_TIMEOUT = 30000;
|
|
7
|
+
export const DEFAULT_COMMAND_TIMEOUT = 30000;
|
|
8
8
|
/**
|
|
9
9
|
* The connection to the browser is gone (the browser exited or was closed, or the socket dropped). Commands that
|
|
10
10
|
* were waiting fail with it, and so does every command sent afterwards: a closed socket would swallow them silently.
|
package/dist/types/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StepDefinition } from "../flows/steps/define.js";
|
|
1
2
|
import type { SupportedBrowser } from "./browser.js";
|
|
2
3
|
|
|
3
4
|
interface SamuraiTest {
|
|
@@ -48,6 +49,11 @@ export interface SamuraiTestConfig {
|
|
|
48
49
|
* Is overwritten by the 'browser' in a group's config
|
|
49
50
|
*/
|
|
50
51
|
browser?: SupportedBrowser;
|
|
52
|
+
/**
|
|
53
|
+
* Browser binaries to launch instead of the builds SAMURAI downloads, relative to the project.
|
|
54
|
+
* `SAMURAI_FIREFOX_PATH` / `SAMURAI_CHROME_PATH` win over these.
|
|
55
|
+
*/
|
|
56
|
+
browserPaths?: { firefox?: string; chrome?: string };
|
|
51
57
|
/**
|
|
52
58
|
* The timeout (in ms) to use for all tests
|
|
53
59
|
* @default 30000
|
|
@@ -57,6 +63,12 @@ export interface SamuraiTestConfig {
|
|
|
57
63
|
* Define a group of test's you want to have executed with a certain configuration
|
|
58
64
|
**/
|
|
59
65
|
groups?: SamuraiGroup[];
|
|
66
|
+
/**
|
|
67
|
+
* Flow steps of this project, made with `defineStep`. Listed after the built-in steps;
|
|
68
|
+
* a kind a built-in or another step uses is refused.
|
|
69
|
+
*/
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
steps?: readonly StepDefinition<any>[];
|
|
60
72
|
/**
|
|
61
73
|
* The base URL to use when navigating the browser.
|
|
62
74
|
* When recording starts, the browser will start at this URL.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itmetsam/samurai-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"types": "./dist/cli/init.d.ts",
|
|
25
25
|
"default": "./dist/cli/init.js"
|
|
26
26
|
},
|
|
27
|
+
"./browsers": {
|
|
28
|
+
"types": "./dist/browsers/index.d.ts",
|
|
29
|
+
"default": "./dist/browsers/index.js"
|
|
30
|
+
},
|
|
27
31
|
"./browser": {
|
|
28
32
|
"types": "./dist/browser/index.d.ts",
|
|
29
33
|
"default": "./dist/browser/index.js"
|
|
@@ -65,6 +69,7 @@
|
|
|
65
69
|
"tsx": "^4.21.0"
|
|
66
70
|
},
|
|
67
71
|
"dependencies": {
|
|
72
|
+
"@puppeteer/browsers": "^3.2.4",
|
|
68
73
|
"typescript": "^6.0.2",
|
|
69
74
|
"winston": "^3.19.0"
|
|
70
75
|
},
|