@itmetsam/samurai-framework 0.19.0 → 0.21.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/api.d.ts +1 -1
- package/dist/assert/assertion-error.d.ts +3 -1
- package/dist/assert/assertion-error.js +2 -2
- package/dist/assert/locator-assertions.d.ts +35 -4
- package/dist/assert/locator-assertions.js +56 -28
- package/dist/assert/page-assertions.js +1 -1
- package/dist/assert/pending-assertions.d.ts +3 -2
- package/dist/assert/pending-assertions.js +5 -4
- package/dist/assert/text-match.d.ts +6 -0
- package/dist/assert/text-match.js +12 -0
- package/dist/capabilities.d.ts +16 -3
- package/dist/capabilities.js +16 -3
- package/dist/cli/flow-reporter.d.ts +1 -1
- package/dist/cli/flow-reporter.js +2 -2
- package/dist/cli/flow.js +4 -4
- package/dist/flows/core/check-io.d.ts +23 -0
- package/dist/flows/core/check-io.js +84 -0
- package/dist/flows/core/check.d.ts +16 -2
- package/dist/flows/core/check.js +20 -4
- package/dist/flows/core/expression/scope.d.ts +9 -2
- package/dist/flows/core/expression/scope.js +12 -4
- package/dist/flows/core/index.d.ts +2 -1
- package/dist/flows/core/index.js +2 -1
- package/dist/flows/core/nodes/condition.js +0 -1
- package/dist/flows/core/nodes/define.d.ts +0 -2
- package/dist/flows/core/nodes/group.js +0 -1
- package/dist/flows/core/nodes/test.js +0 -1
- package/dist/flows/core/schema.d.ts +10 -5
- package/dist/flows/core/schema.js +29 -1
- package/dist/flows/core/steps.d.ts +7 -0
- package/dist/flows/core/steps.js +15 -0
- package/dist/flows/core/wiring.d.ts +72 -0
- package/dist/flows/core/wiring.js +151 -0
- package/dist/flows/run/branch-context.d.ts +10 -4
- package/dist/flows/run/branch-context.js +6 -4
- package/dist/flows/run/events.d.ts +30 -2
- package/dist/flows/run/handlers/group.js +6 -2
- package/dist/flows/run/handlers/support.d.ts +7 -2
- package/dist/flows/run/handlers/support.js +36 -7
- package/dist/flows/run/handlers/test.js +6 -2
- package/dist/flows/run/handlers/types.d.ts +11 -3
- package/dist/flows/run/index.d.ts +1 -0
- package/dist/flows/run/index.js +1 -0
- package/dist/flows/run/node-folder.d.ts +10 -0
- package/dist/flows/run/node-folder.js +32 -0
- package/dist/flows/run/node-inputs.d.ts +5 -0
- package/dist/flows/run/node-inputs.js +62 -0
- package/dist/flows/run/node-pool.d.ts +2 -2
- package/dist/flows/run/node-pool.js +6 -6
- package/dist/flows/run/node-runner.d.ts +24 -0
- package/dist/flows/run/node-runner.js +48 -0
- package/dist/flows/run/per-browser.d.ts +21 -0
- package/dist/flows/run/per-browser.js +46 -0
- package/dist/flows/run/project-facts.d.ts +13 -0
- package/dist/flows/run/project-facts.js +12 -0
- package/dist/flows/run/project-options.d.ts +9 -2
- package/dist/flows/run/project.d.ts +13 -8
- package/dist/flows/run/project.js +48 -51
- package/dist/flows/run/report.d.ts +7 -7
- package/dist/flows/run/report.js +3 -5
- package/dist/flows/run/run.d.ts +21 -7
- package/dist/flows/run/run.js +21 -11
- package/dist/flows/run/walker.d.ts +23 -7
- package/dist/flows/run/walker.js +18 -7
- package/dist/locator/locator.d.ts +3 -1
- package/dist/locator/locator.js +6 -0
- package/dist/recorder/apply.js +17 -7
- package/dist/recorder/normalise.d.ts +18 -3
- package/dist/recorder/normalise.js +18 -3
- package/dist/recorder/session-secrets.js +4 -0
- package/dist/recorder/toolbar/action-guard.d.ts +2 -5
- package/dist/recorder/toolbar/action-guard.js +9 -5
- package/dist/recorder/toolbar/actions/insert.d.ts +5 -2
- package/dist/recorder/toolbar/actions/insert.js +43 -7
- package/dist/recorder/toolbar/actions/pick.js +2 -0
- package/dist/recorder/toolbar/actions/reply.d.ts +2 -0
- package/dist/recorder/toolbar/actions/reply.js +9 -0
- package/dist/recorder/toolbar/actions/save-output.d.ts +9 -0
- package/dist/recorder/toolbar/actions/save-output.js +46 -0
- package/dist/recorder/toolbar/controller-fixture.d.ts +2 -2
- package/dist/recorder/toolbar/controller-fixture.js +2 -0
- package/dist/recorder/toolbar/controller.d.ts +2 -1
- package/dist/recorder/toolbar/controller.js +14 -7
- package/dist/recorder/toolbar/insertables.d.ts +7 -0
- package/dist/recorder/toolbar/insertables.js +12 -0
- package/dist/recorder/toolbar/inspect.js +3 -1
- package/dist/recorder/toolbar/protocol.d.ts +20 -3
- package/dist/recorder/toolbar/source/events-page.js +2 -1
- package/dist/recorder/toolbar/source/fields.js +8 -0
- package/dist/recorder/toolbar/source/handlers.js +19 -12
- package/dist/recorder/toolbar/source/picking.js +22 -7
- package/dist/recorder/toolbar/source/recorder-side.js +6 -0
- package/dist/recorder/toolbar/source/setup.js +3 -3
- package/dist/recorder/toolbar/source/views-panels.d.ts +1 -1
- package/dist/recorder/toolbar/source/views-panels.js +50 -10
- package/dist/recorder/toolbar/source/views-rail.js +5 -2
- package/dist/recorder/toolbar/source.d.ts +5 -5
- package/dist/recorder/toolbar/source.js +5 -5
- package/dist/recorder/toolbar/types.d.ts +10 -3
- package/dist/runner/options.d.ts +5 -0
- package/dist/runner/pooled-run.d.ts +3 -2
- package/dist/runner/pooled-run.js +4 -2
- package/dist/runner/registry.d.ts +7 -3
- package/dist/runner/registry.js +29 -11
- package/dist/runner/reporter.d.ts +4 -0
- package/dist/runner/reporter.js +2 -0
- package/dist/runner/run-test.js +23 -2
- package/dist/runner/run.d.ts +7 -0
- package/dist/runner/run.js +8 -1
- package/dist/runner/test-io.d.ts +30 -0
- package/dist/runner/test-io.js +111 -0
- package/dist/runner/test-runner.js +1 -1
- package/dist/runner/worker-protocol.d.ts +2 -0
- package/dist/runner/worker-session.js +1 -0
- package/dist/runner/worker.d.ts +1 -0
- package/dist/runner/worker.js +4 -1
- package/dist/steps/edit.d.ts +14 -3
- package/dist/steps/edit.js +65 -23
- package/dist/steps/emit.d.ts +2 -0
- package/dist/steps/emit.js +32 -8
- package/dist/steps/index.d.ts +1 -1
- package/dist/steps/model.d.ts +42 -2
- package/dist/steps/parse/expectation.d.ts +8 -1
- package/dist/steps/parse/expectation.js +17 -11
- package/dist/steps/parse/step.js +64 -8
- package/dist/steps/parse.js +25 -16
- package/dist/steps/test-options.d.ts +25 -0
- package/dist/steps/test-options.js +158 -0
- package/dist/steps/text.d.ts +7 -0
- package/dist/steps/text.js +12 -0
- package/dist/types/test.d.ts +34 -0
- package/dist/wait/output-sink.d.ts +24 -0
- package/dist/wait/output-sink.js +59 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TestResult } from "../../types/test.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { FlowBrowserRun, FlowEvent, FlowRunResult } from "./events.js";
|
|
3
3
|
/** One test a Test or Group node ran, as the runner reported it */
|
|
4
4
|
export type FlowTestResult = {
|
|
5
5
|
nodeId: string;
|
|
@@ -9,14 +9,14 @@ export interface FlowReport {
|
|
|
9
9
|
flow: string;
|
|
10
10
|
name: string;
|
|
11
11
|
environment: string;
|
|
12
|
-
|
|
12
|
+
/** Across the browsers: see `FlowRunResult` */
|
|
13
|
+
status: FlowRunResult["status"];
|
|
13
14
|
startedAt: string;
|
|
14
15
|
durationMs: number;
|
|
15
|
-
|
|
16
|
-
vars: Record<string, unknown>;
|
|
16
|
+
/** Every test of every run; each names its browser */
|
|
17
17
|
tests: FlowTestResult[];
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/** One entry per browser the flow ran in, in order: its nodes, variables and error */
|
|
19
|
+
runs: FlowBrowserRun[];
|
|
20
20
|
}
|
|
21
21
|
/** Follows a run's events and builds its report once it ends (or `finish` is given what is known) */
|
|
22
22
|
export declare class FlowReportBuilder {
|
|
@@ -31,7 +31,7 @@ export declare class FlowReportBuilder {
|
|
|
31
31
|
});
|
|
32
32
|
handle(event: FlowEvent): void;
|
|
33
33
|
private collect;
|
|
34
|
-
finish(
|
|
34
|
+
finish(result: FlowRunResult, environment?: string): FlowReport;
|
|
35
35
|
}
|
|
36
36
|
/** Writes `<projectDir>/result/flows/<id>.json` with the registered secrets masked, as `report.json` is. Returns the path */
|
|
37
37
|
export declare function writeFlowReport(projectDir: string, report: FlowReport): Promise<string>;
|
package/dist/flows/run/report.js
CHANGED
|
@@ -20,18 +20,16 @@ export class FlowReportBuilder {
|
|
|
20
20
|
if (event.type === "test-end")
|
|
21
21
|
this.tests.push({ nodeId, ...event.result });
|
|
22
22
|
}
|
|
23
|
-
finish(
|
|
23
|
+
finish(result, environment = this.environment) {
|
|
24
24
|
return {
|
|
25
25
|
flow: this.flow.id,
|
|
26
26
|
name: this.flow.name,
|
|
27
27
|
environment,
|
|
28
|
-
status:
|
|
28
|
+
status: result.status,
|
|
29
29
|
startedAt: this.startedAt,
|
|
30
30
|
durationMs: Date.now() - this.started,
|
|
31
|
-
nodes: summary.nodes,
|
|
32
|
-
vars: summary.vars,
|
|
33
31
|
tests: this.tests,
|
|
34
|
-
|
|
32
|
+
runs: result.runs,
|
|
35
33
|
};
|
|
36
34
|
}
|
|
37
35
|
}
|
package/dist/flows/run/run.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnyNodeDefinition, type FlowFile } from "../core/index.js";
|
|
1
|
+
import { type AnyNodeDefinition, type FlowFacts, type FlowFile } from "../core/index.js";
|
|
2
2
|
import type { RunEvent } from "../../runner/reporter.js";
|
|
3
3
|
import type { FlowEvent, FlowSummary, NodeTest } from "./events.js";
|
|
4
4
|
import { type Handler } from "./handlers/index.js";
|
|
@@ -7,12 +7,23 @@ export interface RunFlowOptions {
|
|
|
7
7
|
flow: FlowFile;
|
|
8
8
|
/** The tests of each Test and Group node, by node id */
|
|
9
9
|
nodeTests: Record<string, NodeTest[]>;
|
|
10
|
-
/** The
|
|
10
|
+
/** The browser of this walk: a flow runs once per browser, `runFlow` walks it once */
|
|
11
|
+
browser: SupportedBrowser;
|
|
12
|
+
/**
|
|
13
|
+
* The browsers each Test and Group node runs in. A listed node whose list lacks `browser` is skipped with the
|
|
14
|
+
* note `not run in <browser>`; a node not listed runs
|
|
15
|
+
*/
|
|
11
16
|
nodeBrowsers?: Record<string, SupportedBrowser[]>;
|
|
12
17
|
environment: {
|
|
13
18
|
name: string;
|
|
14
19
|
variables: Record<string, string | number | boolean>;
|
|
15
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* The project's tests' inputs and outputs and its groups' tests: with them, each Test and Group node's inputs are
|
|
23
|
+
* wired (`wiringAt`) and sent to its tests, and expressions can read a Test node's outputs. Without them no input
|
|
24
|
+
* is sent
|
|
25
|
+
*/
|
|
26
|
+
facts?: FlowFacts;
|
|
16
27
|
/** @default "manual" */
|
|
17
28
|
trigger?: "manual";
|
|
18
29
|
/** @default now */
|
|
@@ -25,8 +36,10 @@ export interface RunFlowOptions {
|
|
|
25
36
|
tests: NodeTest[];
|
|
26
37
|
onEvent: (e: RunEvent) => void;
|
|
27
38
|
signal: AbortSignal;
|
|
28
|
-
/**
|
|
29
|
-
|
|
39
|
+
/** The run's browser */
|
|
40
|
+
browser: SupportedBrowser;
|
|
41
|
+
/** Values for the tests' inputs; absent when the node sends none */
|
|
42
|
+
inputs?: Record<string, string>;
|
|
30
43
|
}) => Promise<"passed" | "failed" | "cancelled">;
|
|
31
44
|
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
32
45
|
/**
|
|
@@ -41,8 +54,9 @@ export interface RunFlowOptions {
|
|
|
41
54
|
nodes?: readonly AnyNodeDefinition[];
|
|
42
55
|
}
|
|
43
56
|
/**
|
|
44
|
-
* Runs a flow
|
|
45
|
-
* Start (see `FlowWalker`) and reports how it
|
|
46
|
-
*
|
|
57
|
+
* Runs a flow once, in `browser` (`runFlowInProject` runs it once per browser): sets up what its nodes share
|
|
58
|
+
* (expression context, handler access to the tests), walks it from Start (see `FlowWalker`) and reports how it
|
|
59
|
+
* ended. Every event carries `browser`. A flow whose shape can't run ends with an `error`; a flow with a kind that
|
|
60
|
+
* has no handler throws before it starts, unless `unsupported` is `"skip"`.
|
|
47
61
|
*/
|
|
48
62
|
export declare function runFlow(options: RunFlowOptions): Promise<FlowSummary>;
|
package/dist/flows/run/run.js
CHANGED
|
@@ -5,12 +5,13 @@ import { HANDLERS } from "./handlers/index.js";
|
|
|
5
5
|
import { assertRunnable, defaultSleep, definitionOf, ShapeError, shapeError, } from "./prepare-flow.js";
|
|
6
6
|
import { FlowWalker } from "./walker.js";
|
|
7
7
|
/**
|
|
8
|
-
* Runs a flow
|
|
9
|
-
* Start (see `FlowWalker`) and reports how it
|
|
10
|
-
*
|
|
8
|
+
* Runs a flow once, in `browser` (`runFlowInProject` runs it once per browser): sets up what its nodes share
|
|
9
|
+
* (expression context, handler access to the tests), walks it from Start (see `FlowWalker`) and reports how it
|
|
10
|
+
* ended. Every event carries `browser`. A flow whose shape can't run ends with an `error`; a flow with a kind that
|
|
11
|
+
* has no handler throws before it starts, unless `unsupported` is `"skip"`.
|
|
11
12
|
*/
|
|
12
13
|
export async function runFlow(options) {
|
|
13
|
-
const { flow, onEvent = () => { } } = options;
|
|
14
|
+
const { flow, browser, onEvent = () => { } } = options;
|
|
14
15
|
const signal = options.signal ?? new AbortController().signal;
|
|
15
16
|
const handlers = options.handlers ?? HANDLERS;
|
|
16
17
|
const nodes = options.nodes ?? STEP_NODES;
|
|
@@ -26,22 +27,26 @@ export async function runFlow(options) {
|
|
|
26
27
|
startedAt: options.startedAt ?? new Date().toISOString(),
|
|
27
28
|
},
|
|
28
29
|
};
|
|
29
|
-
const handlerRunFor = (context, signal) => ({
|
|
30
|
+
const handlerRunFor = (context, outputsById, signal) => ({
|
|
30
31
|
flow,
|
|
31
32
|
environment: options.environment,
|
|
32
33
|
context,
|
|
34
|
+
outputsById,
|
|
35
|
+
...(options.facts && { facts: options.facts }),
|
|
33
36
|
nodeTests: options.nodeTests,
|
|
37
|
+
browser,
|
|
34
38
|
...(options.nodeBrowsers && { nodeBrowsers: options.nodeBrowsers }),
|
|
35
39
|
signal,
|
|
36
40
|
sleep: options.sleep ?? defaultSleep,
|
|
37
|
-
runTests: (nodeId, tests, onTestEvent,
|
|
41
|
+
runTests: (nodeId, tests, onTestEvent, inputs) => options.runTests({
|
|
38
42
|
nodeId,
|
|
39
43
|
tests,
|
|
40
44
|
onEvent: onTestEvent,
|
|
41
45
|
signal,
|
|
42
|
-
|
|
46
|
+
browser,
|
|
47
|
+
...(inputs && { inputs }),
|
|
43
48
|
}),
|
|
44
|
-
onTestEvent: (nodeId, event) => onEvent({ type: "test", nodeId, event }),
|
|
49
|
+
onTestEvent: (nodeId, event) => onEvent({ type: "test", browser, nodeId, event }),
|
|
45
50
|
nodes,
|
|
46
51
|
log: (nodeId, level, text) => logger[level](`[${nodeId}] ${text}`),
|
|
47
52
|
});
|
|
@@ -52,9 +57,14 @@ export async function runFlow(options) {
|
|
|
52
57
|
nodes,
|
|
53
58
|
handlerRunFor,
|
|
54
59
|
context,
|
|
55
|
-
|
|
60
|
+
outputsById: {},
|
|
61
|
+
onEvent: (event) => onEvent({ ...event, browser }),
|
|
62
|
+
});
|
|
63
|
+
onEvent({
|
|
64
|
+
type: "flow-start",
|
|
65
|
+
browser,
|
|
66
|
+
environment: options.environment.name,
|
|
56
67
|
});
|
|
57
|
-
onEvent({ type: "flow-start", environment: options.environment.name });
|
|
58
68
|
let error;
|
|
59
69
|
const start = flow.nodes.find((n) => definitionOf(nodes, n.kind)?.shape === "start");
|
|
60
70
|
try {
|
|
@@ -82,6 +92,6 @@ export async function runFlow(options) {
|
|
|
82
92
|
vars: context.vars,
|
|
83
93
|
...(error ? { error } : {}),
|
|
84
94
|
};
|
|
85
|
-
onEvent({ type: "flow-end", summary });
|
|
95
|
+
onEvent({ type: "flow-end", browser, summary });
|
|
86
96
|
return summary;
|
|
87
97
|
}
|
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import { type AnyNodeDefinition, type ExpressionContext, type FlowFile } from "../core/index.js";
|
|
2
|
-
import { type Fork } from "./branch-context.js";
|
|
3
|
-
import type {
|
|
2
|
+
import { type Fork, type OutputsById } from "./branch-context.js";
|
|
3
|
+
import type { NodeResult } from "./events.js";
|
|
4
4
|
import type { Handler, HandlerRun } from "./handlers/index.js";
|
|
5
|
+
/** A node's event as the walker sends it; `runFlow` adds the run's browser */
|
|
6
|
+
type NodeEvent = {
|
|
7
|
+
type: "node-start";
|
|
8
|
+
nodeId: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "node-end";
|
|
11
|
+
nodeId: string;
|
|
12
|
+
result: NodeResult;
|
|
13
|
+
};
|
|
5
14
|
/** What the walker needs of the run: the flow, who handles each kind, and where results and events go */
|
|
6
15
|
export interface WalkerInput {
|
|
7
16
|
flow: FlowFile;
|
|
8
17
|
signal: AbortSignal;
|
|
9
18
|
handlers: Readonly<Record<string, Handler>>;
|
|
10
19
|
nodes: readonly AnyNodeDefinition[];
|
|
11
|
-
/**
|
|
12
|
-
|
|
20
|
+
/**
|
|
21
|
+
* What the handlers get for a context, the outputs by node id and a signal; the walker also writes each result
|
|
22
|
+
* into `context.nodes` and its outputs into `outputsById`
|
|
23
|
+
*/
|
|
24
|
+
handlerRunFor: (context: ExpressionContext, outputsById: OutputsById, signal: AbortSignal) => HandlerRun;
|
|
13
25
|
context: ExpressionContext;
|
|
14
|
-
|
|
26
|
+
outputsById: OutputsById;
|
|
27
|
+
onEvent: (event: NodeEvent) => void;
|
|
15
28
|
}
|
|
16
29
|
/**
|
|
17
30
|
* Walks the flow from Start by the shape of each node's kind: a step runs and follows its one way out; a node
|
|
@@ -33,8 +46,10 @@ export declare class FlowWalker {
|
|
|
33
46
|
results: Record<string, NodeResult>;
|
|
34
47
|
visited: Set<string>;
|
|
35
48
|
});
|
|
36
|
-
/** The variables and node outputs this walk reads and writes */
|
|
49
|
+
/** The variables and node outputs (by key) this walk reads and writes */
|
|
37
50
|
get context(): ExpressionContext;
|
|
51
|
+
/** The node outputs by node id this walk reads and writes */
|
|
52
|
+
get outputsById(): OutputsById;
|
|
38
53
|
private definition;
|
|
39
54
|
private record;
|
|
40
55
|
private out;
|
|
@@ -49,8 +64,9 @@ export declare class FlowWalker {
|
|
|
49
64
|
* the others.
|
|
50
65
|
*/
|
|
51
66
|
private walkBranches;
|
|
52
|
-
/** A walker for one branch: the same flow, results and visited nodes, but its own variables
|
|
67
|
+
/** A walker for one branch: the same flow, results and visited nodes, but its own variables, node outputs and signal */
|
|
53
68
|
forBranch(fork: Fork, signal: AbortSignal): FlowWalker;
|
|
54
69
|
/** Runs the chain from `from` up to `stop` (undefined: through End) */
|
|
55
70
|
walk(from: string, stop?: string): Promise<void>;
|
|
56
71
|
}
|
|
72
|
+
export {};
|
package/dist/flows/run/walker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inFillOrder, meetOf, } from "../core/index.js";
|
|
2
|
-
import { forkContext, mergeForks } from "./branch-context.js";
|
|
2
|
+
import { forkContext, mergeForks, } from "./branch-context.js";
|
|
3
3
|
import { definitionOf, dynamicBranches, shapeError } from "./prepare-flow.js";
|
|
4
4
|
/**
|
|
5
5
|
* Walks the flow from Start by the shape of each node's kind: a step runs and follows its one way out; a node
|
|
@@ -13,17 +13,23 @@ export class FlowWalker {
|
|
|
13
13
|
this.byId = new Map(input.flow.nodes.map((n) => [n.id, n]));
|
|
14
14
|
this.results = shared.results;
|
|
15
15
|
this.visited = shared.visited;
|
|
16
|
-
this.handlerRun = input.handlerRunFor(input.context, input.signal);
|
|
16
|
+
this.handlerRun = input.handlerRunFor(input.context, input.outputsById, input.signal);
|
|
17
17
|
}
|
|
18
|
-
/** The variables and node outputs this walk reads and writes */
|
|
18
|
+
/** The variables and node outputs (by key) this walk reads and writes */
|
|
19
19
|
get context() {
|
|
20
20
|
return this.input.context;
|
|
21
21
|
}
|
|
22
|
+
/** The node outputs by node id this walk reads and writes */
|
|
23
|
+
get outputsById() {
|
|
24
|
+
return this.input.outputsById;
|
|
25
|
+
}
|
|
22
26
|
definition(kind) {
|
|
23
27
|
return definitionOf(this.input.nodes, kind);
|
|
24
28
|
}
|
|
25
29
|
record(node, result) {
|
|
26
30
|
this.results[node.id] = result;
|
|
31
|
+
if (result.outputs)
|
|
32
|
+
this.input.outputsById[node.id] = result.outputs;
|
|
27
33
|
if (node.key)
|
|
28
34
|
this.input.context.nodes[node.key] = {
|
|
29
35
|
...result.outputs,
|
|
@@ -131,7 +137,7 @@ export class FlowWalker {
|
|
|
131
137
|
delete this.results[node.id];
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
134
|
-
const { clash } = mergeForks(this.input.context, walks.map((w) => w.fork));
|
|
140
|
+
const { clash } = mergeForks(this.input.context, this.input.outputsById, walks.map((w) => w.fork));
|
|
135
141
|
// The node fails when anything in its branches failed
|
|
136
142
|
const failed = flow.nodes.some((n) => this.results[n.id]?.status === "failed" &&
|
|
137
143
|
edges.some((e) => e.target !== meet && this.reachesBefore(e.target, n.id, meet)));
|
|
@@ -156,9 +162,14 @@ export class FlowWalker {
|
|
|
156
162
|
if (ended.status === "failed" && node.onFailure === "stop")
|
|
157
163
|
this.stopped = "failed";
|
|
158
164
|
}
|
|
159
|
-
/** A walker for one branch: the same flow, results and visited nodes, but its own variables
|
|
165
|
+
/** A walker for one branch: the same flow, results and visited nodes, but its own variables, node outputs and signal */
|
|
160
166
|
forBranch(fork, signal) {
|
|
161
|
-
return new FlowWalker({
|
|
167
|
+
return new FlowWalker({
|
|
168
|
+
...this.input,
|
|
169
|
+
context: fork.context,
|
|
170
|
+
outputsById: fork.outputsById,
|
|
171
|
+
signal,
|
|
172
|
+
}, { results: this.results, visited: this.visited });
|
|
162
173
|
}
|
|
163
174
|
/** Runs the chain from `from` up to `stop` (undefined: through End) */
|
|
164
175
|
async walk(from, stop) {
|
|
@@ -208,7 +219,7 @@ export class FlowWalker {
|
|
|
208
219
|
/** One branch of a Parallel node, walking from its first node up to where the branches meet */
|
|
209
220
|
class BranchWalk {
|
|
210
221
|
constructor(parent, from, meet, cancel) {
|
|
211
|
-
this.fork = forkContext(parent.context);
|
|
222
|
+
this.fork = forkContext(parent.context, parent.outputsById);
|
|
212
223
|
this.walker = parent.forBranch(this.fork, cancel.signal);
|
|
213
224
|
this.done = this.walker.walk(from, meet).then(() => {
|
|
214
225
|
// A failure that stops the flow cancels the branches still running
|
|
@@ -169,5 +169,7 @@ export default class Locator {
|
|
|
169
169
|
*/
|
|
170
170
|
press(key: string, options?: ActionOptions): Promise<void>;
|
|
171
171
|
/** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
|
|
172
|
-
fill(value: string, options?: ActionOptions
|
|
172
|
+
fill(value: string, options?: ActionOptions & {
|
|
173
|
+
output?: string;
|
|
174
|
+
}): Promise<void>;
|
|
173
175
|
}
|
package/dist/locator/locator.js
CHANGED
|
@@ -6,6 +6,7 @@ import { probeState, waitForActionable, waitForState, } from "./actionability.js
|
|
|
6
6
|
import { ChainResolver } from "./chain-resolver.js";
|
|
7
7
|
import { dropFiles, setInputFiles, } from "./file-actions.js";
|
|
8
8
|
import { describeType, } from "./element-state.js";
|
|
9
|
+
import { bindOutput } from "../wait/output-sink.js";
|
|
9
10
|
import { cssPropertyName, IS_CHECKED_BODY } from "./element-reads.js";
|
|
10
11
|
import { clickRect, DEFAULT_PAUSE_MS, NAMED_KEYS, pause, pressKey, typeText, } from "./input-actions.js";
|
|
11
12
|
import { CLICK_CHECKS, FILL_CHECKS, PRESS_CHECKS, SCREENSHOT_CHECKS, ATTACHED_ONLY, } from "./page-scripts.js";
|
|
@@ -347,6 +348,10 @@ export default class Locator {
|
|
|
347
348
|
}
|
|
348
349
|
/** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
|
|
349
350
|
async fill(value, options) {
|
|
351
|
+
// Taken when the call starts, so a fill that outlives its test saves into that test's closed sink
|
|
352
|
+
const save = options?.output === undefined
|
|
353
|
+
? undefined
|
|
354
|
+
: bindOutput("fill()", options.output);
|
|
350
355
|
const state = await this.waitForActionable("fill", FILL_CHECKS, { scroll: true, hitTest: true }, options);
|
|
351
356
|
// Typing over the selected existing value replaces it; Backspace clears it for an empty value
|
|
352
357
|
const keys = value === "" ? NAMED_KEYS.Backspace : value;
|
|
@@ -355,6 +360,7 @@ export default class Locator {
|
|
|
355
360
|
await this.wait();
|
|
356
361
|
await this.callOnElement(undefined, `if (typeof el.select === "function") el.select();`);
|
|
357
362
|
await typeText(this.biDiConnector, this.contextId, keys);
|
|
363
|
+
save?.(value);
|
|
358
364
|
}
|
|
359
365
|
}
|
|
360
366
|
function booleanOrNull(value) {
|
package/dist/recorder/apply.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { applyEdit } from "../steps/edit.js";
|
|
2
|
+
import { parseSpec } from "../steps/parse.js";
|
|
2
3
|
/** Writes a recorder event into the spec source, as an edit of test number `test` */
|
|
3
4
|
export function applyRecorderEvent(source, test, event) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
if (event.op === "remove")
|
|
6
|
+
return applyEdit(source, { op: "remove", test, index: event.index });
|
|
7
|
+
const { declare } = event.op === "insert" ? event : {};
|
|
8
|
+
const inputs = parseSpec(source)[test]?.inputs ?? [];
|
|
9
|
+
const declared = declare && !inputs.some((input) => input.name === declare.name)
|
|
10
|
+
? applyEdit(source, {
|
|
11
|
+
op: "declare",
|
|
8
12
|
test,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
inputs: [...inputs, declare],
|
|
14
|
+
})
|
|
15
|
+
: source;
|
|
16
|
+
return applyEdit(declared, {
|
|
17
|
+
op: event.op,
|
|
18
|
+
test,
|
|
19
|
+
index: event.index,
|
|
20
|
+
step: event.step,
|
|
21
|
+
});
|
|
12
22
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { Expectation, FileEntry, LocatorSpec, PageExpectation, Step } from "../steps/model.js";
|
|
1
|
+
import type { Expectation, FileEntry, LocatorSpec, PageExpectation, Step, TestInput } from "../steps/model.js";
|
|
2
2
|
/** What the recorder does to the step list: a new step at `index`, a step rewritten in place, or one taken out */
|
|
3
|
-
export type RecorderEvent =
|
|
3
|
+
export type RecorderEvent =
|
|
4
|
+
/** `declare`: an input the step reads, declared with its default before the step goes in */
|
|
5
|
+
{
|
|
4
6
|
op: "insert";
|
|
5
7
|
index: number;
|
|
6
8
|
step: Step;
|
|
9
|
+
declare?: TestInput;
|
|
7
10
|
} | {
|
|
8
11
|
op: "replace";
|
|
9
12
|
index: number;
|
|
@@ -42,6 +45,16 @@ export type Observation = {
|
|
|
42
45
|
not: boolean;
|
|
43
46
|
expectation: Expectation;
|
|
44
47
|
}
|
|
48
|
+
/** The toolbar's Save as output: the element's text, or what `pattern` captures of it */
|
|
49
|
+
| {
|
|
50
|
+
type: "saveOutput";
|
|
51
|
+
locator: LocatorSpec;
|
|
52
|
+
name: string;
|
|
53
|
+
pattern?: {
|
|
54
|
+
source: string;
|
|
55
|
+
flags: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
45
58
|
/** The toolbar's Page panel: the page's URL or title */
|
|
46
59
|
| {
|
|
47
60
|
type: "expectPage";
|
|
@@ -51,9 +64,11 @@ export type Observation = {
|
|
|
51
64
|
type: "insert";
|
|
52
65
|
locator: LocatorSpec;
|
|
53
66
|
value: {
|
|
54
|
-
kind: "env" | "secret";
|
|
67
|
+
kind: "env" | "secret" | "input";
|
|
55
68
|
name: string;
|
|
56
69
|
};
|
|
70
|
+
/** A new input: its default */
|
|
71
|
+
declare?: string;
|
|
57
72
|
}
|
|
58
73
|
/** A file input got a click: the click recorded just before it opened the file picker */
|
|
59
74
|
| {
|
|
@@ -20,10 +20,15 @@ export class Normaliser {
|
|
|
20
20
|
get nextIndex() {
|
|
21
21
|
return this.next;
|
|
22
22
|
}
|
|
23
|
-
insert(step) {
|
|
23
|
+
insert(step, declare) {
|
|
24
24
|
this.lastStep = step;
|
|
25
25
|
this.picker = undefined;
|
|
26
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
op: "insert",
|
|
28
|
+
index: this.next++,
|
|
29
|
+
step,
|
|
30
|
+
...(declare && { declare }),
|
|
31
|
+
};
|
|
27
32
|
}
|
|
28
33
|
/** The path (and query) of `url` when it is inside the base URL, else `url` */
|
|
29
34
|
relative(url) {
|
|
@@ -47,6 +52,13 @@ export class Normaliser {
|
|
|
47
52
|
not: observation.not,
|
|
48
53
|
expectation: observation.expectation,
|
|
49
54
|
};
|
|
55
|
+
case "saveOutput":
|
|
56
|
+
return {
|
|
57
|
+
kind: "saveOutput",
|
|
58
|
+
name: observation.name,
|
|
59
|
+
locator: observation.locator,
|
|
60
|
+
...(observation.pattern && { pattern: observation.pattern }),
|
|
61
|
+
};
|
|
50
62
|
case "expectPage":
|
|
51
63
|
return {
|
|
52
64
|
kind: "expectPage",
|
|
@@ -79,7 +91,10 @@ export class Normaliser {
|
|
|
79
91
|
const fromToolbar = this.toolbarStep(observation);
|
|
80
92
|
if (fromToolbar) {
|
|
81
93
|
this.lastFill = undefined;
|
|
82
|
-
|
|
94
|
+
const declare = observation.type === "insert" && observation.declare !== undefined
|
|
95
|
+
? { name: observation.value.name, default: observation.declare }
|
|
96
|
+
: undefined;
|
|
97
|
+
return [this.insert(fromToolbar, declare)];
|
|
83
98
|
}
|
|
84
99
|
switch (observation.type) {
|
|
85
100
|
case "picker":
|
|
@@ -26,6 +26,10 @@ export async function toolbarOptions(run, source, dir, config, testIndex) {
|
|
|
26
26
|
variables: run.settings.variables,
|
|
27
27
|
secrets,
|
|
28
28
|
secretsIgnored: await isGitIgnored(dir, `.env.${environment}`),
|
|
29
|
+
inputs: Object.fromEntries((tests[testIndex]?.inputs ?? []).map((input) => [
|
|
30
|
+
input.name,
|
|
31
|
+
input.default,
|
|
32
|
+
])),
|
|
29
33
|
screenshots: tests[testIndex]?.steps.filter(({ step }) => step.kind === "screenshot")
|
|
30
34
|
.length ?? 0,
|
|
31
35
|
};
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { ActionReply } from "./protocol.js";
|
|
2
|
-
import type { Message,
|
|
3
|
-
/** The messages that expect a reply, and that a double click sends twice */
|
|
4
|
-
type Replying = Of<"assert" | "assert-page" | "insert" | "add-insertable">;
|
|
2
|
+
import type { Message, Replying } from "./types.js";
|
|
5
3
|
/**
|
|
6
4
|
* Drops the repeat of an action that is running or was answered `{ ok: true }` (a double click: its first copy
|
|
7
5
|
* answers), and lets a retry after `{ ok: false }` through.
|
|
8
6
|
*/
|
|
9
7
|
export declare class ActionGuard {
|
|
10
|
-
/** Ids of
|
|
8
|
+
/** Ids of the replying actions (`REPLYING`) being processed */
|
|
11
9
|
private readonly inFlight;
|
|
12
10
|
/** Ids `accept` let through whose `run` hasn't started yet */
|
|
13
11
|
private readonly claimed;
|
|
@@ -18,4 +16,3 @@ export declare class ActionGuard {
|
|
|
18
16
|
/** Runs an action that expects a reply, unless it is a repeat (see `accept`); `reply` gets the answer */
|
|
19
17
|
run(message: Replying, work: () => Promise<ActionReply>, reply: (id: number, result: ActionReply) => void): Promise<void>;
|
|
20
18
|
}
|
|
21
|
-
export {};
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
const REPLYING = [
|
|
2
|
+
"assert",
|
|
3
|
+
"assert-page",
|
|
4
|
+
"insert",
|
|
5
|
+
"add-insertable",
|
|
6
|
+
"save-output",
|
|
7
|
+
];
|
|
1
8
|
/**
|
|
2
9
|
* Drops the repeat of an action that is running or was answered `{ ok: true }` (a double click: its first copy
|
|
3
10
|
* answers), and lets a retry after `{ ok: false }` through.
|
|
4
11
|
*/
|
|
5
12
|
export class ActionGuard {
|
|
6
13
|
constructor() {
|
|
7
|
-
/** Ids of
|
|
14
|
+
/** Ids of the replying actions (`REPLYING`) being processed */
|
|
8
15
|
this.inFlight = new Set();
|
|
9
16
|
/** Ids `accept` let through whose `run` hasn't started yet */
|
|
10
17
|
this.claimed = new Set();
|
|
@@ -13,10 +20,7 @@ export class ActionGuard {
|
|
|
13
20
|
}
|
|
14
21
|
/** Whether to process `message`; call it as the message arrives, before queueing it */
|
|
15
22
|
accept(message) {
|
|
16
|
-
if (message.action
|
|
17
|
-
message.action !== "assert-page" &&
|
|
18
|
-
message.action !== "insert" &&
|
|
19
|
-
message.action !== "add-insertable")
|
|
23
|
+
if (!REPLYING.includes(message.action))
|
|
20
24
|
return true;
|
|
21
25
|
const { id } = message;
|
|
22
26
|
if (this.inFlight.has(id) || this.done.has(id))
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ActionReply } from "../protocol.js";
|
|
2
2
|
import type { ActionContext, Of } from "../types.js";
|
|
3
|
-
/** Types a variable or
|
|
3
|
+
/** Types a variable, secret or input the tester already has into the field they picked, and records one `insert` step */
|
|
4
4
|
export declare function insertAction(ctx: ActionContext, message: Of<"insert">): Promise<ActionReply>;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Adds a new variable, secret or input, then types it into the field and records the `insert` step. A variable or
|
|
7
|
+
* secret is saved to the project first; an input is declared by the step itself.
|
|
8
|
+
*/
|
|
6
9
|
export declare function addInsertableAction(ctx: ActionContext, message: Of<"add-insertable">): Promise<ActionReply>;
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
import { ConfigEditError } from "../../../project/config-edit.js";
|
|
2
2
|
import { SECRET_NAME } from "../../../project/secrets-file.js";
|
|
3
3
|
import { ExistsError, VARIABLE_NAME } from "../../../project/variables.js";
|
|
4
|
-
import { fail, NO_ENVIRONMENT, PAUSED, TYPE_FAILED, UNIDENTIFIABLE, } from "./reply.js";
|
|
5
|
-
/** Types a variable or
|
|
4
|
+
import { fail, nameProblem, NO_ENVIRONMENT, PAUSED, TYPE_FAILED, UNIDENTIFIABLE, } from "./reply.js";
|
|
5
|
+
/** Types a variable, secret or input the tester already has into the field they picked, and records one `insert` step */
|
|
6
6
|
export async function insertAction(ctx, message) {
|
|
7
7
|
if (ctx.isPaused())
|
|
8
8
|
return fail(PAUSED);
|
|
9
9
|
const { kind, name } = message;
|
|
10
|
-
const value = kind === "secret"
|
|
10
|
+
const value = kind === "secret"
|
|
11
|
+
? ctx.secrets.get(name)
|
|
12
|
+
: kind === "input"
|
|
13
|
+
? ctx.inputs.get(name)
|
|
14
|
+
: ctx.variables.get(name);
|
|
11
15
|
if (value === undefined)
|
|
12
16
|
return fail(kind === "secret"
|
|
13
17
|
? `Set a value for ${name} first`
|
|
14
|
-
:
|
|
18
|
+
: kind === "input"
|
|
19
|
+
? `There is no input "${name}"`
|
|
20
|
+
: `There is no variable "${name}"`);
|
|
15
21
|
const locator = await locate(ctx, message);
|
|
16
22
|
if (!locator)
|
|
17
23
|
return fail(UNIDENTIFIABLE);
|
|
18
24
|
return typeIn(ctx, locator, kind, name, value);
|
|
19
25
|
}
|
|
20
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Adds a new variable, secret or input, then types it into the field and records the `insert` step. A variable or
|
|
28
|
+
* secret is saved to the project first; an input is declared by the step itself.
|
|
29
|
+
*/
|
|
21
30
|
export async function addInsertableAction(ctx, message) {
|
|
22
31
|
if (ctx.isPaused())
|
|
23
32
|
return fail(PAUSED);
|
|
24
33
|
const { kind, name } = message;
|
|
25
34
|
const value = String(message.value ?? "");
|
|
35
|
+
if (kind === "input")
|
|
36
|
+
return addInput(ctx, message, name, value);
|
|
26
37
|
if (kind === "env" && !ctx.options.canAddVariables)
|
|
27
38
|
return fail(NO_ENVIRONMENT);
|
|
28
39
|
const known = kind === "env"
|
|
@@ -56,6 +67,26 @@ export async function addInsertableAction(ctx, message) {
|
|
|
56
67
|
ctx.push();
|
|
57
68
|
return typeIn(ctx, locator, kind, name, value, true);
|
|
58
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Types a new input's value and records the step with the declaration; the project is not written, the
|
|
72
|
+
* test's own `inputs` carries the default. The name counts as taken once the step is recorded.
|
|
73
|
+
*/
|
|
74
|
+
async function addInput(ctx, message, name, value) {
|
|
75
|
+
const problem = nameProblem(name);
|
|
76
|
+
if (problem)
|
|
77
|
+
return fail(problem, "name");
|
|
78
|
+
if (ctx.inputs.has(name))
|
|
79
|
+
return fail(`"${name}" already exists`, "name");
|
|
80
|
+
const locator = await locate(ctx, message);
|
|
81
|
+
if (!locator)
|
|
82
|
+
return fail(UNIDENTIFIABLE);
|
|
83
|
+
const reply = await typeIn(ctx, locator, "input", name, value, false, true);
|
|
84
|
+
if (reply.ok) {
|
|
85
|
+
ctx.inputs.set(name, value);
|
|
86
|
+
ctx.push();
|
|
87
|
+
}
|
|
88
|
+
return reply;
|
|
89
|
+
}
|
|
59
90
|
/** Undefined when `work` succeeded, else the reply for its error */
|
|
60
91
|
async function write(work, replyFor) {
|
|
61
92
|
try {
|
|
@@ -76,7 +107,7 @@ async function locate(ctx, message) {
|
|
|
76
107
|
return undefined; // the element went away
|
|
77
108
|
}
|
|
78
109
|
}
|
|
79
|
-
async function typeIn(ctx, locator, kind, name, value, saved = false) {
|
|
110
|
+
async function typeIn(ctx, locator, kind, name, value, saved = false, declare = false) {
|
|
80
111
|
try {
|
|
81
112
|
await ctx.deps.type(locator, value);
|
|
82
113
|
}
|
|
@@ -86,7 +117,12 @@ async function typeIn(ctx, locator, kind, name, value, saved = false) {
|
|
|
86
117
|
ctx.push();
|
|
87
118
|
return fail(TYPE_FAILED);
|
|
88
119
|
}
|
|
89
|
-
ctx.deps.observe({
|
|
120
|
+
ctx.deps.observe({
|
|
121
|
+
type: "insert",
|
|
122
|
+
locator,
|
|
123
|
+
value: { kind, name },
|
|
124
|
+
...(declare && { declare: value }),
|
|
125
|
+
});
|
|
90
126
|
return { ok: true };
|
|
91
127
|
}
|
|
92
128
|
const errorText = (err) => err instanceof Error ? err.message : String(err);
|