walkwright 0.0.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/LICENSE +21 -0
- package/README.md +276 -0
- package/bin/walkwright.js +3 -0
- package/build/aria.d.ts +92 -0
- package/build/aria.js +215 -0
- package/build/aria.js.map +1 -0
- package/build/browser.d.ts +3 -0
- package/build/browser.js +18 -0
- package/build/browser.js.map +1 -0
- package/build/chunk-5CHKYNP7.js +507 -0
- package/build/chunk-5CHKYNP7.js.map +1 -0
- package/build/chunk-6YRKPGUO.js +1197 -0
- package/build/chunk-6YRKPGUO.js.map +1 -0
- package/build/chunk-A452S7R3.js +35 -0
- package/build/chunk-A452S7R3.js.map +1 -0
- package/build/chunk-BODO6HIH.js +60 -0
- package/build/chunk-BODO6HIH.js.map +1 -0
- package/build/chunk-FJI2CMCE.js +439 -0
- package/build/chunk-FJI2CMCE.js.map +1 -0
- package/build/chunk-JYHZVRB2.js +80 -0
- package/build/chunk-JYHZVRB2.js.map +1 -0
- package/build/chunk-KQN5IYN6.js +97 -0
- package/build/chunk-KQN5IYN6.js.map +1 -0
- package/build/chunk-KTXNERHT.js +72 -0
- package/build/chunk-KTXNERHT.js.map +1 -0
- package/build/chunk-MFTC7XL2.js +141 -0
- package/build/chunk-MFTC7XL2.js.map +1 -0
- package/build/chunk-N4SSLHMP.js +108 -0
- package/build/chunk-N4SSLHMP.js.map +1 -0
- package/build/chunk-RNNW7OW2.js +270 -0
- package/build/chunk-RNNW7OW2.js.map +1 -0
- package/build/chunk-UAWBPTDW.js +51 -0
- package/build/chunk-UAWBPTDW.js.map +1 -0
- package/build/chunk-Z7CW5CAY.js +64 -0
- package/build/chunk-Z7CW5CAY.js.map +1 -0
- package/build/cli.d.ts +1 -0
- package/build/cli.js +253 -0
- package/build/cli.js.map +1 -0
- package/build/config.d.ts +13 -0
- package/build/config.js +15 -0
- package/build/config.js.map +1 -0
- package/build/descriptor.d.ts +96 -0
- package/build/descriptor.js +27 -0
- package/build/descriptor.js.map +1 -0
- package/build/device-auth.d.ts +6 -0
- package/build/device-auth.js +11 -0
- package/build/device-auth.js.map +1 -0
- package/build/locator.d.ts +140 -0
- package/build/locator.js +92 -0
- package/build/locator.js.map +1 -0
- package/build/paths.d.ts +11 -0
- package/build/paths.js +22 -0
- package/build/paths.js.map +1 -0
- package/build/pom-kcSMxwX2.d.ts +349 -0
- package/build/pom.d.ts +6 -0
- package/build/pom.js +37 -0
- package/build/pom.js.map +1 -0
- package/build/project.d.ts +13 -0
- package/build/project.js +19 -0
- package/build/project.js.map +1 -0
- package/build/provisioner.d.ts +34 -0
- package/build/provisioner.js +14 -0
- package/build/provisioner.js.map +1 -0
- package/build/recorder.d.ts +6 -0
- package/build/recorder.js +15 -0
- package/build/recorder.js.map +1 -0
- package/build/reporter.d.ts +16 -0
- package/build/reporter.js +426 -0
- package/build/reporter.js.map +1 -0
- package/build/schema.d.ts +49 -0
- package/build/schema.js +16 -0
- package/build/schema.js.map +1 -0
- package/build/storage-state.d.ts +22 -0
- package/build/storage-state.js +42 -0
- package/build/storage-state.js.map +1 -0
- package/build/store.d.ts +17 -0
- package/build/store.js +9 -0
- package/build/store.js.map +1 -0
- package/build/ui.d.ts +49 -0
- package/build/ui.js +56 -0
- package/build/ui.js.map +1 -0
- package/build/upload.d.ts +10 -0
- package/build/upload.js +8 -0
- package/build/upload.js.map +1 -0
- package/build/wait.d.ts +14 -0
- package/build/wait.js +50 -0
- package/build/wait.js.map +1 -0
- package/package.json +52 -0
- package/src/aria.ts +326 -0
- package/src/browser.ts +15 -0
- package/src/cli.ts +295 -0
- package/src/config.ts +16 -0
- package/src/descriptor.ts +179 -0
- package/src/device-auth.ts +188 -0
- package/src/locator.ts +683 -0
- package/src/paths.ts +70 -0
- package/src/pom.ts +2363 -0
- package/src/project.ts +77 -0
- package/src/provisioner.ts +183 -0
- package/src/recorder.ts +877 -0
- package/src/reporter.ts +561 -0
- package/src/schema.ts +37 -0
- package/src/storage-state.ts +71 -0
- package/src/store.ts +96 -0
- package/src/ui.ts +333 -0
- package/src/upload.ts +116 -0
- package/src/wait.ts +63 -0
package/src/locator.ts
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import type { Locator, Page } from "@playwright/test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export type Mount = Page | Locator;
|
|
5
|
+
|
|
6
|
+
export type Scope = Page | LazyLocator;
|
|
7
|
+
|
|
8
|
+
export function pageOf(mount: Mount): Page {
|
|
9
|
+
return "goto" in mount ? mount : mount.page();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Text =
|
|
13
|
+
| string
|
|
14
|
+
| { pattern: string; flags?: string }
|
|
15
|
+
| { param: string }
|
|
16
|
+
| { template: (string | { param: string })[] };
|
|
17
|
+
|
|
18
|
+
export const textSchema: z.ZodType<Text> = z.union([
|
|
19
|
+
z.string(),
|
|
20
|
+
z.object({ pattern: z.string(), flags: z.string().optional() }),
|
|
21
|
+
z.object({ param: z.string() }),
|
|
22
|
+
z.object({
|
|
23
|
+
template: z.array(z.union([z.string(), z.object({ param: z.string() })])),
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export type By =
|
|
28
|
+
| {
|
|
29
|
+
by: "role";
|
|
30
|
+
role: string;
|
|
31
|
+
name?: Text;
|
|
32
|
+
exact?: true;
|
|
33
|
+
checked?: boolean;
|
|
34
|
+
expanded?: boolean;
|
|
35
|
+
selected?: boolean;
|
|
36
|
+
pressed?: boolean;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
}
|
|
39
|
+
| { by: "label" | "placeholder" | "text" | "title"; text: Text; exact?: true }
|
|
40
|
+
| { by: "testid"; id: string };
|
|
41
|
+
|
|
42
|
+
const roleStepSchema = z.object({
|
|
43
|
+
by: z.literal("role"),
|
|
44
|
+
role: z.string(),
|
|
45
|
+
name: textSchema.optional(),
|
|
46
|
+
exact: z.literal(true).optional(),
|
|
47
|
+
checked: z.boolean().optional(),
|
|
48
|
+
expanded: z.boolean().optional(),
|
|
49
|
+
selected: z.boolean().optional(),
|
|
50
|
+
pressed: z.boolean().optional(),
|
|
51
|
+
disabled: z.boolean().optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const textStepSchema = z.object({
|
|
55
|
+
by: z.enum(["label", "placeholder", "text", "title"]),
|
|
56
|
+
text: textSchema,
|
|
57
|
+
exact: z.literal(true).optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const testIdStepSchema = z.object({
|
|
61
|
+
by: z.literal("testid"),
|
|
62
|
+
id: z.string(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export interface LocatorFilter {
|
|
66
|
+
hasText?: Text;
|
|
67
|
+
has?: LocatorDescriptor;
|
|
68
|
+
hasNot?: LocatorDescriptor;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type Refinement = { filter: LocatorFilter } | { nth: number };
|
|
72
|
+
|
|
73
|
+
export interface Refined {
|
|
74
|
+
refine?: Refinement[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const filterSchema = z.object({
|
|
78
|
+
hasText: textSchema.optional(),
|
|
79
|
+
has: z
|
|
80
|
+
.lazy((): z.ZodType<LocatorDescriptor> => locatorDescriptorSchema)
|
|
81
|
+
.optional(),
|
|
82
|
+
hasNot: z
|
|
83
|
+
.lazy((): z.ZodType<LocatorDescriptor> => locatorDescriptorSchema)
|
|
84
|
+
.optional(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const refinementSchema: z.ZodType<Refinement> = z.union([
|
|
88
|
+
z.object({ filter: filterSchema }),
|
|
89
|
+
z.object({ nth: z.number() }),
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const refinementShape = {
|
|
93
|
+
refine: z.array(refinementSchema).optional(),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const cssXpathStepSchema = z.object({
|
|
97
|
+
by: z.enum(["css", "xpath"]),
|
|
98
|
+
selector: z.string(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export type LocatorNode =
|
|
102
|
+
| (By & Refined)
|
|
103
|
+
| ({ by: "css" | "xpath"; selector: string } & Refined)
|
|
104
|
+
| { or: LocatorDescriptor[] }
|
|
105
|
+
| { and: LocatorDescriptor[] };
|
|
106
|
+
|
|
107
|
+
const locatorNodeSchema: z.ZodType<LocatorNode> = z.lazy(() =>
|
|
108
|
+
z.union([
|
|
109
|
+
roleStepSchema.extend(refinementShape),
|
|
110
|
+
textStepSchema.extend(refinementShape),
|
|
111
|
+
testIdStepSchema.extend(refinementShape),
|
|
112
|
+
cssXpathStepSchema.extend(refinementShape),
|
|
113
|
+
z.object({ or: z.array(locatorDescriptorSchema) }),
|
|
114
|
+
z.object({ and: z.array(locatorDescriptorSchema) }),
|
|
115
|
+
]),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
export type LocatorDescriptor = LocatorNode[];
|
|
119
|
+
|
|
120
|
+
export const locatorDescriptorSchema: z.ZodType<LocatorDescriptor> =
|
|
121
|
+
z.array(locatorNodeSchema);
|
|
122
|
+
|
|
123
|
+
export type Role = Parameters<Page["getByRole"]>[0];
|
|
124
|
+
type RoleQueryOptions = Parameters<Page["getByRole"]>[1];
|
|
125
|
+
|
|
126
|
+
function paramValueOf(name: string, params: Record<string, string>): string {
|
|
127
|
+
const value = params[name];
|
|
128
|
+
if (value === undefined) {
|
|
129
|
+
throw new Error(`missing locator param "${name}"`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function textOf(text: Text, params: Record<string, string>): string | RegExp {
|
|
136
|
+
if (typeof text === "string") {
|
|
137
|
+
return text;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ("pattern" in text) {
|
|
141
|
+
return new RegExp(text.pattern, text.flags);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ("template" in text) {
|
|
145
|
+
return text.template
|
|
146
|
+
.map((part) =>
|
|
147
|
+
typeof part === "string" ? part : paramValueOf(part.param, params),
|
|
148
|
+
)
|
|
149
|
+
.join("");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return paramValueOf(text.param, params);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function exactOptions(exact: true | undefined): { exact: true } | undefined {
|
|
156
|
+
return exact === undefined ? undefined : { exact };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function roleOptions(
|
|
160
|
+
node: Extract<By, { by: "role" }>,
|
|
161
|
+
params: Record<string, string>,
|
|
162
|
+
): RoleQueryOptions {
|
|
163
|
+
const { name, exact, checked, expanded, selected, pressed, disabled } = node;
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
name === undefined &&
|
|
167
|
+
exact === undefined &&
|
|
168
|
+
checked === undefined &&
|
|
169
|
+
expanded === undefined &&
|
|
170
|
+
selected === undefined &&
|
|
171
|
+
pressed === undefined &&
|
|
172
|
+
disabled === undefined
|
|
173
|
+
) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...(name === undefined ? {} : { name: textOf(name, params) }),
|
|
179
|
+
...(exact === undefined ? {} : { exact }),
|
|
180
|
+
...(checked === undefined ? {} : { checked }),
|
|
181
|
+
...(expanded === undefined ? {} : { expanded }),
|
|
182
|
+
...(selected === undefined ? {} : { selected }),
|
|
183
|
+
...(pressed === undefined ? {} : { pressed }),
|
|
184
|
+
...(disabled === undefined ? {} : { disabled }),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function applyStep(
|
|
189
|
+
current: Page | Locator,
|
|
190
|
+
node: LocatorNode,
|
|
191
|
+
params: Record<string, string>,
|
|
192
|
+
): Locator {
|
|
193
|
+
if ("or" in node) {
|
|
194
|
+
return orOf(current, node.or, params);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if ("and" in node) {
|
|
198
|
+
return andOf(current, node.and, params);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
switch (node.by) {
|
|
202
|
+
case "role":
|
|
203
|
+
return current.getByRole(node.role as Role, roleOptions(node, params));
|
|
204
|
+
case "label":
|
|
205
|
+
return current.getByLabel(
|
|
206
|
+
textOf(node.text, params),
|
|
207
|
+
exactOptions(node.exact),
|
|
208
|
+
);
|
|
209
|
+
case "placeholder":
|
|
210
|
+
return current.getByPlaceholder(
|
|
211
|
+
textOf(node.text, params),
|
|
212
|
+
exactOptions(node.exact),
|
|
213
|
+
);
|
|
214
|
+
case "text":
|
|
215
|
+
return current.getByText(
|
|
216
|
+
textOf(node.text, params),
|
|
217
|
+
exactOptions(node.exact),
|
|
218
|
+
);
|
|
219
|
+
case "title":
|
|
220
|
+
return current.getByTitle(
|
|
221
|
+
textOf(node.text, params),
|
|
222
|
+
exactOptions(node.exact),
|
|
223
|
+
);
|
|
224
|
+
case "testid":
|
|
225
|
+
return current.getByTestId(node.id);
|
|
226
|
+
case "css":
|
|
227
|
+
return current.locator(node.selector);
|
|
228
|
+
case "xpath":
|
|
229
|
+
return current.locator(`xpath=${node.selector}`);
|
|
230
|
+
default: {
|
|
231
|
+
const exhaustive: never = node;
|
|
232
|
+
throw new Error(`unknown locator step: ${JSON.stringify(exhaustive)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function applyRefinement(
|
|
238
|
+
located: Locator,
|
|
239
|
+
current: Page | Locator,
|
|
240
|
+
refinement: Refinement,
|
|
241
|
+
params: Record<string, string>,
|
|
242
|
+
): Locator {
|
|
243
|
+
if ("nth" in refinement) {
|
|
244
|
+
return located.nth(refinement.nth);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { hasText, has, hasNot } = refinement.filter;
|
|
248
|
+
const page = pageOf(current);
|
|
249
|
+
|
|
250
|
+
return located.filter({
|
|
251
|
+
...(hasText === undefined ? {} : { hasText: textOf(hasText, params) }),
|
|
252
|
+
...(has === undefined ? {} : { has: resolveLocator(page, has, params) }),
|
|
253
|
+
...(hasNot === undefined
|
|
254
|
+
? {}
|
|
255
|
+
: { hasNot: resolveLocator(page, hasNot, params) }),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function applyNode(
|
|
260
|
+
current: Page | Locator,
|
|
261
|
+
node: LocatorNode,
|
|
262
|
+
params: Record<string, string>,
|
|
263
|
+
): Locator {
|
|
264
|
+
const located = applyStep(current, node, params);
|
|
265
|
+
const refine = "by" in node ? (node.refine ?? []) : [];
|
|
266
|
+
|
|
267
|
+
return refine.reduce(
|
|
268
|
+
(refined, refinement) =>
|
|
269
|
+
applyRefinement(refined, current, refinement, params),
|
|
270
|
+
located,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function orOf(
|
|
275
|
+
current: Page | Locator,
|
|
276
|
+
branches: LocatorDescriptor[],
|
|
277
|
+
params: Record<string, string>,
|
|
278
|
+
): Locator {
|
|
279
|
+
const [first, ...rest] = branches;
|
|
280
|
+
if (first === undefined) {
|
|
281
|
+
throw new Error('locator descriptor\'s "or" has no branches');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return rest.reduce(
|
|
285
|
+
(acc, branch) => acc.or(resolveLocator(current, branch, params)),
|
|
286
|
+
resolveLocator(current, first, params),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function andOf(
|
|
291
|
+
current: Page | Locator,
|
|
292
|
+
branches: LocatorDescriptor[],
|
|
293
|
+
params: Record<string, string>,
|
|
294
|
+
): Locator {
|
|
295
|
+
const [first, ...rest] = branches;
|
|
296
|
+
if (first === undefined) {
|
|
297
|
+
throw new Error('locator descriptor\'s "and" has no branches');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return rest.reduce(
|
|
301
|
+
(acc, branch) => acc.and(resolveLocator(current, branch, params)),
|
|
302
|
+
resolveLocator(current, first, params),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function resolveLocator(
|
|
307
|
+
scope: Page | Locator,
|
|
308
|
+
locator: LocatorDescriptor,
|
|
309
|
+
params: Record<string, string> = {},
|
|
310
|
+
): Locator {
|
|
311
|
+
const [first, ...rest] = locator;
|
|
312
|
+
if (first === undefined) {
|
|
313
|
+
throw new Error("locator descriptor has no nodes");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return rest.reduce<Locator>(
|
|
317
|
+
(current, node) => applyNode(current, node, params),
|
|
318
|
+
applyNode(scope, first, params),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface FilterOptions {
|
|
323
|
+
hasText?: string | RegExp | Text;
|
|
324
|
+
has?: LazyLocator;
|
|
325
|
+
hasNot?: LazyLocator;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface LazyLocator {
|
|
329
|
+
(scope: LazyLocator): LazyLocator;
|
|
330
|
+
(scope: Page): Locator;
|
|
331
|
+
chain: LocatorDescriptor;
|
|
332
|
+
params: readonly string[];
|
|
333
|
+
getByRole: (
|
|
334
|
+
role: Role,
|
|
335
|
+
options?: RoleLeafOptions & { name?: string | RegExp },
|
|
336
|
+
) => LazyLocator;
|
|
337
|
+
getByLabel: (text: string | RegExp, options?: NameOptions) => LazyLocator;
|
|
338
|
+
getByPlaceholder: (
|
|
339
|
+
text: string | RegExp,
|
|
340
|
+
options?: NameOptions,
|
|
341
|
+
) => LazyLocator;
|
|
342
|
+
getByText: (text: string | RegExp, options?: NameOptions) => LazyLocator;
|
|
343
|
+
getByTitle: (text: string | RegExp, options?: NameOptions) => LazyLocator;
|
|
344
|
+
getByTestId: (id: string) => LazyLocator;
|
|
345
|
+
locator: (selector: string) => LazyLocator;
|
|
346
|
+
filter: (options: FilterOptions) => LazyLocator;
|
|
347
|
+
nth: (n: number) => LazyLocator;
|
|
348
|
+
first: () => LazyLocator;
|
|
349
|
+
last: () => LazyLocator;
|
|
350
|
+
or: (other: LazyLocator) => LazyLocator;
|
|
351
|
+
and: (other: LazyLocator) => LazyLocator;
|
|
352
|
+
within: (parent: LazyLocator) => LazyLocator;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function isLazyLocator(value: unknown): value is LazyLocator {
|
|
356
|
+
return (
|
|
357
|
+
typeof value === "function" &&
|
|
358
|
+
Array.isArray((value as { chain?: unknown }).chain) &&
|
|
359
|
+
Array.isArray((value as { params?: unknown }).params)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function textFrom(value: string | RegExp): Text {
|
|
364
|
+
return typeof value === "string"
|
|
365
|
+
? value
|
|
366
|
+
: { pattern: value.source, flags: value.flags };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function plainTextOf(text: Text, what: string): string | RegExp {
|
|
370
|
+
if (typeof text === "string") {
|
|
371
|
+
return text;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if ("pattern" in text) {
|
|
375
|
+
return new RegExp(text.pattern, text.flags);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
throw new Error(`a ${what} cannot be a param placeholder`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function hasTextOf(value: string | RegExp | Text): Text {
|
|
382
|
+
return value instanceof RegExp ? textFrom(value) : value;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function lastNodeOf(locator: LocatorDescriptor): LocatorNode & Refined {
|
|
386
|
+
const last = locator[locator.length - 1];
|
|
387
|
+
if (last === undefined) {
|
|
388
|
+
throw new Error("locator descriptor has no nodes");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if ("or" in last || "and" in last) {
|
|
392
|
+
const kind = "or" in last ? "or" : "and";
|
|
393
|
+
throw new Error(`cannot refine an ${kind} node`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return last;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function mergeRefinement(
|
|
400
|
+
locator: LocatorDescriptor,
|
|
401
|
+
step: Refinement,
|
|
402
|
+
): LocatorDescriptor {
|
|
403
|
+
const last = lastNodeOf(locator);
|
|
404
|
+
return [
|
|
405
|
+
...locator.slice(0, -1),
|
|
406
|
+
{ ...last, refine: [...(last.refine ?? []), step] },
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function filterDescriptorOf(options: FilterOptions): LocatorFilter {
|
|
411
|
+
return {
|
|
412
|
+
...(options.hasText === undefined
|
|
413
|
+
? {}
|
|
414
|
+
: { hasText: hasTextOf(options.hasText) }),
|
|
415
|
+
...(options.has === undefined ? {} : { has: options.has.chain }),
|
|
416
|
+
...(options.hasNot === undefined ? {} : { hasNot: options.hasNot.chain }),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function collectParamNames(
|
|
421
|
+
locator: LocatorDescriptor,
|
|
422
|
+
seen: Set<string>,
|
|
423
|
+
names: string[],
|
|
424
|
+
): void {
|
|
425
|
+
const noteName = (name: string): void => {
|
|
426
|
+
if (!seen.has(name)) {
|
|
427
|
+
seen.add(name);
|
|
428
|
+
names.push(name);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const noteText = (text: Text | undefined): void => {
|
|
433
|
+
if (text === undefined || typeof text === "string") {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if ("param" in text) {
|
|
438
|
+
noteName(text.param);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if ("template" in text) {
|
|
443
|
+
for (const part of text.template) {
|
|
444
|
+
if (typeof part !== "string") {
|
|
445
|
+
noteName(part.param);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
for (const node of locator) {
|
|
452
|
+
if ("or" in node || "and" in node) {
|
|
453
|
+
for (const branch of "or" in node ? node.or : node.and) {
|
|
454
|
+
collectParamNames(branch, seen, names);
|
|
455
|
+
}
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (node.by === "role") {
|
|
460
|
+
noteText(node.name);
|
|
461
|
+
} else if ("text" in node) {
|
|
462
|
+
noteText(node.text);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
for (const refinement of node.refine ?? []) {
|
|
466
|
+
if ("filter" in refinement) {
|
|
467
|
+
noteText(refinement.filter.hasText);
|
|
468
|
+
if (refinement.filter.has !== undefined) {
|
|
469
|
+
collectParamNames(refinement.filter.has, seen, names);
|
|
470
|
+
}
|
|
471
|
+
if (refinement.filter.hasNot !== undefined) {
|
|
472
|
+
collectParamNames(refinement.filter.hasNot, seen, names);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function paramNamesOf(locator: LocatorDescriptor): string[] {
|
|
480
|
+
const names: string[] = [];
|
|
481
|
+
collectParamNames(locator, new Set(), names);
|
|
482
|
+
return names;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function lazyLocatorOf(locator: LocatorDescriptor): LazyLocator {
|
|
486
|
+
function call(scope: LazyLocator): LazyLocator;
|
|
487
|
+
function call(scope: Page): Locator;
|
|
488
|
+
function call(scope: Scope): LazyLocator | Locator {
|
|
489
|
+
return isLazyLocator(scope)
|
|
490
|
+
? lazyLocatorOf([...scope.chain, ...locator])
|
|
491
|
+
: resolveLocator(scope, locator);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return Object.assign(call, {
|
|
495
|
+
chain: locator,
|
|
496
|
+
params: paramNamesOf(locator),
|
|
497
|
+
getByRole: (
|
|
498
|
+
role: Role,
|
|
499
|
+
options?: RoleLeafOptions & { name?: string | RegExp },
|
|
500
|
+
): LazyLocator =>
|
|
501
|
+
lazyLocatorOf([...locator, roleNodeOf(role, options?.name, options)]),
|
|
502
|
+
getByLabel: (text: string | RegExp, options?: NameOptions): LazyLocator =>
|
|
503
|
+
lazyLocatorOf([...locator, textNodeOf("label", text, options?.exact)]),
|
|
504
|
+
getByPlaceholder: (
|
|
505
|
+
text: string | RegExp,
|
|
506
|
+
options?: NameOptions,
|
|
507
|
+
): LazyLocator =>
|
|
508
|
+
lazyLocatorOf([
|
|
509
|
+
...locator,
|
|
510
|
+
textNodeOf("placeholder", text, options?.exact),
|
|
511
|
+
]),
|
|
512
|
+
getByText: (text: string | RegExp, options?: NameOptions): LazyLocator =>
|
|
513
|
+
lazyLocatorOf([...locator, textNodeOf("text", text, options?.exact)]),
|
|
514
|
+
getByTitle: (text: string | RegExp, options?: NameOptions): LazyLocator =>
|
|
515
|
+
lazyLocatorOf([...locator, textNodeOf("title", text, options?.exact)]),
|
|
516
|
+
getByTestId: (id: string): LazyLocator =>
|
|
517
|
+
lazyLocatorOf([...locator, { by: "testid", id }]),
|
|
518
|
+
locator: (selector: string): LazyLocator =>
|
|
519
|
+
lazyLocatorOf([...locator, { by: "css", selector }]),
|
|
520
|
+
filter: (options: FilterOptions): LazyLocator =>
|
|
521
|
+
lazyLocatorOf(
|
|
522
|
+
mergeRefinement(locator, { filter: filterDescriptorOf(options) }),
|
|
523
|
+
),
|
|
524
|
+
nth: (n: number): LazyLocator =>
|
|
525
|
+
lazyLocatorOf(mergeRefinement(locator, { nth: n })),
|
|
526
|
+
first: (): LazyLocator =>
|
|
527
|
+
lazyLocatorOf(mergeRefinement(locator, { nth: 0 })),
|
|
528
|
+
last: (): LazyLocator =>
|
|
529
|
+
lazyLocatorOf(mergeRefinement(locator, { nth: -1 })),
|
|
530
|
+
or: (other: LazyLocator): LazyLocator =>
|
|
531
|
+
lazyLocatorOf([{ or: [locator, other.chain] }]),
|
|
532
|
+
and: (other: LazyLocator): LazyLocator =>
|
|
533
|
+
lazyLocatorOf([{ and: [locator, other.chain] }]),
|
|
534
|
+
within: (parent: LazyLocator): LazyLocator =>
|
|
535
|
+
lazyLocatorOf([...parent.chain, ...locator]),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function stepLocator(by: By): LazyLocator {
|
|
540
|
+
return lazyLocatorOf([by]);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function css(selector: string): LazyLocator {
|
|
544
|
+
return lazyLocatorOf([{ by: "css", selector }]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function xpath(selector: string): LazyLocator {
|
|
548
|
+
return lazyLocatorOf([{ by: "xpath", selector }]);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export interface NameOptions {
|
|
552
|
+
exact?: boolean;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export interface RoleLeafOptions extends NameOptions {
|
|
556
|
+
checked?: boolean;
|
|
557
|
+
expanded?: boolean;
|
|
558
|
+
selected?: boolean;
|
|
559
|
+
pressed?: boolean;
|
|
560
|
+
disabled?: boolean;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
type NamedLazyLocator = (
|
|
564
|
+
name: string | RegExp,
|
|
565
|
+
options?: RoleLeafOptions,
|
|
566
|
+
) => LazyLocator;
|
|
567
|
+
|
|
568
|
+
type OptionallyNamedLazyLocator = (
|
|
569
|
+
name?: string | RegExp,
|
|
570
|
+
options?: RoleLeafOptions,
|
|
571
|
+
) => LazyLocator;
|
|
572
|
+
|
|
573
|
+
function roleNodeOf(
|
|
574
|
+
role: Role,
|
|
575
|
+
name: string | RegExp | undefined,
|
|
576
|
+
options: RoleLeafOptions | undefined,
|
|
577
|
+
): Extract<By, { by: "role" }> {
|
|
578
|
+
return {
|
|
579
|
+
by: "role",
|
|
580
|
+
role,
|
|
581
|
+
...(name === undefined ? {} : { name: textFrom(name) }),
|
|
582
|
+
...(options?.exact === true ? { exact: true as const } : {}),
|
|
583
|
+
...(options?.checked === undefined ? {} : { checked: options.checked }),
|
|
584
|
+
...(options?.expanded === undefined ? {} : { expanded: options.expanded }),
|
|
585
|
+
...(options?.selected === undefined ? {} : { selected: options.selected }),
|
|
586
|
+
...(options?.pressed === undefined ? {} : { pressed: options.pressed }),
|
|
587
|
+
...(options?.disabled === undefined ? {} : { disabled: options.disabled }),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function roleLeaf(
|
|
592
|
+
role: Role,
|
|
593
|
+
name: string | RegExp | undefined,
|
|
594
|
+
options: RoleLeafOptions | undefined,
|
|
595
|
+
): LazyLocator {
|
|
596
|
+
return stepLocator(roleNodeOf(role, name, options));
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function textNodeOf(
|
|
600
|
+
by: "label" | "placeholder" | "text" | "title",
|
|
601
|
+
value: string | RegExp,
|
|
602
|
+
exact: boolean | undefined,
|
|
603
|
+
): Extract<By, { by: "label" | "placeholder" | "text" | "title" }> {
|
|
604
|
+
return {
|
|
605
|
+
by,
|
|
606
|
+
text: textFrom(value),
|
|
607
|
+
...(exact === true ? { exact: true as const } : {}),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function textStepLeaf(
|
|
612
|
+
by: "label" | "placeholder" | "text" | "title",
|
|
613
|
+
value: string | RegExp,
|
|
614
|
+
exact: boolean | undefined,
|
|
615
|
+
): LazyLocator {
|
|
616
|
+
return stepLocator(textNodeOf(by, value, exact));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const named =
|
|
620
|
+
(role: Role): NamedLazyLocator =>
|
|
621
|
+
(name, ...rest: [RoleLeafOptions?]) =>
|
|
622
|
+
roleLeaf(role, name, rest[0]);
|
|
623
|
+
|
|
624
|
+
const optionallyNamed =
|
|
625
|
+
(role: Role): OptionallyNamedLazyLocator =>
|
|
626
|
+
(name, ...rest: [RoleLeafOptions?]) =>
|
|
627
|
+
roleLeaf(role, name, rest[0]);
|
|
628
|
+
|
|
629
|
+
export const button = named("button");
|
|
630
|
+
export const link = named("link");
|
|
631
|
+
export const heading = named("heading");
|
|
632
|
+
export const radio = named("radio");
|
|
633
|
+
export const tab = named("tab");
|
|
634
|
+
export const option = named("option");
|
|
635
|
+
export const menuitem = named("menuitem");
|
|
636
|
+
export const columnheader = named("columnheader");
|
|
637
|
+
|
|
638
|
+
export const textbox = optionallyNamed("textbox");
|
|
639
|
+
export const checkbox = optionallyNamed("checkbox");
|
|
640
|
+
export const combobox = optionallyNamed("combobox");
|
|
641
|
+
export const treeitem = optionallyNamed("treeitem");
|
|
642
|
+
export const table = optionallyNamed("table");
|
|
643
|
+
export const grid = optionallyNamed("grid");
|
|
644
|
+
export const tree = optionallyNamed("tree");
|
|
645
|
+
export const list = optionallyNamed("list");
|
|
646
|
+
export const listitem = optionallyNamed("listitem");
|
|
647
|
+
export const alert = optionallyNamed("alert");
|
|
648
|
+
export const main = optionallyNamed("main");
|
|
649
|
+
export const navigation = optionallyNamed("navigation");
|
|
650
|
+
export const tabpanel = optionallyNamed("tabpanel");
|
|
651
|
+
export const menu = optionallyNamed("menu");
|
|
652
|
+
export const row = optionallyNamed("row");
|
|
653
|
+
export const cell = optionallyNamed("cell");
|
|
654
|
+
export const listbox = optionallyNamed("listbox");
|
|
655
|
+
export const radiogroup = optionallyNamed("radiogroup");
|
|
656
|
+
export const dialog = optionallyNamed("dialog");
|
|
657
|
+
export const banner = optionallyNamed("banner");
|
|
658
|
+
|
|
659
|
+
export function label(
|
|
660
|
+
value: string | RegExp,
|
|
661
|
+
options?: NameOptions,
|
|
662
|
+
): LazyLocator {
|
|
663
|
+
return textStepLeaf("label", value, options?.exact);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function testId(id: string): LazyLocator {
|
|
667
|
+
return stepLocator({ by: "testid", id });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function text(
|
|
671
|
+
value: string | RegExp,
|
|
672
|
+
options?: NameOptions,
|
|
673
|
+
): LazyLocator {
|
|
674
|
+
return textStepLeaf("text", value, options?.exact);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function role(
|
|
678
|
+
roleName: Role,
|
|
679
|
+
name?: string | RegExp,
|
|
680
|
+
options?: RoleLeafOptions,
|
|
681
|
+
): LazyLocator {
|
|
682
|
+
return roleLeaf(roleName, name, options);
|
|
683
|
+
}
|