@itmetsam/samurai-framework 0.22.0 → 0.22.2
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 +3 -3
- package/dist/browser/page-content.d.ts +4 -0
- package/dist/browser/page-content.js +12 -0
- package/dist/browser/page-events.d.ts +11 -0
- package/dist/browser/page-events.js +23 -0
- package/dist/browser/page-logging.d.ts +24 -0
- package/dist/browser/page-logging.js +43 -0
- package/dist/browser/page-settings.d.ts +1 -1
- package/dist/browser/page-viewport.d.ts +21 -0
- package/dist/browser/page-viewport.js +33 -0
- package/dist/browser/page.d.ts +6 -9
- package/dist/browser/page.js +31 -63
- package/dist/browser/screenshot.d.ts +6 -0
- package/dist/browser/screenshot.js +14 -0
- package/dist/capabilities.d.ts +16 -2
- package/dist/capabilities.js +16 -2
- package/dist/config/config.js +2 -1
- package/dist/config/fixture-use.d.ts +19 -0
- package/dist/config/fixture-use.js +44 -0
- package/dist/config/group-use.d.ts +4 -2
- package/dist/config/group-use.js +30 -20
- package/dist/config/use-options.js +5 -1
- package/dist/recorder/capture.d.ts +3 -0
- package/dist/recorder/capture.js +18 -1
- package/dist/recorder/normalise.d.ts +12 -1
- package/dist/recorder/normalise.js +29 -1
- package/dist/recorder/page-messages.d.ts +6 -2
- package/dist/recorder/page-messages.js +29 -21
- package/dist/recorder/toolbar/inspect.js +10 -3
- package/dist/runner/fixture-defs.d.ts +2 -1
- package/dist/runner/fixture-defs.js +51 -18
- package/dist/runner/fixture-meta.d.ts +20 -0
- package/dist/runner/fixture-meta.js +109 -0
- package/dist/runner/fixture-options.d.ts +8 -0
- package/dist/runner/fixture-options.js +35 -0
- package/dist/runner/fixture-outputs.d.ts +11 -0
- package/dist/runner/fixture-outputs.js +28 -0
- package/dist/runner/fixture-registry.d.ts +35 -0
- package/dist/runner/fixture-registry.js +73 -0
- package/dist/runner/fixture-scope.d.ts +39 -9
- package/dist/runner/fixture-scope.js +139 -70
- package/dist/runner/fixture-start.d.ts +27 -0
- package/dist/runner/fixture-start.js +76 -0
- package/dist/runner/fixture-use.d.ts +23 -0
- package/dist/runner/fixture-use.js +64 -0
- package/dist/runner/fixture-worker.d.ts +26 -0
- package/dist/runner/fixture-worker.js +46 -0
- package/dist/runner/prepare-run.js +5 -0
- package/dist/runner/registry.js +15 -5
- package/dist/runner/reporter.d.ts +3 -1
- package/dist/runner/reporter.js +1 -0
- package/dist/runner/run-test.d.ts +3 -0
- package/dist/runner/run-test.js +16 -1
- package/dist/runner/run.d.ts +7 -0
- package/dist/runner/run.js +9 -0
- package/dist/runner/test-function.d.ts +2 -2
- package/dist/runner/test-runner.js +6 -1
- package/dist/runner/use-options.d.ts +16 -2
- package/dist/runner/use-options.js +28 -8
- package/dist/runner/worker-session.js +6 -0
- package/dist/steps/build.js +49 -25
- package/dist/steps/emit.js +64 -7
- package/dist/steps/index.d.ts +1 -1
- package/dist/steps/model.d.ts +60 -10
- package/dist/steps/parse/call.js +125 -39
- package/dist/steps/parse/expectation.d.ts +2 -1
- package/dist/steps/parse/expectation.js +30 -8
- package/dist/steps/parse/literals.d.ts +9 -0
- package/dist/steps/parse/literals.js +42 -0
- package/dist/steps/parse/step.js +28 -1
- package/dist/types/config.d.ts +35 -22
- package/dist/types/extend.d.ts +54 -2
- package/dist/types/test.d.ts +24 -0
- package/dist/types/use.d.ts +14 -0
- package/package.json +1 -1
package/dist/steps/emit.js
CHANGED
|
@@ -17,13 +17,29 @@ export function filesToSource(files) {
|
|
|
17
17
|
}
|
|
18
18
|
/** A name that can follow a `.` or stand as an object key unquoted */
|
|
19
19
|
export const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
/** `, { a: 1, b: "x" }` from key and source pairs; a pair without source is left out, and so is an empty object */
|
|
21
|
+
function optionsToSource(pairs) {
|
|
22
|
+
const parts = pairs
|
|
23
|
+
.filter(([, source]) => source !== undefined)
|
|
24
|
+
.map(([key, source]) => `${key}: ${source}`);
|
|
25
25
|
return parts.length ? `, { ${parts.join(", ")} }` : "";
|
|
26
26
|
}
|
|
27
|
+
/** A string, number or boolean as source, or undefined when it isn't set */
|
|
28
|
+
const literal = (value) => value === undefined
|
|
29
|
+
? undefined
|
|
30
|
+
: typeof value === "string"
|
|
31
|
+
? str(value)
|
|
32
|
+
: String(value);
|
|
33
|
+
const ROLE_OPTIONS = [
|
|
34
|
+
"exact",
|
|
35
|
+
"level",
|
|
36
|
+
"checked",
|
|
37
|
+
"pressed",
|
|
38
|
+
"expanded",
|
|
39
|
+
"selected",
|
|
40
|
+
"disabled",
|
|
41
|
+
"includeHidden",
|
|
42
|
+
];
|
|
27
43
|
export function callToSource(call) {
|
|
28
44
|
switch (call.method) {
|
|
29
45
|
case "locator":
|
|
@@ -32,11 +48,32 @@ export function callToSource(call) {
|
|
|
32
48
|
return `getByCss(${str(call.css)})`;
|
|
33
49
|
case "getByText":
|
|
34
50
|
case "getByLabel":
|
|
35
|
-
return `${call.method}(${
|
|
51
|
+
return `${call.method}(${matcherToSource(call.text)}${optionsToSource([
|
|
52
|
+
["exact", literal(call.exact)],
|
|
53
|
+
["match", literal(call.match)],
|
|
54
|
+
["ignoreCase", literal(call.ignoreCase)],
|
|
55
|
+
])})`;
|
|
56
|
+
case "getByPlaceholder":
|
|
57
|
+
case "getByAltText":
|
|
58
|
+
case "getByTitle":
|
|
59
|
+
return `${call.method}(${matcherToSource(call.text)}${optionsToSource([["exact", literal(call.exact)]])})`;
|
|
36
60
|
case "getByRole":
|
|
37
|
-
return `getByRole(${str(call.role)}${
|
|
61
|
+
return `getByRole(${str(call.role)}${optionsToSource([
|
|
62
|
+
[
|
|
63
|
+
"name",
|
|
64
|
+
call.name === undefined ? undefined : matcherToSource(call.name),
|
|
65
|
+
],
|
|
66
|
+
...ROLE_OPTIONS.map((key) => [key, literal(call[key])]),
|
|
67
|
+
])})`;
|
|
38
68
|
case "getByTestId":
|
|
39
69
|
return `getByTestId(${str(call.testId)})`;
|
|
70
|
+
case "nth":
|
|
71
|
+
return `nth(${call.index})`;
|
|
72
|
+
case "first":
|
|
73
|
+
case "last":
|
|
74
|
+
return `${call.method}()`;
|
|
75
|
+
case "filter":
|
|
76
|
+
return `filter({ hasText: ${matcherToSource(call.hasText)} })`;
|
|
40
77
|
}
|
|
41
78
|
}
|
|
42
79
|
const chainToSource = (chain) => ["page", ...chain.map(callToSource)].join(".");
|
|
@@ -86,6 +123,12 @@ function expectationToSource(expectation, output) {
|
|
|
86
123
|
case "toBeHidden":
|
|
87
124
|
case "toBeEnabled":
|
|
88
125
|
case "toBeChecked":
|
|
126
|
+
case "toBeFocused":
|
|
127
|
+
case "toBeDisabled":
|
|
128
|
+
case "toBeEditable":
|
|
129
|
+
case "toBeAttached":
|
|
130
|
+
case "toBeInViewport":
|
|
131
|
+
case "toBeEmpty":
|
|
89
132
|
return `${expectation.matcher}()`;
|
|
90
133
|
case "toHaveAttribute":
|
|
91
134
|
return `toHaveAttribute(${str(expectation.name)}, ${matcherToSource(expectation.expected)}${tail})`;
|
|
@@ -97,6 +140,12 @@ function expectationToSource(expectation, output) {
|
|
|
97
140
|
return `${expectation.matcher}(${matcherToSource(expectation.expected)}${tail})`;
|
|
98
141
|
}
|
|
99
142
|
}
|
|
143
|
+
function choiceToSource(option) {
|
|
144
|
+
if (typeof option === "string")
|
|
145
|
+
return str(option);
|
|
146
|
+
const [key, value] = Object.entries(option)[0];
|
|
147
|
+
return `{ ${key}: ${typeof value === "string" ? str(value) : value} }`;
|
|
148
|
+
}
|
|
100
149
|
/** The statement for a step, on one line */
|
|
101
150
|
export function stepToSource(step) {
|
|
102
151
|
switch (step.kind) {
|
|
@@ -106,6 +155,14 @@ export function stepToSource(step) {
|
|
|
106
155
|
return `await ${locatorToSource(step.locator)}.click();`;
|
|
107
156
|
case "fill":
|
|
108
157
|
return `await ${locatorToSource(step.locator)}.fill(${valueToSource(step.value)}${outputOption(step.output)});`;
|
|
158
|
+
case "check":
|
|
159
|
+
case "uncheck":
|
|
160
|
+
case "hover":
|
|
161
|
+
case "dblclick":
|
|
162
|
+
case "clear":
|
|
163
|
+
return `await ${locatorToSource(step.locator)}.${step.kind}();`;
|
|
164
|
+
case "selectOption":
|
|
165
|
+
return `await ${locatorToSource(step.locator)}.selectOption(${choiceToSource(step.option)});`;
|
|
109
166
|
case "press":
|
|
110
167
|
return `await ${locatorToSource(step.locator)}.press(${str(step.key)});`;
|
|
111
168
|
case "expect":
|
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, TestInput, TextValue, UnparsedTest, } from "./model.js";
|
|
6
|
+
export type { Expectation, FileEntry, GeneratedFileSpec, LocatorCall, LocatorSpec, PageExpectation, SelectChoice, ParsedStep, ParsedTest, Step, TextMatcher, TestInput, TextValue, UnparsedTest, } from "./model.js";
|
package/dist/steps/model.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Modifier } from "../types/test.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* One call of a locator expression, as written in a spec: `page.getByRole("button", { name: "Save" })`.
|
|
4
|
+
* A RegExp text or name is tested as written, so it never comes with `exact`, `match` or `ignoreCase`.
|
|
5
|
+
* `nth`, `first`, `last` and `filter` narrow the calls before them, so they never start a chain
|
|
6
|
+
*/
|
|
3
7
|
export type LocatorCall = {
|
|
4
8
|
method: "locator";
|
|
5
9
|
xpath: string;
|
|
@@ -7,22 +11,46 @@ export type LocatorCall = {
|
|
|
7
11
|
method: "getByCss";
|
|
8
12
|
css: string;
|
|
9
13
|
} | {
|
|
10
|
-
method: "getByText";
|
|
11
|
-
text:
|
|
14
|
+
method: "getByText" | "getByLabel";
|
|
15
|
+
text: TextMatcher;
|
|
16
|
+
/** `false`: a case-insensitive substring. Absent: the framework's default, a full case-sensitive match */
|
|
17
|
+
exact?: boolean;
|
|
12
18
|
match?: "full" | "partial";
|
|
13
19
|
ignoreCase?: boolean;
|
|
14
20
|
} | {
|
|
15
|
-
method: "
|
|
16
|
-
text:
|
|
17
|
-
|
|
18
|
-
ignoreCase?: boolean;
|
|
21
|
+
method: "getByPlaceholder" | "getByAltText" | "getByTitle";
|
|
22
|
+
text: TextMatcher;
|
|
23
|
+
exact?: boolean;
|
|
19
24
|
} | {
|
|
20
25
|
method: "getByRole";
|
|
21
26
|
role: string;
|
|
22
|
-
name?:
|
|
27
|
+
name?: TextMatcher;
|
|
28
|
+
exact?: boolean;
|
|
29
|
+
/** The heading level */
|
|
30
|
+
level?: number;
|
|
31
|
+
checked?: boolean | "mixed";
|
|
32
|
+
pressed?: boolean | "mixed";
|
|
33
|
+
expanded?: boolean;
|
|
34
|
+
selected?: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
includeHidden?: boolean;
|
|
23
37
|
} | {
|
|
24
38
|
method: "getByTestId";
|
|
25
39
|
testId: string;
|
|
40
|
+
}
|
|
41
|
+
/** The n-th match, 0-based; a negative index counts from the end */
|
|
42
|
+
| {
|
|
43
|
+
method: "nth";
|
|
44
|
+
index: number;
|
|
45
|
+
} | {
|
|
46
|
+
method: "first";
|
|
47
|
+
} | {
|
|
48
|
+
method: "last";
|
|
49
|
+
}
|
|
50
|
+
/** Keeps the matches whose text contains the string (case-insensitive) or matches the RegExp */
|
|
51
|
+
| {
|
|
52
|
+
method: "filter";
|
|
53
|
+
hasText: TextMatcher;
|
|
26
54
|
};
|
|
27
55
|
/** A chain of locator calls on `page`, and the locators `withFallbacks` falls back to, most preferred first */
|
|
28
56
|
export interface LocatorSpec {
|
|
@@ -53,7 +81,7 @@ export type TextMatcher = string | {
|
|
|
53
81
|
};
|
|
54
82
|
};
|
|
55
83
|
export type Expectation = {
|
|
56
|
-
matcher: "toBeVisible" | "toBeHidden" | "toBeEnabled" | "toBeChecked";
|
|
84
|
+
matcher: "toBeVisible" | "toBeHidden" | "toBeEnabled" | "toBeChecked" | "toBeFocused" | "toBeDisabled" | "toBeEditable" | "toBeAttached" | "toBeInViewport" | "toBeEmpty";
|
|
57
85
|
} | {
|
|
58
86
|
matcher: "toHaveClass";
|
|
59
87
|
expected: TextMatcher;
|
|
@@ -62,7 +90,7 @@ export type Expectation = {
|
|
|
62
90
|
name: string;
|
|
63
91
|
expected: TextMatcher;
|
|
64
92
|
} | {
|
|
65
|
-
matcher: "toHaveText" | "toContainText" | "toHaveValue";
|
|
93
|
+
matcher: "toHaveText" | "toContainText" | "toHaveValue" | "toHaveId" | "toHaveAccessibleName" | "toHaveAccessibleDescription";
|
|
66
94
|
expected: TextMatcher;
|
|
67
95
|
} | {
|
|
68
96
|
matcher: "toHaveAttribute";
|
|
@@ -93,6 +121,17 @@ export type FileEntry = {
|
|
|
93
121
|
} | {
|
|
94
122
|
generate: GeneratedFileSpec;
|
|
95
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* The option `selectOption` chooses: a string (the option's value or its label), or exactly one of its `value`,
|
|
126
|
+
* `label` or position
|
|
127
|
+
*/
|
|
128
|
+
export type SelectChoice = string | {
|
|
129
|
+
value: string;
|
|
130
|
+
} | {
|
|
131
|
+
label: string;
|
|
132
|
+
} | {
|
|
133
|
+
index: number;
|
|
134
|
+
};
|
|
96
135
|
/** The restricted set of statements the recorder writes. Anything else in a test is a `custom` step */
|
|
97
136
|
export type Step = {
|
|
98
137
|
kind: "goto";
|
|
@@ -101,6 +140,17 @@ export type Step = {
|
|
|
101
140
|
kind: "click";
|
|
102
141
|
locator: LocatorSpec;
|
|
103
142
|
}
|
|
143
|
+
/** An action on an element with no argument: ticks or unticks it, moves the pointer over it, double-clicks it, empties a field */
|
|
144
|
+
| {
|
|
145
|
+
kind: "check" | "uncheck" | "hover" | "dblclick" | "clear";
|
|
146
|
+
locator: LocatorSpec;
|
|
147
|
+
}
|
|
148
|
+
/** Chooses one option of a `<select>`; a list of options is `custom` */
|
|
149
|
+
| {
|
|
150
|
+
kind: "selectOption";
|
|
151
|
+
locator: LocatorSpec;
|
|
152
|
+
option: SelectChoice;
|
|
153
|
+
}
|
|
104
154
|
/** `output` saves the typed value as that output, `{ output: "name" }` */
|
|
105
155
|
| {
|
|
106
156
|
kind: "fill";
|
package/dist/steps/parse/call.js
CHANGED
|
@@ -1,48 +1,131 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { readTextMatcher } from "./expectation.js";
|
|
3
|
+
import { booleanOf, integerOf, readProps, stringOf } from "./literals.js";
|
|
4
|
+
/** The calls that narrow the ones before them, so they can't start a chain on `page` */
|
|
5
|
+
const NARROWING = ["nth", "first", "last", "filter"];
|
|
6
|
+
const MATCHES = ["full", "partial"];
|
|
7
|
+
const TRISTATE = ["checked", "pressed"];
|
|
8
|
+
const FLAGS = ["expanded", "selected", "disabled", "includeHidden"];
|
|
9
|
+
const ROLE_KEYS = ["name", "exact", "level", ...TRISTATE, ...FLAGS];
|
|
10
|
+
/** `{ exact, match, ignoreCase }` of a text call, as far as the call allows */
|
|
11
|
+
function readTextOptions(node, allowed) {
|
|
12
|
+
if (!node)
|
|
13
|
+
return {};
|
|
14
|
+
const props = readProps(node, allowed);
|
|
15
|
+
if (!props)
|
|
16
|
+
return undefined;
|
|
17
|
+
const { exact, match, ignoreCase } = props;
|
|
18
|
+
const matchValue = stringOf(match);
|
|
19
|
+
if ((exact && booleanOf(exact) === undefined) ||
|
|
20
|
+
(ignoreCase && booleanOf(ignoreCase) === undefined) ||
|
|
21
|
+
(match && (matchValue === undefined || !MATCHES.includes(matchValue))))
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
...(exact && { exact: booleanOf(exact) }),
|
|
25
|
+
...(match && { match: matchValue }),
|
|
26
|
+
...(ignoreCase && { ignoreCase: booleanOf(ignoreCase) }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A call that takes a text (or RegExp) and the options `allowed`; a RegExp takes none */
|
|
30
|
+
function readTextCall(method, args, allowed) {
|
|
31
|
+
const text = readTextMatcher(args[0]);
|
|
32
|
+
if (text === undefined || args.length > 2)
|
|
33
|
+
return undefined;
|
|
34
|
+
const options = readTextOptions(args[1], allowed);
|
|
35
|
+
if (!options || (typeof text !== "string" && args.length === 2))
|
|
36
|
+
return undefined;
|
|
37
|
+
return { method, text, ...options };
|
|
38
|
+
}
|
|
39
|
+
function readRole(args) {
|
|
40
|
+
const role = stringOf(args[0]);
|
|
41
|
+
if (role === undefined || args.length > 2)
|
|
42
|
+
return undefined;
|
|
43
|
+
const props = args.length === 2 ? readProps(args[1], ROLE_KEYS) : {};
|
|
44
|
+
if (!props)
|
|
6
45
|
return undefined;
|
|
46
|
+
const call = { method: "getByRole", role };
|
|
47
|
+
if (props.name) {
|
|
48
|
+
const name = readTextMatcher(props.name);
|
|
49
|
+
if (name === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
call.name = name;
|
|
52
|
+
}
|
|
53
|
+
if (props.exact) {
|
|
54
|
+
const exact = booleanOf(props.exact);
|
|
55
|
+
if (exact === undefined || (call.name && typeof call.name !== "string"))
|
|
56
|
+
return undefined;
|
|
57
|
+
call.exact = exact;
|
|
58
|
+
}
|
|
59
|
+
if (props.level) {
|
|
60
|
+
const level = integerOf(props.level);
|
|
61
|
+
if (level === undefined || level < 1)
|
|
62
|
+
return undefined;
|
|
63
|
+
call.level = level;
|
|
64
|
+
}
|
|
65
|
+
for (const key of TRISTATE) {
|
|
66
|
+
const node = props[key];
|
|
67
|
+
if (!node)
|
|
68
|
+
continue;
|
|
69
|
+
const value = stringOf(node) === "mixed" ? "mixed" : booleanOf(node);
|
|
70
|
+
if (value === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
call[key] = value;
|
|
73
|
+
}
|
|
74
|
+
for (const key of FLAGS) {
|
|
75
|
+
const node = props[key];
|
|
76
|
+
if (!node)
|
|
77
|
+
continue;
|
|
78
|
+
const value = booleanOf(node);
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
call[key] = value;
|
|
82
|
+
}
|
|
83
|
+
return call;
|
|
84
|
+
}
|
|
85
|
+
function readNarrowing(method, args) {
|
|
86
|
+
if (method === "first" || method === "last")
|
|
87
|
+
return args.length === 0 ? { method } : undefined;
|
|
88
|
+
if (method === "nth") {
|
|
89
|
+
const index = integerOf(args[0]);
|
|
90
|
+
return args.length === 1 && index !== undefined
|
|
91
|
+
? { method, index }
|
|
92
|
+
: undefined;
|
|
93
|
+
}
|
|
94
|
+
const hasText = readProps(args[0], ["hasText"])?.hasText;
|
|
95
|
+
const text = readTextMatcher(hasText);
|
|
96
|
+
return method === "filter" && args.length === 1 && text !== undefined
|
|
97
|
+
? { method, hasText: text }
|
|
98
|
+
: undefined;
|
|
99
|
+
}
|
|
100
|
+
function readCall(method, args) {
|
|
101
|
+
if (NARROWING.includes(method))
|
|
102
|
+
return readNarrowing(method, args);
|
|
7
103
|
switch (method) {
|
|
8
104
|
case "locator":
|
|
9
|
-
return args.length === 1 ? { method, xpath: first } : undefined;
|
|
10
105
|
case "getByCss":
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
case "getByText":
|
|
15
|
-
case "getByLabel": {
|
|
16
|
-
if (args.length > 2)
|
|
17
|
-
return undefined;
|
|
18
|
-
const options = args.length === 2 ? readObject(args[1], ["match", "ignoreCase"]) : {};
|
|
19
|
-
if (!options)
|
|
106
|
+
case "getByTestId": {
|
|
107
|
+
const first = stringOf(args[0]);
|
|
108
|
+
if (first === undefined || args.length !== 1)
|
|
20
109
|
return undefined;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
method,
|
|
28
|
-
text: first,
|
|
29
|
-
...(match !== undefined && { match: match }),
|
|
30
|
-
...(ignoreCase !== undefined && { ignoreCase: ignoreCase }),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
case "getByRole": {
|
|
34
|
-
if (args.length > 2)
|
|
35
|
-
return undefined;
|
|
36
|
-
const options = args.length === 2 ? readObject(args[1], ["name"]) : {};
|
|
37
|
-
if (!options ||
|
|
38
|
-
(options.name !== undefined && typeof options.name !== "string"))
|
|
39
|
-
return undefined;
|
|
40
|
-
return {
|
|
41
|
-
method,
|
|
42
|
-
role: first,
|
|
43
|
-
...(options.name !== undefined && { name: options.name }),
|
|
44
|
-
};
|
|
110
|
+
return method === "locator"
|
|
111
|
+
? { method, xpath: first }
|
|
112
|
+
: method === "getByCss"
|
|
113
|
+
? { method, css: first }
|
|
114
|
+
: { method, testId: first };
|
|
45
115
|
}
|
|
116
|
+
case "getByText":
|
|
117
|
+
case "getByLabel":
|
|
118
|
+
return readTextCall(method, args, [
|
|
119
|
+
"exact",
|
|
120
|
+
"match",
|
|
121
|
+
"ignoreCase",
|
|
122
|
+
]);
|
|
123
|
+
case "getByPlaceholder":
|
|
124
|
+
case "getByAltText":
|
|
125
|
+
case "getByTitle":
|
|
126
|
+
return readTextCall(method, args, ["exact"]);
|
|
127
|
+
case "getByRole":
|
|
128
|
+
return readRole(args);
|
|
46
129
|
default:
|
|
47
130
|
return undefined;
|
|
48
131
|
}
|
|
@@ -60,7 +143,10 @@ function readChain(node) {
|
|
|
60
143
|
calls.unshift(call);
|
|
61
144
|
current = current.expression.expression;
|
|
62
145
|
}
|
|
63
|
-
return ts.isIdentifier(current) &&
|
|
146
|
+
return ts.isIdentifier(current) &&
|
|
147
|
+
current.text === "page" &&
|
|
148
|
+
calls.length > 0 &&
|
|
149
|
+
!NARROWING.includes(calls[0].method)
|
|
64
150
|
? calls
|
|
65
151
|
: undefined;
|
|
66
152
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import type { Expectation, PageExpectation, TextValue } from "../model.js";
|
|
2
|
+
import type { Expectation, PageExpectation, TextMatcher, TextValue } from "../model.js";
|
|
3
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
5
|
/** `/source/flags` */
|
|
@@ -7,6 +7,7 @@ export declare function readRegex(node: ts.Expression | undefined): {
|
|
|
7
7
|
source: string;
|
|
8
8
|
flags: string;
|
|
9
9
|
} | undefined;
|
|
10
|
+
export declare function readTextMatcher(node: ts.Expression | undefined): TextMatcher | undefined;
|
|
10
11
|
/** The matchers that take `{ output }` as their last argument */
|
|
11
12
|
export declare const OUTPUT_MATCHERS: readonly string[];
|
|
12
13
|
export declare function readExpectation(matcher: string, args: readonly ts.Expression[]): Expectation | undefined;
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { stringOf } from "./literals.js";
|
|
3
3
|
const TEXT_MATCHERS = ["toHaveText", "toContainText", "toHaveValue"];
|
|
4
|
+
/** The matchers that take no argument: a call with options (`{ visible: false }`, `{ timeout }`) is custom */
|
|
5
|
+
const NO_ARGUMENT_MATCHERS = [
|
|
6
|
+
"toBeVisible",
|
|
7
|
+
"toBeHidden",
|
|
8
|
+
"toBeEnabled",
|
|
9
|
+
"toBeChecked",
|
|
10
|
+
"toBeFocused",
|
|
11
|
+
"toBeDisabled",
|
|
12
|
+
"toBeEditable",
|
|
13
|
+
"toBeAttached",
|
|
14
|
+
"toBeInViewport",
|
|
15
|
+
"toBeEmpty",
|
|
16
|
+
];
|
|
17
|
+
/** The matchers that take one text or RegExp; only `TEXT_MATCHERS` can also save an output */
|
|
18
|
+
const TEXT_ARGUMENT_MATCHERS = [
|
|
19
|
+
...TEXT_MATCHERS,
|
|
20
|
+
"toHaveId",
|
|
21
|
+
"toHaveAccessibleName",
|
|
22
|
+
"toHaveAccessibleDescription",
|
|
23
|
+
];
|
|
4
24
|
/** `env.NAME`, `secrets.NAME`, `inputs.NAME` (or `env["NAME"]` and so on) or a string literal */
|
|
5
25
|
export function readValue(node) {
|
|
6
26
|
if (!node)
|
|
@@ -37,7 +57,7 @@ export function readRegex(node) {
|
|
|
37
57
|
const end = node.text.lastIndexOf("/");
|
|
38
58
|
return { source: node.text.slice(1, end), flags: node.text.slice(end + 1) };
|
|
39
59
|
}
|
|
40
|
-
function readTextMatcher(node) {
|
|
60
|
+
export function readTextMatcher(node) {
|
|
41
61
|
if (!node)
|
|
42
62
|
return undefined;
|
|
43
63
|
const literal = stringOf(node);
|
|
@@ -52,11 +72,10 @@ export const OUTPUT_MATCHERS = [
|
|
|
52
72
|
"toHaveAttribute",
|
|
53
73
|
];
|
|
54
74
|
export function readExpectation(matcher, args) {
|
|
55
|
-
if (matcher
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return args.length === 0 ? { matcher } : undefined;
|
|
75
|
+
if (NO_ARGUMENT_MATCHERS.includes(matcher))
|
|
76
|
+
return args.length === 0
|
|
77
|
+
? { matcher: matcher }
|
|
78
|
+
: undefined;
|
|
60
79
|
if (matcher === "toHaveClass") {
|
|
61
80
|
const expected = readTextMatcher(args[0]);
|
|
62
81
|
return args.length === 1 && expected !== undefined
|
|
@@ -83,10 +102,13 @@ export function readExpectation(matcher, args) {
|
|
|
83
102
|
? { matcher, name, expected }
|
|
84
103
|
: undefined;
|
|
85
104
|
}
|
|
86
|
-
if (
|
|
105
|
+
if (TEXT_ARGUMENT_MATCHERS.includes(matcher)) {
|
|
87
106
|
const expected = readTextMatcher(args[0]);
|
|
88
107
|
return args.length === 1 && expected !== undefined
|
|
89
|
-
? {
|
|
108
|
+
? {
|
|
109
|
+
matcher: matcher,
|
|
110
|
+
expected,
|
|
111
|
+
}
|
|
90
112
|
: undefined;
|
|
91
113
|
}
|
|
92
114
|
return undefined;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
export declare function stringOf(node: ts.Node | undefined): string | undefined;
|
|
3
|
+
/** `true` or `false` */
|
|
4
|
+
export declare function booleanOf(node: ts.Node | undefined): boolean | undefined;
|
|
5
|
+
/** An integer literal, with a `-` in front when negative */
|
|
6
|
+
export declare function integerOf(node: ts.Node | undefined): number | undefined;
|
|
3
7
|
/** `{ key: literal, … }` with only the allowed keys, or undefined when it has anything else */
|
|
4
8
|
export declare function readObject(node: ts.Node | undefined, allowed: readonly string[]): Record<string, string | boolean> | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The values of `{ key: value, … }` as expressions, by key, or undefined when the object has a key that isn't
|
|
11
|
+
* allowed or isn't a plain name, a spread, a shorthand or a key twice
|
|
12
|
+
*/
|
|
13
|
+
export declare function readProps(node: ts.Node | undefined, allowed: readonly string[]): Record<string, ts.Expression> | undefined;
|
|
@@ -5,6 +5,30 @@ export function stringOf(node) {
|
|
|
5
5
|
? node.text
|
|
6
6
|
: undefined;
|
|
7
7
|
}
|
|
8
|
+
/** `true` or `false` */
|
|
9
|
+
export function booleanOf(node) {
|
|
10
|
+
if (node?.kind === ts.SyntaxKind.TrueKeyword)
|
|
11
|
+
return true;
|
|
12
|
+
if (node?.kind === ts.SyntaxKind.FalseKeyword)
|
|
13
|
+
return false;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
/** An integer literal, with a `-` in front when negative */
|
|
17
|
+
export function integerOf(node) {
|
|
18
|
+
if (!node)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (ts.isNumericLiteral(node)) {
|
|
21
|
+
const value = Number(node.text);
|
|
22
|
+
return Number.isInteger(value) ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
if (ts.isPrefixUnaryExpression(node) &&
|
|
25
|
+
node.operator === ts.SyntaxKind.MinusToken &&
|
|
26
|
+
ts.isNumericLiteral(node.operand)) {
|
|
27
|
+
const value = integerOf(node.operand);
|
|
28
|
+
return value === undefined || value === 0 ? undefined : -value;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
8
32
|
/** `{ key: literal, … }` with only the allowed keys, or undefined when it has anything else */
|
|
9
33
|
export function readObject(node, allowed) {
|
|
10
34
|
if (!node || !ts.isObjectLiteralExpression(node))
|
|
@@ -26,3 +50,21 @@ export function readObject(node, allowed) {
|
|
|
26
50
|
}
|
|
27
51
|
return result;
|
|
28
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The values of `{ key: value, … }` as expressions, by key, or undefined when the object has a key that isn't
|
|
55
|
+
* allowed or isn't a plain name, a spread, a shorthand or a key twice
|
|
56
|
+
*/
|
|
57
|
+
export function readProps(node, allowed) {
|
|
58
|
+
if (!node || !ts.isObjectLiteralExpression(node))
|
|
59
|
+
return undefined;
|
|
60
|
+
const props = {};
|
|
61
|
+
for (const property of node.properties) {
|
|
62
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name))
|
|
63
|
+
return undefined;
|
|
64
|
+
const name = property.name.text;
|
|
65
|
+
if (!allowed.includes(name) || name in props)
|
|
66
|
+
return undefined;
|
|
67
|
+
props[name] = property.initializer;
|
|
68
|
+
}
|
|
69
|
+
return props;
|
|
70
|
+
}
|
package/dist/steps/parse/step.js
CHANGED
|
@@ -2,7 +2,7 @@ import ts from "typescript";
|
|
|
2
2
|
import { readLocator } from "./call.js";
|
|
3
3
|
import { OUTPUT_MATCHERS, readExpectation, readPageExpectation, readRegex, readValue, } from "./expectation.js";
|
|
4
4
|
import { readFiles } from "./file-entry.js";
|
|
5
|
-
import { readObject, stringOf } from "./literals.js";
|
|
5
|
+
import { integerOf, readObject, readProps, stringOf } from "./literals.js";
|
|
6
6
|
/**
|
|
7
7
|
* The name in a trailing `{ output: "name" }`: undefined without one, null when the argument is there but
|
|
8
8
|
* isn't exactly that
|
|
@@ -53,6 +53,25 @@ function readLocatorExpect(subject, negated, matcher, args) {
|
|
|
53
53
|
}
|
|
54
54
|
: undefined;
|
|
55
55
|
}
|
|
56
|
+
/** The actions that take a locator and nothing else */
|
|
57
|
+
const ACTIONS = ["check", "uncheck", "hover", "dblclick", "clear"];
|
|
58
|
+
/** `"value or label"`, `{ value }`, `{ label }` or `{ index }`: exactly one key; anything else is undefined */
|
|
59
|
+
function readSelectChoice(node) {
|
|
60
|
+
const text = stringOf(node);
|
|
61
|
+
if (text !== undefined)
|
|
62
|
+
return text;
|
|
63
|
+
const props = readProps(node, ["value", "label", "index"]);
|
|
64
|
+
const keys = Object.keys(props ?? {});
|
|
65
|
+
if (!props || keys.length !== 1)
|
|
66
|
+
return undefined;
|
|
67
|
+
const [key] = keys;
|
|
68
|
+
if (key === "index") {
|
|
69
|
+
const index = integerOf(props.index);
|
|
70
|
+
return index !== undefined && index >= 0 ? { index } : undefined;
|
|
71
|
+
}
|
|
72
|
+
const value = stringOf(props[key]);
|
|
73
|
+
return value === undefined ? undefined : { [key]: value };
|
|
74
|
+
}
|
|
56
75
|
/** The step a statement stands for, or undefined when it is outside the recorder's set */
|
|
57
76
|
export function readStep(statement) {
|
|
58
77
|
if (!ts.isExpressionStatement(statement) ||
|
|
@@ -117,6 +136,14 @@ export function readStep(statement) {
|
|
|
117
136
|
return undefined;
|
|
118
137
|
if (name.text === "click" && args.length === 0)
|
|
119
138
|
return { kind: "click", locator };
|
|
139
|
+
if (ACTIONS.includes(name.text) && args.length === 0)
|
|
140
|
+
return { kind: name.text, locator };
|
|
141
|
+
if (name.text === "selectOption" && args.length === 1) {
|
|
142
|
+
const option = readSelectChoice(args[0]);
|
|
143
|
+
return option === undefined
|
|
144
|
+
? undefined
|
|
145
|
+
: { kind: "selectOption", locator, option };
|
|
146
|
+
}
|
|
120
147
|
if (name.text === "press" && args.length === 1) {
|
|
121
148
|
const key = stringOf(args[0]);
|
|
122
149
|
return key === undefined ? undefined : { kind: "press", locator, key };
|