@itmetsam/samurai-framework 0.20.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- 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/browser/browser.js +19 -3
- package/dist/browser/chrome-launch.d.ts +2 -0
- package/dist/browser/chrome-launch.js +5 -0
- package/dist/capabilities.d.ts +16 -5
- package/dist/capabilities.js +16 -5
- 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/config/mask.d.ts +6 -2
- package/dist/config/mask.js +4 -1
- 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/node-folder.d.ts +8 -3
- package/dist/flows/run/node-folder.js +28 -4
- 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.d.ts +3 -0
- package/dist/recorder/toolbar/actions/pick.js +12 -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 +15 -0
- package/dist/recorder/toolbar/actions/save-output.js +73 -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 +16 -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 +43 -3
- package/dist/recorder/toolbar/source/events-page.js +2 -1
- package/dist/recorder/toolbar/source/fields.js +11 -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 +14 -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 +62 -10
- package/dist/recorder/toolbar/source/views-rail.js +5 -2
- package/dist/recorder/toolbar/source.d.ts +6 -5
- package/dist/recorder/toolbar/source.js +6 -5
- package/dist/recorder/toolbar/types.d.ts +12 -5
- 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 +44 -4
|
@@ -14,8 +14,9 @@ import TestRunner from "../../runner/test-runner.js";
|
|
|
14
14
|
import { WorkerPool } from "../../runner/worker-pool.js";
|
|
15
15
|
import { processWorkerFactory } from "../../runner/worker-process.js";
|
|
16
16
|
import { readFlow } from "./files.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { flowFacts } from "./project-facts.js";
|
|
18
|
+
import { nodeRunner } from "./node-runner.js";
|
|
19
|
+
import { flowBrowsers, runPerBrowser } from "./per-browser.js";
|
|
19
20
|
import { FlowCheckError, } from "./project-options.js";
|
|
20
21
|
import { projectSteps } from "../steps/set.js";
|
|
21
22
|
import { HANDLERS } from "./handlers/index.js";
|
|
@@ -34,8 +35,8 @@ export function chooseEnvironment(config, flow, option) {
|
|
|
34
35
|
return flow.envDefault;
|
|
35
36
|
return config.defaultEnvironment ?? names[0] ?? "default";
|
|
36
37
|
}
|
|
37
|
-
/** The tests, and the browsers, of each Test and Group node of a flow
|
|
38
|
-
export function nodeTestsOf(flow, tests, groups) {
|
|
38
|
+
/** The tests, and the browsers, of each Test and Group node of a flow: a Test node's are the project's, a Group node's its group's */
|
|
39
|
+
export function nodeTestsOf(flow, tests, groups, projectBrowsers) {
|
|
39
40
|
const byId = new Map(tests.map((t) => [t.id, t]));
|
|
40
41
|
const nodeTests = {};
|
|
41
42
|
const nodeBrowsers = {};
|
|
@@ -52,6 +53,8 @@ export function nodeTestsOf(flow, tests, groups) {
|
|
|
52
53
|
continue;
|
|
53
54
|
if (group)
|
|
54
55
|
nodeBrowsers[node.id] = group.browsers;
|
|
56
|
+
if (node.kind === "test")
|
|
57
|
+
nodeBrowsers[node.id] = projectBrowsers;
|
|
55
58
|
nodeTests[node.id] = ids.flatMap((id) => {
|
|
56
59
|
const t = byId.get(id);
|
|
57
60
|
return t ? [{ file: t.file, name: t.name }] : [];
|
|
@@ -92,10 +95,13 @@ async function prepare(options) {
|
|
|
92
95
|
file: t.file,
|
|
93
96
|
name: t.name,
|
|
94
97
|
relative: toPosix(path.relative(srcDir, t.file)),
|
|
98
|
+
...(t.inputs && { inputs: t.inputs }),
|
|
99
|
+
...(t.outputs && { outputs: t.outputs }),
|
|
95
100
|
}));
|
|
96
101
|
const groups = resolveGroups(config, tests.map((t) => ({ file: t.relative, name: t.name })), realDir, srcDir);
|
|
97
102
|
return { tests, groups };
|
|
98
103
|
});
|
|
104
|
+
const facts = flowFacts(tests, groups);
|
|
99
105
|
const problems = checkFlow(flow, {
|
|
100
106
|
tests: tests.map((t) => t.id),
|
|
101
107
|
groups: groups.map((g) => g.name),
|
|
@@ -106,6 +112,7 @@ async function prepare(options) {
|
|
|
106
112
|
]),
|
|
107
113
|
unsupported,
|
|
108
114
|
nodes: steps.nodes,
|
|
115
|
+
facts,
|
|
109
116
|
});
|
|
110
117
|
return {
|
|
111
118
|
projectDir,
|
|
@@ -117,23 +124,27 @@ async function prepare(options) {
|
|
|
117
124
|
environment,
|
|
118
125
|
tests,
|
|
119
126
|
groups,
|
|
127
|
+
facts,
|
|
120
128
|
problems,
|
|
121
129
|
};
|
|
122
130
|
}
|
|
123
131
|
/**
|
|
124
132
|
* Checks a flow against the project without running it: its tests, groups and environment
|
|
125
|
-
* variables,
|
|
133
|
+
* variables, which kinds can run, and where each Test and Group node's inputs come from. Returns the flow (with keys) and every problem, warnings included.
|
|
126
134
|
*/
|
|
127
135
|
export async function checkFlowInProject(options) {
|
|
128
136
|
const { flow, problems } = await prepare(options);
|
|
129
137
|
return { flow, problems };
|
|
130
138
|
}
|
|
131
139
|
async function walkFlowInProject(options) {
|
|
132
|
-
const { projectDir, flow, unsupported, config, steps, name, environment, tests, groups, problems, } = await prepare(options);
|
|
140
|
+
const { projectDir, flow, unsupported, config, steps, name, environment, tests, groups, facts, problems, } = await prepare(options);
|
|
133
141
|
const errors = problems.filter((p) => p.level === "error");
|
|
134
142
|
if (errors.length)
|
|
135
143
|
throw new FlowCheckError(errors);
|
|
136
|
-
const
|
|
144
|
+
const projectBrowsers = browsersFor({ project: config.browsers });
|
|
145
|
+
const { nodeTests, nodeBrowsers } = nodeTestsOf(flow, tests, groups, projectBrowsers);
|
|
146
|
+
// Before anything starts: a list of browsers this flow doesn't use is refused
|
|
147
|
+
const browsers = flowBrowsers(nodeBrowsers, flow, projectBrowsers, options.browsers);
|
|
137
148
|
const run = options.testRunner ?? runTests;
|
|
138
149
|
// An injected runner (tests) stays in this process unless the workers are asked for
|
|
139
150
|
const asked = options.workers !== undefined ||
|
|
@@ -183,50 +194,35 @@ async function walkFlowInProject(options) {
|
|
|
183
194
|
},
|
|
184
195
|
})
|
|
185
196
|
: undefined;
|
|
197
|
+
const walk = (browser) => runFlow({
|
|
198
|
+
flow,
|
|
199
|
+
browser,
|
|
200
|
+
nodeTests,
|
|
201
|
+
nodeBrowsers,
|
|
202
|
+
environment,
|
|
203
|
+
facts,
|
|
204
|
+
...(options.signal && { signal: options.signal }),
|
|
205
|
+
...(options.onEvent && { onEvent: options.onEvent }),
|
|
206
|
+
...(options.sleep && { sleep: options.sleep }),
|
|
207
|
+
unsupported,
|
|
208
|
+
handlers: { ...HANDLERS, ...steps.handlers },
|
|
209
|
+
nodes: steps.nodes,
|
|
210
|
+
runTests: nodeRunner({
|
|
211
|
+
projectDir,
|
|
212
|
+
config,
|
|
213
|
+
options,
|
|
214
|
+
...(hasEnvironment(config, name) && { environment: name }),
|
|
215
|
+
retries,
|
|
216
|
+
run,
|
|
217
|
+
...(pool && { pool }),
|
|
218
|
+
}, browser),
|
|
219
|
+
});
|
|
186
220
|
try {
|
|
187
|
-
return await
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
nodeBrowsers,
|
|
191
|
-
environment,
|
|
221
|
+
return await runPerBrowser({
|
|
222
|
+
browsers,
|
|
223
|
+
walk,
|
|
192
224
|
...(options.signal && { signal: options.signal }),
|
|
193
225
|
...(options.onEvent && { onEvent: options.onEvent }),
|
|
194
|
-
...(options.sleep && { sleep: options.sleep }),
|
|
195
|
-
unsupported,
|
|
196
|
-
handlers: { ...HANDLERS, ...steps.handlers },
|
|
197
|
-
nodes: steps.nodes,
|
|
198
|
-
runTests: async ({ nodeId, tests, onEvent, signal, browsers }) => {
|
|
199
|
-
// Each node gets a folder of its own: the same test in two nodes would overwrite its screenshots
|
|
200
|
-
const outputDir = options.outputDir !== undefined
|
|
201
|
-
? path.join(options.outputDir, ...nodeFolder(nodeId).split("/"))
|
|
202
|
-
: undefined;
|
|
203
|
-
if (pool)
|
|
204
|
-
return runOnPool(pool, itemsOf(tests, browsers ?? browsersFor({ project: config.browsers }), outputDir), onEvent, signal);
|
|
205
|
-
const summary = await run({
|
|
206
|
-
projectDir,
|
|
207
|
-
config,
|
|
208
|
-
...(options.dataDir !== undefined && { dataDir: options.dataDir }),
|
|
209
|
-
// The environment the expressions see, whenever the project defines it
|
|
210
|
-
...(hasEnvironment(config, name) ? { environment: name } : {}),
|
|
211
|
-
...(options.headless !== undefined && { headless: options.headless }),
|
|
212
|
-
...(options.timeout !== undefined && { timeout: options.timeout }),
|
|
213
|
-
...(options.expectTimeout !== undefined && {
|
|
214
|
-
expectTimeout: options.expectTimeout,
|
|
215
|
-
}),
|
|
216
|
-
...(options.port !== undefined && { port: options.port }),
|
|
217
|
-
...(retries > 0 && { retries }),
|
|
218
|
-
...(browsers && { browsers }),
|
|
219
|
-
files: [...new Set(tests.map((t) => t.file))],
|
|
220
|
-
testNames: tests.map((t) => t.name),
|
|
221
|
-
reportPath: false,
|
|
222
|
-
...(outputDir !== undefined && { outputDir }),
|
|
223
|
-
signal,
|
|
224
|
-
onEvent,
|
|
225
|
-
});
|
|
226
|
-
if (signal.aborted)
|
|
227
|
-
return "cancelled";
|
|
228
|
-
return summary.status === "success" ? "passed" : "failed";
|
|
229
|
-
},
|
|
230
226
|
});
|
|
231
227
|
}
|
|
232
228
|
finally {
|
|
@@ -235,9 +231,10 @@ async function walkFlowInProject(options) {
|
|
|
235
231
|
}
|
|
236
232
|
/**
|
|
237
233
|
* Checks and runs a flow of a project: loads its config, lists its tests and groups, refuses to start
|
|
238
|
-
* when `checkFlow` finds errors (`FlowCheckError`), then walks the flow
|
|
239
|
-
*
|
|
240
|
-
*
|
|
234
|
+
* when `checkFlow` finds errors (`FlowCheckError`), then walks the flow once per browser (see `flowBrowsers`),
|
|
235
|
+
* one browser after the other, each run with its own variables and results. Each Test and Group node runs its
|
|
236
|
+
* tests through `runTests`, one after the other, or on one shared worker pool when `workers` is above 1.
|
|
237
|
+
* Resolves to every run's summary; the last event is `flow-done`. One at a time per process.
|
|
241
238
|
*/
|
|
242
239
|
export async function runFlowInProject(options) {
|
|
243
240
|
// One log for the whole run, across the project sessions it opens one after the other
|
|
@@ -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":
|