@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
package/dist/steps/edit.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { stepToSource } from "./emit.js";
|
|
3
3
|
import { parseSpec } from "./parse.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return newline === -1 ? source.length : newline + 1;
|
|
8
|
-
}
|
|
9
|
-
const indentOf = (source, offset) => /^[ \t]*/.exec(source.slice(lineStart(source, offset)))[0];
|
|
4
|
+
import { OUTPUT_MATCHERS } from "./parse/expectation.js";
|
|
5
|
+
import { declareList } from "./test-options.js";
|
|
6
|
+
import { indentOf, lineEnd, lineStart, splice } from "./text.js";
|
|
10
7
|
function pickTest(source, test) {
|
|
11
8
|
const found = parseSpec(source)[test];
|
|
12
9
|
if (!found)
|
|
@@ -19,9 +16,6 @@ function pickStep(parsed, index) {
|
|
|
19
16
|
throw new RangeError(`Test "${parsed.name}" has no step ${index}`);
|
|
20
17
|
return step;
|
|
21
18
|
}
|
|
22
|
-
function splice(source, from, to, text) {
|
|
23
|
-
return source.slice(0, from) + text + source.slice(to);
|
|
24
|
-
}
|
|
25
19
|
/** Inserts statement text so it becomes step `index`, copying the indentation of the steps around it */
|
|
26
20
|
function insertCode(source, parsed, index, code) {
|
|
27
21
|
if (index < 0 || index > parsed.steps.length) {
|
|
@@ -52,11 +46,58 @@ function removeStep(source, parsed, index) {
|
|
|
52
46
|
const { anchor, end } = pickStep(parsed, index);
|
|
53
47
|
return splice(source, lineStart(source, anchor), lineEnd(source, end), "");
|
|
54
48
|
}
|
|
49
|
+
const VALUE_FIXTURES = { env: "env", secret: "secrets", input: "inputs" };
|
|
55
50
|
/** The fixtures a step's statement reads */
|
|
56
51
|
function fixturesOf(step) {
|
|
52
|
+
if (step.kind === "saveOutput")
|
|
53
|
+
return ["outputs"];
|
|
57
54
|
if (step.kind !== "fill" || step.value.kind === "literal")
|
|
58
55
|
return [];
|
|
59
|
-
return [step.value.kind
|
|
56
|
+
return [VALUE_FIXTURES[step.value.kind]];
|
|
57
|
+
}
|
|
58
|
+
/** The output a step saves, if any */
|
|
59
|
+
function outputOf(step) {
|
|
60
|
+
if (step.kind === "saveOutput")
|
|
61
|
+
return step.name;
|
|
62
|
+
return step.kind === "fill" || step.kind === "expect"
|
|
63
|
+
? step.output
|
|
64
|
+
: undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Refuses a step that reads an input the test doesn't declare: the default has to be set first */
|
|
67
|
+
function checkInputs(parsed, step) {
|
|
68
|
+
if (step.kind !== "fill" || step.value.kind !== "input")
|
|
69
|
+
return;
|
|
70
|
+
const { name } = step.value;
|
|
71
|
+
if (!parsed.inputs?.some((input) => input.name === name))
|
|
72
|
+
throw new Error(`applyEdit(): input "${name}" is not declared`);
|
|
73
|
+
}
|
|
74
|
+
/** Refuses an output the runtime can't save: from a negated check, or from a matcher that doesn't take one */
|
|
75
|
+
function checkOutput(step) {
|
|
76
|
+
if (step.kind !== "expect" || step.output === undefined)
|
|
77
|
+
return;
|
|
78
|
+
if (step.not)
|
|
79
|
+
throw new Error("applyEdit(): output can't be saved from a negated check");
|
|
80
|
+
if (!OUTPUT_MATCHERS.includes(step.expectation.matcher))
|
|
81
|
+
throw new Error(`applyEdit(): output can't be saved from ${step.expectation.matcher}`);
|
|
82
|
+
}
|
|
83
|
+
/** Adds the output a step saves to the test's `outputs`, when it isn't there yet */
|
|
84
|
+
function withOutput(source, parsed, step) {
|
|
85
|
+
const name = outputOf(step);
|
|
86
|
+
const outputs = parsed.outputs ?? [];
|
|
87
|
+
if (name === undefined || outputs.includes(name))
|
|
88
|
+
return source;
|
|
89
|
+
return declareList(source, parsed, "outputs", [...outputs, name]);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Puts a step's statement in at `[from, to)` of the body, then what it needs: its fixtures, its output and
|
|
93
|
+
* the `generatedFile` import. Each goes before the last in the file, so the parsed offsets stay valid
|
|
94
|
+
*/
|
|
95
|
+
function withStep(source, parsed, step, place) {
|
|
96
|
+
checkInputs(parsed, step);
|
|
97
|
+
checkOutput(step);
|
|
98
|
+
const next = withOutput(withFixtures(place(source, stepToSource(step)), parsed, step), parsed, step);
|
|
99
|
+
// Last: the import shifts every offset
|
|
100
|
+
return usesGeneratedFile(step) ? withGeneratedFileImport(next) : next;
|
|
60
101
|
}
|
|
61
102
|
/** Adds the fixtures the step reads to the test's parameters, when they aren't there yet */
|
|
62
103
|
function withFixtures(source, parsed, step) {
|
|
@@ -102,26 +143,27 @@ function withGeneratedFileImport(source) {
|
|
|
102
143
|
}
|
|
103
144
|
/**
|
|
104
145
|
* Applies one edit and returns the new source. Only the touched statement changes, plus the test's parameters
|
|
105
|
-
* when the step reads a fixture (`env`, `secrets`) they don't have yet
|
|
106
|
-
* comments included, stays byte for
|
|
146
|
+
* when the step reads a fixture (`env`, `secrets`, `inputs`, `outputs`) they don't have yet, and its `outputs`
|
|
147
|
+
* when the step saves one not declared yet: everything else, formatting and comments included, stays byte for
|
|
148
|
+
* byte. A step that reads an undeclared input is refused: `declare` it, with its default, first.
|
|
107
149
|
*/
|
|
108
150
|
export function applyEdit(source, edit) {
|
|
109
151
|
const parsed = pickTest(source, edit.test);
|
|
110
152
|
switch (edit.op) {
|
|
111
153
|
case "replace": {
|
|
112
154
|
const { start, end } = pickStep(parsed, edit.index);
|
|
113
|
-
|
|
114
|
-
const next = withFixtures(splice(source, start, end, stepToSource(edit.step)), parsed, edit.step);
|
|
115
|
-
// Last: the import shifts every offset
|
|
116
|
-
return usesGeneratedFile(edit.step)
|
|
117
|
-
? withGeneratedFileImport(next)
|
|
118
|
-
: next;
|
|
155
|
+
return withStep(source, parsed, edit.step, (text, code) => splice(text, start, end, code));
|
|
119
156
|
}
|
|
120
|
-
case "insert":
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
case "insert":
|
|
158
|
+
return withStep(source, parsed, edit.step, (text, code) => insertCode(text, parsed, edit.index, code));
|
|
159
|
+
case "declare": {
|
|
160
|
+
let next = source;
|
|
161
|
+
for (const key of ["inputs", "outputs"]) {
|
|
162
|
+
const list = edit[key];
|
|
163
|
+
if (list)
|
|
164
|
+
next = declareList(next, pickTest(next, edit.test), key, list);
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
125
167
|
}
|
|
126
168
|
case "remove":
|
|
127
169
|
return removeStep(source, parsed, edit.index);
|
package/dist/steps/emit.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { FileEntry, LocatorCall, LocatorSpec, Step, TextValue } from "./model.js";
|
|
2
2
|
/** The argument of `setInputFiles` / `dropFiles`: one entry bare, else an array */
|
|
3
3
|
export declare function filesToSource(files: readonly FileEntry[]): string;
|
|
4
|
+
/** A name that can follow a `.` or stand as an object key unquoted */
|
|
5
|
+
export declare const IDENTIFIER: RegExp;
|
|
4
6
|
export declare function callToSource(call: LocatorCall): string;
|
|
5
7
|
export declare function locatorToSource({ chain, fallbacks }: LocatorSpec): string;
|
|
6
8
|
export declare function valueToSource(value: TextValue): string;
|
package/dist/steps/emit.js
CHANGED
|
@@ -15,7 +15,8 @@ export function filesToSource(files) {
|
|
|
15
15
|
? fileEntryToSource(files[0])
|
|
16
16
|
: `[${files.map(fileEntryToSource).join(", ")}]`;
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
/** A name that can follow a `.` or stand as an object key unquoted */
|
|
19
|
+
export const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
19
20
|
function textOptions(call) {
|
|
20
21
|
const parts = [
|
|
21
22
|
call.match !== undefined && `match: ${str(call.match)}`,
|
|
@@ -48,17 +49,38 @@ export function locatorToSource({ chain, fallbacks }) {
|
|
|
48
49
|
export function valueToSource(value) {
|
|
49
50
|
if (value.kind === "literal")
|
|
50
51
|
return str(value.value);
|
|
51
|
-
const object = value.kind === "env"
|
|
52
|
+
const object = value.kind === "env"
|
|
53
|
+
? "env"
|
|
54
|
+
: value.kind === "secret"
|
|
55
|
+
? "secrets"
|
|
56
|
+
: "inputs";
|
|
52
57
|
return IDENTIFIER.test(value.name)
|
|
53
58
|
? `${object}.${value.name}`
|
|
54
59
|
: `${object}[${str(value.name)}]`;
|
|
55
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* A regex literal for a pattern. The source goes through `RegExp`, which escapes a `/` and a line break and leaves
|
|
63
|
+
* text that is already escaped alone, so a pattern typed by hand still makes a literal that compiles.
|
|
64
|
+
*/
|
|
65
|
+
function regexToSource({ source, flags }) {
|
|
66
|
+
let escaped = source;
|
|
67
|
+
try {
|
|
68
|
+
escaped = new RegExp(source, flags).source;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Not a valid pattern: written unchanged
|
|
72
|
+
}
|
|
73
|
+
return `/${escaped}/${flags}`;
|
|
74
|
+
}
|
|
56
75
|
function matcherToSource(expected) {
|
|
57
76
|
return typeof expected === "string"
|
|
58
77
|
? str(expected)
|
|
59
|
-
:
|
|
78
|
+
: regexToSource(expected.regex);
|
|
60
79
|
}
|
|
61
|
-
|
|
80
|
+
/** The `{ output }` option a fill or a check ends with, as its last argument */
|
|
81
|
+
const outputOption = (output) => output === undefined ? "" : `, { output: ${str(output)} }`;
|
|
82
|
+
function expectationToSource(expectation, output) {
|
|
83
|
+
const tail = outputOption(output);
|
|
62
84
|
switch (expectation.matcher) {
|
|
63
85
|
case "toBeVisible":
|
|
64
86
|
case "toBeHidden":
|
|
@@ -66,13 +88,13 @@ function expectationToSource(expectation) {
|
|
|
66
88
|
case "toBeChecked":
|
|
67
89
|
return `${expectation.matcher}()`;
|
|
68
90
|
case "toHaveAttribute":
|
|
69
|
-
return `toHaveAttribute(${str(expectation.name)}, ${matcherToSource(expectation.expected)})`;
|
|
91
|
+
return `toHaveAttribute(${str(expectation.name)}, ${matcherToSource(expectation.expected)}${tail})`;
|
|
70
92
|
case "toHaveCSS":
|
|
71
93
|
return `toHaveCSS(${str(expectation.name)}, ${matcherToSource(expectation.expected)})`;
|
|
72
94
|
case "toHaveCount":
|
|
73
95
|
return `toHaveCount(${expectation.expected})`;
|
|
74
96
|
default:
|
|
75
|
-
return `${expectation.matcher}(${matcherToSource(expectation.expected)})`;
|
|
97
|
+
return `${expectation.matcher}(${matcherToSource(expectation.expected)}${tail})`;
|
|
76
98
|
}
|
|
77
99
|
}
|
|
78
100
|
/** The statement for a step, on one line */
|
|
@@ -83,11 +105,11 @@ export function stepToSource(step) {
|
|
|
83
105
|
case "click":
|
|
84
106
|
return `await ${locatorToSource(step.locator)}.click();`;
|
|
85
107
|
case "fill":
|
|
86
|
-
return `await ${locatorToSource(step.locator)}.fill(${valueToSource(step.value)});`;
|
|
108
|
+
return `await ${locatorToSource(step.locator)}.fill(${valueToSource(step.value)}${outputOption(step.output)});`;
|
|
87
109
|
case "press":
|
|
88
110
|
return `await ${locatorToSource(step.locator)}.press(${str(step.key)});`;
|
|
89
111
|
case "expect":
|
|
90
|
-
return `await expect(${locatorToSource(step.locator)})${step.not ? ".not" : ""}.${expectationToSource(step.expectation)};`;
|
|
112
|
+
return `await expect(${locatorToSource(step.locator)})${step.not ? ".not" : ""}.${expectationToSource(step.expectation, step.output)};`;
|
|
91
113
|
case "expectPage":
|
|
92
114
|
return `await expect(page)${step.not ? ".not" : ""}.${expectationToSource(step.expectation)};`;
|
|
93
115
|
case "waitForNetworkIdle":
|
|
@@ -99,6 +121,8 @@ export function stepToSource(step) {
|
|
|
99
121
|
case "setInputFiles":
|
|
100
122
|
case "dropFiles":
|
|
101
123
|
return `await ${locatorToSource(step.locator)}.${step.kind}(${filesToSource(step.files)});`;
|
|
124
|
+
case "saveOutput":
|
|
125
|
+
return `await outputs.save(${str(step.name)}, ${locatorToSource(step.locator)}${step.pattern ? `, ${regexToSource(step.pattern)}` : ""});`;
|
|
102
126
|
case "custom":
|
|
103
127
|
return step.code;
|
|
104
128
|
}
|
package/dist/steps/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { applyEdit, type StepEdit } from "./edit.js";
|
|
|
3
3
|
export { stepToSource, locatorToSource, callToSource, filesToSource, valueToSource, } from "./emit.js";
|
|
4
4
|
export { formatSize, humanSize, parseSize, sizeFromSource } from "./files.js";
|
|
5
5
|
export { locatorFromSpec } from "./build.js";
|
|
6
|
-
export type { Expectation, FileEntry, GeneratedFileSpec, LocatorCall, LocatorSpec, PageExpectation, ParsedStep, ParsedTest, Step, TextMatcher, TextValue, UnparsedTest, } from "./model.js";
|
|
6
|
+
export type { Expectation, FileEntry, GeneratedFileSpec, LocatorCall, LocatorSpec, PageExpectation, ParsedStep, ParsedTest, Step, TextMatcher, TestInput, TextValue, UnparsedTest, } from "./model.js";
|
package/dist/steps/model.d.ts
CHANGED
|
@@ -39,6 +39,11 @@ export type TextValue = {
|
|
|
39
39
|
} | {
|
|
40
40
|
kind: "secret";
|
|
41
41
|
name: string;
|
|
42
|
+
}
|
|
43
|
+
/** One of the test's declared `inputs`, `inputs.name` */
|
|
44
|
+
| {
|
|
45
|
+
kind: "input";
|
|
46
|
+
name: string;
|
|
42
47
|
};
|
|
43
48
|
/** What an assertion expects of text: a string or a regular expression */
|
|
44
49
|
export type TextMatcher = string | {
|
|
@@ -95,15 +100,33 @@ export type Step = {
|
|
|
95
100
|
} | {
|
|
96
101
|
kind: "click";
|
|
97
102
|
locator: LocatorSpec;
|
|
98
|
-
}
|
|
103
|
+
}
|
|
104
|
+
/** `output` saves the typed value as that output, `{ output: "name" }` */
|
|
105
|
+
| {
|
|
99
106
|
kind: "fill";
|
|
100
107
|
locator: LocatorSpec;
|
|
101
108
|
value: TextValue;
|
|
109
|
+
output?: string;
|
|
102
110
|
} | {
|
|
103
111
|
kind: "expect";
|
|
104
112
|
locator: LocatorSpec;
|
|
105
113
|
not: boolean;
|
|
106
114
|
expectation: Expectation;
|
|
115
|
+
/**
|
|
116
|
+
* Saves what passed as that output. Read only with `toHaveText`, `toContainText`, `toHaveValue` and
|
|
117
|
+
* `toHaveAttribute`, never with `not`: any other shape is `custom`
|
|
118
|
+
*/
|
|
119
|
+
output?: string;
|
|
120
|
+
}
|
|
121
|
+
/** Saves an element's text as an output, `outputs.save(name, locator, /pattern/)`: the first group, else the whole match */
|
|
122
|
+
| {
|
|
123
|
+
kind: "saveOutput";
|
|
124
|
+
name: string;
|
|
125
|
+
locator: LocatorSpec;
|
|
126
|
+
pattern?: {
|
|
127
|
+
source: string;
|
|
128
|
+
flags: string;
|
|
129
|
+
};
|
|
107
130
|
}
|
|
108
131
|
/** An assertion on the page, `expect(page).toHaveURL(…)` */
|
|
109
132
|
| {
|
|
@@ -187,6 +210,22 @@ export interface ParsedTest {
|
|
|
187
210
|
modifier?: Modifier;
|
|
188
211
|
/** The `{ reason }` written with the `skip` or `fixme` that applies, when it is a string literal */
|
|
189
212
|
reason?: string;
|
|
213
|
+
/** The `inputs` of the test's options object, in source order; undefined without the key */
|
|
214
|
+
inputs?: TestInput[];
|
|
215
|
+
/** The `outputs` of the test's options object; undefined without the key */
|
|
216
|
+
outputs?: string[];
|
|
217
|
+
/** The range of the test's options object (`test(title, { … }, fn)`), when it has one */
|
|
218
|
+
options?: {
|
|
219
|
+
start: number;
|
|
220
|
+
end: number;
|
|
221
|
+
};
|
|
222
|
+
/** Offset just after the title argument, where an options object goes */
|
|
223
|
+
titleEnd: number;
|
|
224
|
+
}
|
|
225
|
+
/** A declared input and the value it gets when nothing supplies one */
|
|
226
|
+
export interface TestInput {
|
|
227
|
+
name: string;
|
|
228
|
+
default: string;
|
|
190
229
|
}
|
|
191
230
|
/** A `test` or `describe` call the codec can't read: its title isn't a plain string, or its function isn't written inline */
|
|
192
231
|
export interface UnparsedTest {
|
|
@@ -194,5 +233,6 @@ export interface UnparsedTest {
|
|
|
194
233
|
/** The statement's range, as for `ParsedTest` */
|
|
195
234
|
start: number;
|
|
196
235
|
end: number;
|
|
197
|
-
|
|
236
|
+
/** `dynamic-options`: the options object isn't a literal the codec can read (`{ inputs: defaults }`, a variable) */
|
|
237
|
+
reason: "dynamic-title" | "not-inline" | "dynamic-options";
|
|
198
238
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import type { Expectation, PageExpectation, TextValue } from "../model.js";
|
|
3
|
-
/** `env.NAME`, `
|
|
3
|
+
/** `env.NAME`, `secrets.NAME`, `inputs.NAME` (or `env["NAME"]` and so on) or a string literal */
|
|
4
4
|
export declare function readValue(node: ts.Expression | undefined): TextValue | undefined;
|
|
5
|
+
/** `/source/flags` */
|
|
6
|
+
export declare function readRegex(node: ts.Expression | undefined): {
|
|
7
|
+
source: string;
|
|
8
|
+
flags: string;
|
|
9
|
+
} | undefined;
|
|
10
|
+
/** The matchers that take `{ output }` as their last argument */
|
|
11
|
+
export declare const OUTPUT_MATCHERS: readonly string[];
|
|
5
12
|
export declare function readExpectation(matcher: string, args: readonly ts.Expression[]): Expectation | undefined;
|
|
6
13
|
export declare function readPageExpectation(matcher: string, args: readonly ts.Expression[]): PageExpectation | undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { stringOf } from "./literals.js";
|
|
3
3
|
const TEXT_MATCHERS = ["toHaveText", "toContainText", "toHaveValue"];
|
|
4
|
-
/** `env.NAME`, `
|
|
4
|
+
/** `env.NAME`, `secrets.NAME`, `inputs.NAME` (or `env["NAME"]` and so on) or a string literal */
|
|
5
5
|
export function readValue(node) {
|
|
6
6
|
if (!node)
|
|
7
7
|
return undefined;
|
|
@@ -26,25 +26,31 @@ export function readValue(node) {
|
|
|
26
26
|
return { kind: "env", name };
|
|
27
27
|
if (object.text === "secrets")
|
|
28
28
|
return { kind: "secret", name };
|
|
29
|
+
if (object.text === "inputs")
|
|
30
|
+
return { kind: "input", name };
|
|
29
31
|
return undefined;
|
|
30
32
|
}
|
|
33
|
+
/** `/source/flags` */
|
|
34
|
+
export function readRegex(node) {
|
|
35
|
+
if (!node || !ts.isRegularExpressionLiteral(node))
|
|
36
|
+
return undefined;
|
|
37
|
+
const end = node.text.lastIndexOf("/");
|
|
38
|
+
return { source: node.text.slice(1, end), flags: node.text.slice(end + 1) };
|
|
39
|
+
}
|
|
31
40
|
function readTextMatcher(node) {
|
|
32
41
|
if (!node)
|
|
33
42
|
return undefined;
|
|
34
43
|
const literal = stringOf(node);
|
|
35
44
|
if (literal !== undefined)
|
|
36
45
|
return literal;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
regex: {
|
|
41
|
-
source: node.text.slice(1, end),
|
|
42
|
-
flags: node.text.slice(end + 1),
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
return undefined;
|
|
46
|
+
const regex = readRegex(node);
|
|
47
|
+
return regex && { regex };
|
|
47
48
|
}
|
|
49
|
+
/** The matchers that take `{ output }` as their last argument */
|
|
50
|
+
export const OUTPUT_MATCHERS = [
|
|
51
|
+
...TEXT_MATCHERS,
|
|
52
|
+
"toHaveAttribute",
|
|
53
|
+
];
|
|
48
54
|
export function readExpectation(matcher, args) {
|
|
49
55
|
if (matcher === "toBeVisible" ||
|
|
50
56
|
matcher === "toBeHidden" ||
|
package/dist/steps/parse/step.js
CHANGED
|
@@ -1,8 +1,58 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { readLocator } from "./call.js";
|
|
3
|
-
import { readExpectation, readPageExpectation, readValue, } from "./expectation.js";
|
|
3
|
+
import { OUTPUT_MATCHERS, readExpectation, readPageExpectation, readRegex, readValue, } from "./expectation.js";
|
|
4
4
|
import { readFiles } from "./file-entry.js";
|
|
5
5
|
import { readObject, stringOf } from "./literals.js";
|
|
6
|
+
/**
|
|
7
|
+
* The name in a trailing `{ output: "name" }`: undefined without one, null when the argument is there but
|
|
8
|
+
* isn't exactly that
|
|
9
|
+
*/
|
|
10
|
+
function outputOf(node) {
|
|
11
|
+
if (!node)
|
|
12
|
+
return undefined;
|
|
13
|
+
const output = readObject(node, ["output"])?.output;
|
|
14
|
+
return typeof output === "string" ? output : null;
|
|
15
|
+
}
|
|
16
|
+
/** `outputs.save("name", locator)` or `outputs.save("name", locator, /pattern/)` */
|
|
17
|
+
function readSaveOutput(args) {
|
|
18
|
+
const name = stringOf(args[0]);
|
|
19
|
+
const locator = args[1] && readLocator(args[1]);
|
|
20
|
+
const pattern = readRegex(args[2]);
|
|
21
|
+
if (name === undefined ||
|
|
22
|
+
!locator ||
|
|
23
|
+
args.length > 3 ||
|
|
24
|
+
(args.length === 3 && !pattern))
|
|
25
|
+
return undefined;
|
|
26
|
+
return {
|
|
27
|
+
kind: "saveOutput",
|
|
28
|
+
name,
|
|
29
|
+
locator,
|
|
30
|
+
...(pattern && { pattern }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** `expect(locator)…matcher(…)`, with a trailing `{ output }` only on the matchers that take one and never with `not` */
|
|
34
|
+
function readLocatorExpect(subject, negated, matcher, args) {
|
|
35
|
+
const locator = readLocator(subject);
|
|
36
|
+
if (!locator)
|
|
37
|
+
return undefined;
|
|
38
|
+
const last = args[args.length - 1];
|
|
39
|
+
const takesOutput = OUTPUT_MATCHERS.includes(matcher) &&
|
|
40
|
+
!!last &&
|
|
41
|
+
ts.isObjectLiteralExpression(last);
|
|
42
|
+
const output = takesOutput ? outputOf(last) : undefined;
|
|
43
|
+
if (output === null || (output !== undefined && negated))
|
|
44
|
+
return undefined;
|
|
45
|
+
const expectation = readExpectation(matcher, takesOutput ? args.slice(0, -1) : args);
|
|
46
|
+
return expectation
|
|
47
|
+
? {
|
|
48
|
+
kind: "expect",
|
|
49
|
+
locator,
|
|
50
|
+
not: negated,
|
|
51
|
+
expectation,
|
|
52
|
+
...(output !== undefined && { output }),
|
|
53
|
+
}
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
6
56
|
/** The step a statement stands for, or undefined when it is outside the recorder's set */
|
|
7
57
|
export function readStep(statement) {
|
|
8
58
|
if (!ts.isExpressionStatement(statement) ||
|
|
@@ -58,12 +108,10 @@ export function readStep(statement) {
|
|
|
58
108
|
? { kind: "expectPage", not: negated, expectation }
|
|
59
109
|
: undefined;
|
|
60
110
|
}
|
|
61
|
-
|
|
62
|
-
const expectation = readExpectation(name.text, args);
|
|
63
|
-
return locator && expectation
|
|
64
|
-
? { kind: "expect", locator, not: negated, expectation }
|
|
65
|
-
: undefined;
|
|
111
|
+
return readLocatorExpect(arg, negated, name.text, args);
|
|
66
112
|
}
|
|
113
|
+
if (ts.isIdentifier(target) && target.text === "outputs")
|
|
114
|
+
return name.text === "save" ? readSaveOutput(args) : undefined;
|
|
67
115
|
const locator = readLocator(target);
|
|
68
116
|
if (!locator)
|
|
69
117
|
return undefined;
|
|
@@ -73,9 +121,17 @@ export function readStep(statement) {
|
|
|
73
121
|
const key = stringOf(args[0]);
|
|
74
122
|
return key === undefined ? undefined : { kind: "press", locator, key };
|
|
75
123
|
}
|
|
76
|
-
if (name.text === "fill" && args.length === 1) {
|
|
124
|
+
if (name.text === "fill" && (args.length === 1 || args.length === 2)) {
|
|
77
125
|
const value = readValue(args[0]);
|
|
78
|
-
|
|
126
|
+
const output = outputOf(args[1]);
|
|
127
|
+
return value && output !== null
|
|
128
|
+
? {
|
|
129
|
+
kind: "fill",
|
|
130
|
+
locator,
|
|
131
|
+
value,
|
|
132
|
+
...(output !== undefined && { output }),
|
|
133
|
+
}
|
|
134
|
+
: undefined;
|
|
79
135
|
}
|
|
80
136
|
if ((name.text === "setInputFiles" || name.text === "dropFiles") &&
|
|
81
137
|
args.length === 1) {
|
package/dist/steps/parse.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import {
|
|
2
|
+
import { stringOf } from "./parse/literals.js";
|
|
3
3
|
import { readStep } from "./parse/step.js";
|
|
4
|
+
import { readTestOptions } from "./test-options.js";
|
|
4
5
|
function parseStatement(source, statement, sourceFile) {
|
|
5
6
|
const start = statement.getStart(sourceFile);
|
|
6
7
|
const end = statement.end;
|
|
@@ -58,21 +59,19 @@ function calleeOf(expression) {
|
|
|
58
59
|
return undefined;
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
61
|
-
* The callback of `test(title, fn)` and `describe(title, fn)`, and
|
|
62
|
-
*
|
|
62
|
+
* The callback of `test(title, fn)` and `describe(title, fn)`, and of `(title, options, fn)` with what the
|
|
63
|
+
* options say and where they sit. Undefined when the options object isn't one the codec can read
|
|
63
64
|
*/
|
|
64
|
-
function
|
|
65
|
+
function argumentsOf(node, kind, modifier) {
|
|
65
66
|
const [, second, third] = node.arguments;
|
|
66
|
-
if (
|
|
67
|
-
second
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
return { callback: second };
|
|
67
|
+
if (node.arguments.length < 3)
|
|
68
|
+
return { callback: second };
|
|
69
|
+
const options = readTestOptions(second, kind, modifier);
|
|
70
|
+
return (options && {
|
|
71
|
+
callback: third,
|
|
72
|
+
options,
|
|
73
|
+
range: { start: second.getStart(), end: second.end },
|
|
74
|
+
});
|
|
76
75
|
}
|
|
77
76
|
/** What the runner applies to a test (see `modifierOf` in the runner): the innermost `skip` or `fixme` with its reason, else `only` */
|
|
78
77
|
function effectiveMark(own, describes) {
|
|
@@ -128,12 +127,18 @@ export function parseSpecFile(source) {
|
|
|
128
127
|
!(callee.name === "test" && isConditionForm(node, callee))) {
|
|
129
128
|
const kind = callee.name;
|
|
130
129
|
const title = stringOf(node.arguments[0]);
|
|
131
|
-
const { callback, reason } = callbackOf(node, callee.modifier);
|
|
132
|
-
const body = callbackBody(callback);
|
|
133
130
|
if (title === undefined) {
|
|
134
131
|
unparsed.push(unreadable(node, kind, "dynamic-title"));
|
|
135
132
|
return;
|
|
136
133
|
}
|
|
134
|
+
const args = argumentsOf(node, kind, callee.modifier);
|
|
135
|
+
if (!args) {
|
|
136
|
+
unparsed.push(unreadable(node, kind, "dynamic-options"));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const { callback, options = {}, range } = args;
|
|
140
|
+
const { reason, inputs, outputs } = options;
|
|
141
|
+
const body = callbackBody(callback);
|
|
137
142
|
if (!body) {
|
|
138
143
|
unparsed.push(unreadable(node, kind, "not-inline"));
|
|
139
144
|
return;
|
|
@@ -154,6 +159,10 @@ export function parseSpecFile(source) {
|
|
|
154
159
|
removable: isRemovable(node),
|
|
155
160
|
fixtures: callbackFixtures(callback, sourceFile),
|
|
156
161
|
...effectiveMark(mark, describes),
|
|
162
|
+
...(inputs && { inputs }),
|
|
163
|
+
...(outputs && { outputs }),
|
|
164
|
+
...(range && { options: range }),
|
|
165
|
+
titleEnd: node.arguments[0].end,
|
|
157
166
|
});
|
|
158
167
|
return;
|
|
159
168
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { Modifier } from "../types/test.js";
|
|
3
|
+
import type { ParsedTest, TestInput } from "./model.js";
|
|
4
|
+
/** What a `test()` or `describe()` options object says, as far as the codec reads it */
|
|
5
|
+
export interface TestOptionsRead {
|
|
6
|
+
reason?: string;
|
|
7
|
+
inputs?: TestInput[];
|
|
8
|
+
outputs?: string[];
|
|
9
|
+
}
|
|
10
|
+
type ListKey = "inputs" | "outputs";
|
|
11
|
+
/**
|
|
12
|
+
* The options object of a test or describe call: a describe takes only `reason`, a test also `inputs` and
|
|
13
|
+
* `outputs`. Undefined when the object isn't a literal the codec can read, so the call is left unparsed. An
|
|
14
|
+
* empty list is left out, as the runtime leaves it out. A `reason` counts only for skip and fixme, and one that
|
|
15
|
+
* isn't a string literal is passed over, as before
|
|
16
|
+
*/
|
|
17
|
+
export declare function readTestOptions(node: ts.Expression, kind: "test" | "describe", modifier: Modifier | undefined): TestOptionsRead | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Sets one list of the test's options object: replaces it, adds it (making the object when there is none) or,
|
|
20
|
+
* for an empty list, removes it, and the whole object when nothing but empty lists would be left. The rest of
|
|
21
|
+
* the object, its layout and a `reason`, stays as it is; an unchanged list leaves the source as it is. Throws for
|
|
22
|
+
* names the runtime rejects, before writing
|
|
23
|
+
*/
|
|
24
|
+
export declare function declareList(source: string, parsed: ParsedTest, key: ListKey, list: TestInput[] | string[]): string;
|
|
25
|
+
export {};
|