@letsrunit/playwright 0.18.1 → 0.18.3
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/index.js +704 -175
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/browser.ts +7 -5
- package/src/field/aria-select.ts +117 -13
- package/src/field/calendar.ts +77 -11
- package/src/field/composite-date.ts +121 -0
- package/src/field/composite-select.ts +182 -0
- package/src/field/composite-slider.ts +87 -0
- package/src/field/composite-toggle.ts +49 -0
- package/src/field/date-group.ts +99 -69
- package/src/field/date-text-input.ts +49 -31
- package/src/field/index.ts +17 -8
- package/src/field/native-date.ts +7 -4
- package/src/field/radio-group.ts +74 -12
- package/src/field/slider.ts +9 -4
- package/src/field/toggle.ts +28 -5
- package/src/scrub-html.ts +5 -5
- package/src/suppress-interferences.ts +7 -5
- package/src/wait.ts +22 -8
package/src/field/slider.ts
CHANGED
|
@@ -111,10 +111,15 @@ async function getSliderElement(el: Locator, options?: SetOptions): Promise<Loca
|
|
|
111
111
|
const role = await el.getAttribute('role', options).catch(() => null);
|
|
112
112
|
if (role === 'slider') return el;
|
|
113
113
|
|
|
114
|
-
const
|
|
115
|
-
if ((await
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
const sliderByRole = el.getByRole('slider');
|
|
115
|
+
if ((await sliderByRole.count()) > 0) return sliderByRole.first();
|
|
116
|
+
|
|
117
|
+
const sliderByAria = el.locator('[aria-valuenow][aria-valuemin][aria-valuemax]');
|
|
118
|
+
if ((await sliderByAria.count()) > 0) return sliderByAria.first();
|
|
119
|
+
|
|
120
|
+
const sliderByValueNow = el.locator('[aria-valuenow]');
|
|
121
|
+
if ((await sliderByValueNow.count()) > 0) return sliderByValueNow.first();
|
|
122
|
+
|
|
118
123
|
return null;
|
|
119
124
|
}
|
|
120
125
|
|
package/src/field/toggle.ts
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
import type { Loc, SetOptions, Value } from './types';
|
|
2
2
|
|
|
3
|
+
async function getToggleTarget(el: Loc['el'], options?: SetOptions): Promise<Loc['el'] | null> {
|
|
4
|
+
const role = await el.getAttribute('role', options).catch(
|
|
5
|
+
/* v8 ignore next — attribute might be missing or element might have detached during the check */
|
|
6
|
+
() => null,
|
|
7
|
+
);
|
|
8
|
+
if (role === 'checkbox' || role === 'switch') return el;
|
|
9
|
+
|
|
10
|
+
const byRole = el.locator('[role="switch"], [role="checkbox"]').first();
|
|
11
|
+
if ((await byRole.count()) > 0) return byRole;
|
|
12
|
+
|
|
13
|
+
const byState = el.locator('[aria-checked]').first();
|
|
14
|
+
if ((await byState.count()) > 0) return byState;
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export async function setToggle({ el }: Loc, value: Value, options?: SetOptions): Promise<boolean> {
|
|
4
20
|
if (typeof value !== 'boolean' && value !== null) return false;
|
|
5
21
|
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
22
|
+
const target = await getToggleTarget(el, options);
|
|
23
|
+
if (!target) return false;
|
|
8
24
|
|
|
9
|
-
const ariaChecked = await
|
|
25
|
+
const ariaChecked = await target.getAttribute('aria-checked', options).catch(
|
|
26
|
+
/* v8 ignore next — attribute might be missing or element might have detached during the check */
|
|
27
|
+
() => null,
|
|
28
|
+
);
|
|
10
29
|
const isChecked = ariaChecked === 'true';
|
|
11
30
|
|
|
12
|
-
if (Boolean(value) !== isChecked) await
|
|
31
|
+
if (Boolean(value) !== isChecked) await target.click(options);
|
|
13
32
|
|
|
14
|
-
|
|
33
|
+
const nextAriaChecked = await target.getAttribute('aria-checked', options).catch(
|
|
34
|
+
/* v8 ignore next — attribute might be missing or element might have detached during the check */
|
|
35
|
+
() => null,
|
|
36
|
+
);
|
|
37
|
+
return nextAriaChecked === String(Boolean(value));
|
|
15
38
|
}
|
package/src/scrub-html.ts
CHANGED
|
@@ -182,7 +182,7 @@ export async function realScrubHtml(
|
|
|
182
182
|
const doc = dom.window.document;
|
|
183
183
|
|
|
184
184
|
if (o.pickMain) pickMain(doc);
|
|
185
|
-
dropInfraAndSvg(doc,
|
|
185
|
+
dropInfraAndSvg(doc, Boolean(o.dropSvg));
|
|
186
186
|
if (o.dropHidden) dropHiddenTrees(doc);
|
|
187
187
|
if (o.stripAttributes) stripAttributesAndSanitize(doc, o.stripAttributes);
|
|
188
188
|
if (o.dropComments) dropHtmlComments(doc);
|
|
@@ -199,7 +199,7 @@ export async function realScrubHtml(
|
|
|
199
199
|
function hasHiddenAncestor(el: Element): boolean {
|
|
200
200
|
let p: Element | null = el.parentElement;
|
|
201
201
|
while (p) {
|
|
202
|
-
/* v8 ignore next */
|
|
202
|
+
/* v8 ignore next — JSDOM might not have these attributes/values, but we want to check them if they exist */
|
|
203
203
|
if (p.hasAttribute('hidden') || p.hasAttribute('inert') || p.getAttribute('aria-hidden') === 'true') return true;
|
|
204
204
|
|
|
205
205
|
const style = p.getAttribute('style') || '';
|
|
@@ -373,10 +373,10 @@ function isUtilityClass(token: string): boolean {
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
function stripUtilityClasses(doc: Document) {
|
|
376
|
-
for (const el of doc.body.querySelectorAll<
|
|
377
|
-
const kept = el.
|
|
376
|
+
for (const el of doc.body.querySelectorAll<Element>('[class]')) {
|
|
377
|
+
const kept = [...el.classList].filter((t) => t && !isUtilityClass(t));
|
|
378
378
|
if (kept.length === 0) el.removeAttribute('class');
|
|
379
|
-
else el.
|
|
379
|
+
else el.setAttribute('class', kept.join(' '));
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
|
|
@@ -79,7 +79,11 @@ async function tryClick(page: Page, selectors: string[], _label: string) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
async function closeNativeJsAlerts(page: Page) {
|
|
82
|
-
page.on(
|
|
82
|
+
page.on(
|
|
83
|
+
'dialog',
|
|
84
|
+
/* v8 ignore next — callback runs in browser context, not Node */
|
|
85
|
+
(d) => d.accept().catch(() => {}),
|
|
86
|
+
);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
// 1) Known cookie CMPs (quick wins)
|
|
@@ -175,7 +179,7 @@ async function sweepOverlays(page: Page, regex: TrRegExps): Promise<boolean> {
|
|
|
175
179
|
|
|
176
180
|
return await page
|
|
177
181
|
.evaluate(
|
|
178
|
-
/* v8 ignore start */
|
|
182
|
+
/* v8 ignore start — callback runs in browser context, not Node */
|
|
179
183
|
([source, flags]) => {
|
|
180
184
|
const acceptRx = new RegExp(source, flags);
|
|
181
185
|
const isBig = (el: Element) => {
|
|
@@ -227,7 +231,7 @@ async function sweepOverlays(page: Page, regex: TrRegExps): Promise<boolean> {
|
|
|
227
231
|
/* v8 ignore stop */
|
|
228
232
|
[acceptRxSource, acceptRxFlags],
|
|
229
233
|
)
|
|
230
|
-
.catch(
|
|
234
|
+
.catch(() => false);
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
export async function suppressInterferences(page: Page, opts: Options = {}) {
|
|
@@ -268,9 +272,7 @@ export async function suppressInterferences(page: Page, opts: Options = {}) {
|
|
|
268
272
|
|
|
269
273
|
// Optional: bail if we are hitting a loop of re-spawning popups
|
|
270
274
|
if (actions >= maxActions) {
|
|
271
|
-
/* v8 ignore start */
|
|
272
275
|
if (opts.verbose) console.log(`[suppressInterferences] Max actions ${maxActions} reached, stopping.`);
|
|
273
|
-
/* v8 ignore stop */
|
|
274
276
|
break;
|
|
275
277
|
}
|
|
276
278
|
|
package/src/wait.ts
CHANGED
|
@@ -15,7 +15,7 @@ export async function waitForMeta(page: Page, timeout = 2500) {
|
|
|
15
15
|
|
|
16
16
|
await page
|
|
17
17
|
.waitForFunction(
|
|
18
|
-
/* v8 ignore start */
|
|
18
|
+
/* v8 ignore start — callback runs in browser context, not Node */
|
|
19
19
|
() => {
|
|
20
20
|
const head = document.head;
|
|
21
21
|
if (!head) return false;
|
|
@@ -39,7 +39,7 @@ export async function waitForDomIdle(
|
|
|
39
39
|
{ quiet = 500, timeout = 10_000 }: { quiet?: number; timeout?: number } = {},
|
|
40
40
|
) {
|
|
41
41
|
await page.waitForFunction(
|
|
42
|
-
/* v8 ignore start */
|
|
42
|
+
/* v8 ignore start — callback runs in browser context, not Node */
|
|
43
43
|
(q) =>
|
|
44
44
|
new Promise<boolean>((resolve) => {
|
|
45
45
|
let last = performance.now();
|
|
@@ -72,7 +72,7 @@ export async function waitForDomIdle(
|
|
|
72
72
|
// This helps with libraries that slide months using WAAPI (common in React UI libs).
|
|
73
73
|
export async function waitForAnimationsToFinish(root: Locator) {
|
|
74
74
|
await root.page().waitForFunction(
|
|
75
|
-
/* v8 ignore start */
|
|
75
|
+
/* v8 ignore start — callback runs in browser context, not Node */
|
|
76
76
|
(el) => {
|
|
77
77
|
const animations = (el as HTMLElement).getAnimations?.({ subtree: true }) ?? [];
|
|
78
78
|
return animations.every((a) => a.playState !== 'running');
|
|
@@ -81,13 +81,20 @@ export async function waitForAnimationsToFinish(root: Locator) {
|
|
|
81
81
|
await root.elementHandle(),
|
|
82
82
|
);
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
await root.evaluate(
|
|
85
|
+
/* v8 ignore next — callback runs in browser context, not Node */
|
|
86
|
+
() => new Promise<void>((r) => requestAnimationFrame(() => requestAnimationFrame(() => r()))),
|
|
87
|
+
);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
export async function waitForUrlChange(page: Page, prevUrl: string, timeout: number) {
|
|
89
91
|
try {
|
|
90
|
-
await page.waitForFunction(
|
|
92
|
+
await page.waitForFunction(
|
|
93
|
+
/* v8 ignore next — callback runs in browser context, not Node */
|
|
94
|
+
(u) => location.href !== u,
|
|
95
|
+
prevUrl,
|
|
96
|
+
{ timeout },
|
|
97
|
+
);
|
|
91
98
|
return true;
|
|
92
99
|
} catch {
|
|
93
100
|
return false;
|
|
@@ -102,7 +109,7 @@ export async function waitUntilEnabled(page: Page, target: Locator, timeout: num
|
|
|
102
109
|
|
|
103
110
|
await page
|
|
104
111
|
.waitForFunction(
|
|
105
|
-
/* v8 ignore start */
|
|
112
|
+
/* v8 ignore start — callback runs in browser context, not Node */
|
|
106
113
|
(el) => {
|
|
107
114
|
if (!el || !(el as Element).isConnected) return true; // detached → treat as settled
|
|
108
115
|
const aria = (el as HTMLElement).getAttribute('aria-disabled');
|
|
@@ -167,7 +174,14 @@ async function elementKind(target: Locator): Promise<'link' | 'button' | 'other'
|
|
|
167
174
|
if (role === 'link') return 'link';
|
|
168
175
|
if (role === 'button') return 'button';
|
|
169
176
|
|
|
170
|
-
const tag = await target
|
|
177
|
+
const tag = await target
|
|
178
|
+
.evaluate(
|
|
179
|
+
/* v8 ignore next — callback runs in browser context, not Node */
|
|
180
|
+
(el) => el.tagName.toLowerCase(),
|
|
181
|
+
null,
|
|
182
|
+
{ timeout: PROBE },
|
|
183
|
+
)
|
|
184
|
+
.catch(() => '');
|
|
171
185
|
if (tag === 'a') return 'link';
|
|
172
186
|
|
|
173
187
|
if (tag === 'button') return 'button';
|