@letsrunit/playwright 0.7.0 → 0.8.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/index.d.ts +18 -3
- package/dist/index.js +559 -381
- package/dist/index.js.map +1 -1
- package/package.json +3 -6
- package/src/browser.ts +2 -1
- package/src/field/aria-select.ts +3 -3
- package/src/field/calendar.ts +9 -8
- package/src/field/date-group.ts +12 -12
- package/src/field/date-text-input.ts +14 -10
- package/src/field/index.ts +3 -1
- package/src/field/native-date.ts +1 -1
- package/src/field/radio-group.ts +2 -2
- package/src/field/toggle.ts +2 -2
- package/src/fuzzy-locator.ts +24 -29
- package/src/index.ts +1 -0
- package/src/scrub-html.ts +42 -5
- package/src/snapshot.ts +16 -2
- package/src/suppress-interferences.ts +11 -3
- package/src/wait.ts +11 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import rehypeStringify from 'rehype-stringify';
|
|
|
5
5
|
import { unified } from 'unified';
|
|
6
6
|
import stringify from 'fast-json-stable-stringify';
|
|
7
7
|
import { JSDOM } from 'jsdom';
|
|
8
|
+
import * as Diff from 'diff';
|
|
8
9
|
|
|
9
10
|
// src/browser.ts
|
|
10
11
|
async function browse(browser, options = {}) {
|
|
@@ -14,9 +15,12 @@ async function browse(browser, options = {}) {
|
|
|
14
15
|
locale: "en-US",
|
|
15
16
|
...options
|
|
16
17
|
});
|
|
17
|
-
await context.addInitScript(
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
await context.addInitScript(
|
|
19
|
+
/* v8 ignore next */
|
|
20
|
+
() => {
|
|
21
|
+
window.__name = window.__name || ((fn) => fn);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
20
24
|
return await context.newPage();
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -80,11 +84,11 @@ function formatDateForInput(date, type) {
|
|
|
80
84
|
return `${yyyy}-${mm}-${dd}`;
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
|
-
function formatDate(d,
|
|
87
|
+
function formatDate(d, format2) {
|
|
84
88
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
85
89
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
86
90
|
const yyyy = String(d.getFullYear());
|
|
87
|
-
return
|
|
91
|
+
return format2.replace("DD", dd).replace("MM", mm).replace("YYYY", yyyy);
|
|
88
92
|
}
|
|
89
93
|
function getMonthNames(locale) {
|
|
90
94
|
const formatter = new Intl.DateTimeFormat(locale, { month: "long" });
|
|
@@ -101,6 +105,7 @@ async function waitForMeta(page, timeout = 2500) {
|
|
|
101
105
|
await waitForIdle(page);
|
|
102
106
|
page.getByRole("navigation");
|
|
103
107
|
await page.waitForFunction(
|
|
108
|
+
/* v8 ignore start */
|
|
104
109
|
() => {
|
|
105
110
|
const head = document.head;
|
|
106
111
|
if (!head) return false;
|
|
@@ -108,12 +113,14 @@ async function waitForMeta(page, timeout = 2500) {
|
|
|
108
113
|
document.title.trim() || head.querySelector('meta[property^="og:"]') || head.querySelector('meta[name^="twitter:"]') || head.querySelector('script[type="application/ld+json"]')
|
|
109
114
|
);
|
|
110
115
|
},
|
|
116
|
+
/* v8 ignore stop */
|
|
111
117
|
{ timeout }
|
|
112
118
|
).catch(() => {
|
|
113
119
|
});
|
|
114
120
|
}
|
|
115
121
|
async function waitForDomIdle(page, { quiet = 500, timeout = 1e4 } = {}) {
|
|
116
122
|
await page.waitForFunction(
|
|
123
|
+
/* v8 ignore start */
|
|
117
124
|
(q) => new Promise((resolve) => {
|
|
118
125
|
let last = performance.now();
|
|
119
126
|
const obs = new MutationObserver(() => last = performance.now());
|
|
@@ -133,23 +140,31 @@ async function waitForDomIdle(page, { quiet = 500, timeout = 1e4 } = {}) {
|
|
|
133
140
|
};
|
|
134
141
|
tick();
|
|
135
142
|
}),
|
|
143
|
+
/* v8 ignore stop */
|
|
136
144
|
quiet,
|
|
137
145
|
{ timeout }
|
|
138
146
|
);
|
|
139
147
|
}
|
|
140
148
|
async function waitForAnimationsToFinish(root) {
|
|
141
149
|
await root.page().waitForFunction(
|
|
150
|
+
/* v8 ignore start */
|
|
142
151
|
(el) => {
|
|
143
152
|
const animations = el.getAnimations?.({ subtree: true }) ?? [];
|
|
144
153
|
return animations.every((a) => a.playState !== "running");
|
|
145
154
|
},
|
|
155
|
+
/* v8 ignore stop */
|
|
146
156
|
await root.elementHandle()
|
|
147
157
|
);
|
|
148
158
|
await root.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r()))));
|
|
149
159
|
}
|
|
150
160
|
async function waitForUrlChange(page, prevUrl, timeout) {
|
|
151
161
|
try {
|
|
152
|
-
await page.waitForFunction(
|
|
162
|
+
await page.waitForFunction(
|
|
163
|
+
/* v8 ignore next */
|
|
164
|
+
(u) => location.href !== u,
|
|
165
|
+
prevUrl,
|
|
166
|
+
{ timeout }
|
|
167
|
+
);
|
|
153
168
|
return true;
|
|
154
169
|
} catch {
|
|
155
170
|
return false;
|
|
@@ -161,12 +176,14 @@ async function waitUntilEnabled(page, target, timeout) {
|
|
|
161
176
|
const handle = await target.elementHandle().catch(() => null);
|
|
162
177
|
if (!handle) return;
|
|
163
178
|
await page.waitForFunction(
|
|
179
|
+
/* v8 ignore start */
|
|
164
180
|
(el) => {
|
|
165
181
|
if (!el || !el.isConnected) return true;
|
|
166
182
|
const aria = el.getAttribute("aria-disabled");
|
|
167
183
|
const disabled = el.disabled || aria === "true" || el.getAttribute("disabled") !== null;
|
|
168
184
|
return !disabled;
|
|
169
185
|
},
|
|
186
|
+
/* v8 ignore stop */
|
|
170
187
|
handle,
|
|
171
188
|
{ timeout }
|
|
172
189
|
).catch(() => {
|
|
@@ -213,7 +230,12 @@ async function elementKind(target) {
|
|
|
213
230
|
const role = await target.getAttribute("role", { timeout: PROBE }).catch(() => null);
|
|
214
231
|
if (role === "link") return "link";
|
|
215
232
|
if (role === "button") return "button";
|
|
216
|
-
const tag = await target.evaluate(
|
|
233
|
+
const tag = await target.evaluate(
|
|
234
|
+
/* v8 ignore next */
|
|
235
|
+
(el) => el.tagName.toLowerCase(),
|
|
236
|
+
null,
|
|
237
|
+
{ timeout: PROBE }
|
|
238
|
+
).catch(() => "");
|
|
217
239
|
if (tag === "a") return "link";
|
|
218
240
|
if (tag === "button") return "button";
|
|
219
241
|
if (tag === "input") {
|
|
@@ -255,9 +277,7 @@ async function getCalendar(root, options) {
|
|
|
255
277
|
if (!container) return null;
|
|
256
278
|
const found = await container.locator(gridSelector).all();
|
|
257
279
|
const tables = [];
|
|
258
|
-
if (await isCalendarGrid(container))
|
|
259
|
-
tables.push(container);
|
|
260
|
-
}
|
|
280
|
+
if (await isCalendarGrid(container)) tables.push(container);
|
|
261
281
|
for (const grid of found) {
|
|
262
282
|
if (await isCalendarGrid(grid)) {
|
|
263
283
|
tables.push(grid);
|
|
@@ -467,33 +487,37 @@ async function getCandidateLocs(el, options) {
|
|
|
467
487
|
if (candidates.length < 2 || candidates.length > 3) return [];
|
|
468
488
|
return Promise.all(
|
|
469
489
|
candidates.map(async (c) => {
|
|
470
|
-
const info = await c.evaluate(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
attrs
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
options2 =
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
490
|
+
const info = await c.evaluate(
|
|
491
|
+
/* v8 ignore start */
|
|
492
|
+
(node) => {
|
|
493
|
+
const e = node;
|
|
494
|
+
const attrs = {};
|
|
495
|
+
for (const attr of e.attributes) {
|
|
496
|
+
attrs[attr.name] = attr.value;
|
|
497
|
+
}
|
|
498
|
+
let options2 = [];
|
|
499
|
+
if (e.tagName.toLowerCase() === "select") {
|
|
500
|
+
options2 = Array.from(e.options).map((o) => ({
|
|
501
|
+
value: o.value,
|
|
502
|
+
text: o.text
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
tag: e.tagName.toLowerCase(),
|
|
507
|
+
type: e.getAttribute("type"),
|
|
508
|
+
name: e.getAttribute("name"),
|
|
509
|
+
id: e.getAttribute("id"),
|
|
510
|
+
ariaLabel: e.getAttribute("aria-label"),
|
|
511
|
+
placeholder: e.getAttribute("placeholder"),
|
|
512
|
+
min: e.getAttribute("min"),
|
|
513
|
+
max: e.getAttribute("max"),
|
|
514
|
+
inputMode: e.getAttribute("inputmode"),
|
|
515
|
+
attrs,
|
|
516
|
+
options: options2
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
options
|
|
520
|
+
);
|
|
497
521
|
return { el: c, ...info };
|
|
498
522
|
})
|
|
499
523
|
);
|
|
@@ -529,50 +553,70 @@ async function behavioralProbe(candidateLocs, scores, options) {
|
|
|
529
553
|
for (let i = 0; i < candidateLocs.length; i++) {
|
|
530
554
|
const loc = candidateLocs[i];
|
|
531
555
|
if (loc.tag === "input") {
|
|
532
|
-
const can_be_day = await loc.el.evaluate(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
556
|
+
const can_be_day = await loc.el.evaluate(
|
|
557
|
+
/* v8 ignore start */
|
|
558
|
+
(node) => {
|
|
559
|
+
const e = node;
|
|
560
|
+
const old = e.value;
|
|
561
|
+
e.value = "31";
|
|
562
|
+
const valid = e.checkValidity();
|
|
563
|
+
e.value = old;
|
|
564
|
+
return valid;
|
|
565
|
+
},
|
|
566
|
+
options
|
|
567
|
+
);
|
|
540
568
|
if (can_be_day) scores[i].day += 1;
|
|
541
|
-
const cannot_be_day = await loc.el.evaluate(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
569
|
+
const cannot_be_day = await loc.el.evaluate(
|
|
570
|
+
/* v8 ignore start */
|
|
571
|
+
(node) => {
|
|
572
|
+
const e = node;
|
|
573
|
+
const old = e.value;
|
|
574
|
+
e.value = "32";
|
|
575
|
+
const valid = !e.checkValidity();
|
|
576
|
+
e.value = old;
|
|
577
|
+
return valid;
|
|
578
|
+
},
|
|
579
|
+
options
|
|
580
|
+
);
|
|
549
581
|
if (cannot_be_day) scores[i].day += 1;
|
|
550
|
-
const can_be_month = await loc.el.evaluate(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
582
|
+
const can_be_month = await loc.el.evaluate(
|
|
583
|
+
/* v8 ignore start */
|
|
584
|
+
(node) => {
|
|
585
|
+
const e = node;
|
|
586
|
+
const old = e.value;
|
|
587
|
+
e.value = "12";
|
|
588
|
+
const valid = e.checkValidity();
|
|
589
|
+
e.value = old;
|
|
590
|
+
return valid;
|
|
591
|
+
},
|
|
592
|
+
options
|
|
593
|
+
);
|
|
558
594
|
if (can_be_month) scores[i].month += 1;
|
|
559
|
-
const cannot_be_month = await loc.el.evaluate(
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
595
|
+
const cannot_be_month = await loc.el.evaluate(
|
|
596
|
+
/* v8 ignore start */
|
|
597
|
+
(node) => {
|
|
598
|
+
const e = node;
|
|
599
|
+
const old = e.value;
|
|
600
|
+
e.value = "13";
|
|
601
|
+
const valid = !e.checkValidity();
|
|
602
|
+
e.value = old;
|
|
603
|
+
return valid;
|
|
604
|
+
},
|
|
605
|
+
options
|
|
606
|
+
);
|
|
567
607
|
if (cannot_be_month) scores[i].month += 1;
|
|
568
|
-
const can_be_year = await loc.el.evaluate(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
608
|
+
const can_be_year = await loc.el.evaluate(
|
|
609
|
+
/* v8 ignore start */
|
|
610
|
+
(node) => {
|
|
611
|
+
const e = node;
|
|
612
|
+
const old = e.value;
|
|
613
|
+
e.value = "2024";
|
|
614
|
+
const valid = e.checkValidity();
|
|
615
|
+
e.value = old;
|
|
616
|
+
return valid;
|
|
617
|
+
},
|
|
618
|
+
options
|
|
619
|
+
);
|
|
576
620
|
if (can_be_year) scores[i].year += 1;
|
|
577
621
|
}
|
|
578
622
|
}
|
|
@@ -688,22 +732,26 @@ function parseDateString(value, order, sep) {
|
|
|
688
732
|
return dt;
|
|
689
733
|
}
|
|
690
734
|
async function inferLocaleAndPattern(el, options) {
|
|
691
|
-
return el.evaluate(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (
|
|
735
|
+
return el.evaluate(
|
|
736
|
+
/* v8 ignore start */
|
|
737
|
+
() => {
|
|
738
|
+
const lang = document.documentElement.getAttribute("lang") || navigator.language || "en-US";
|
|
739
|
+
const dtf = new Intl.DateTimeFormat(lang, { year: "numeric", month: "2-digit", day: "2-digit" });
|
|
740
|
+
const parts = dtf.formatToParts(new Date(2033, 10, 22));
|
|
741
|
+
const order = [];
|
|
742
|
+
let sep = "/";
|
|
743
|
+
for (const p of parts) {
|
|
744
|
+
if (p.type === "day" || p.type === "month" || p.type === "year") order.push(p.type);
|
|
745
|
+
if (p.type === "literal") {
|
|
746
|
+
const lit = p.value.trim();
|
|
747
|
+
if (lit) sep = lit;
|
|
748
|
+
}
|
|
702
749
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
750
|
+
const finalOrder = order.length === 3 ? order : ["day", "month", "year"];
|
|
751
|
+
return { locale: lang, order: finalOrder, sep };
|
|
752
|
+
},
|
|
753
|
+
options
|
|
754
|
+
);
|
|
707
755
|
}
|
|
708
756
|
async function fillAndReadBack(el, s, options, nextInput) {
|
|
709
757
|
await el.clear(options);
|
|
@@ -711,9 +759,16 @@ async function fillAndReadBack(el, s, options, nextInput) {
|
|
|
711
759
|
if (nextInput) {
|
|
712
760
|
await nextInput.focus(options);
|
|
713
761
|
} else {
|
|
714
|
-
await el.evaluate(
|
|
762
|
+
await el.evaluate(
|
|
763
|
+
/* v8 ignore next */
|
|
764
|
+
(el2) => el2.blur(),
|
|
765
|
+
options
|
|
766
|
+
);
|
|
715
767
|
}
|
|
716
|
-
await el.evaluate(
|
|
768
|
+
await el.evaluate(
|
|
769
|
+
/* v8 ignore next */
|
|
770
|
+
() => new Promise(requestAnimationFrame)
|
|
771
|
+
);
|
|
717
772
|
return await el.inputValue(options);
|
|
718
773
|
}
|
|
719
774
|
function isAmbiguous(value, value2) {
|
|
@@ -853,10 +908,13 @@ async function setSingleDate({ el, tag, type }, value, options) {
|
|
|
853
908
|
"input[type=date], input[type=datetime-local], input[type=month], input[type=week], input[type=time]"
|
|
854
909
|
);
|
|
855
910
|
if (await inputs.count() === 1) {
|
|
856
|
-
const isVisible = await inputs.evaluate(
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
911
|
+
const isVisible = await inputs.evaluate(
|
|
912
|
+
/* v8 ignore next */
|
|
913
|
+
(e) => {
|
|
914
|
+
const style = window.getComputedStyle(e);
|
|
915
|
+
return style.display !== "none" && style.visibility !== "hidden" && e.getAttribute("type") !== "hidden";
|
|
916
|
+
}
|
|
917
|
+
);
|
|
860
918
|
if (isVisible) {
|
|
861
919
|
target = inputs;
|
|
862
920
|
targetType = await target.getAttribute("type", options) || null;
|
|
@@ -909,11 +967,20 @@ async function setNativeInput({ el, tag }, value, options) {
|
|
|
909
967
|
// src/field/aria-select.ts
|
|
910
968
|
async function selectAria({ el }, value, options) {
|
|
911
969
|
if (typeof value !== "string" && typeof value !== "number") return false;
|
|
912
|
-
const role = await el.getAttribute("role", options).catch(
|
|
970
|
+
const role = await el.getAttribute("role", options).catch(
|
|
971
|
+
/* v8 ignore next */
|
|
972
|
+
() => null
|
|
973
|
+
);
|
|
913
974
|
if (role !== "combobox") return false;
|
|
914
|
-
const ariaControls = await el.getAttribute("aria-controls", options).catch(
|
|
975
|
+
const ariaControls = await el.getAttribute("aria-controls", options).catch(
|
|
976
|
+
/* v8 ignore next */
|
|
977
|
+
() => null
|
|
978
|
+
);
|
|
915
979
|
if (!ariaControls) return false;
|
|
916
|
-
const ariaExpanded = await el.getAttribute("aria-expanded", options).catch(
|
|
980
|
+
const ariaExpanded = await el.getAttribute("aria-expanded", options).catch(
|
|
981
|
+
/* v8 ignore next */
|
|
982
|
+
() => null
|
|
983
|
+
);
|
|
917
984
|
if (ariaExpanded !== "true") await el.click(options);
|
|
918
985
|
const stringValue = String(value);
|
|
919
986
|
const listbox = el.page().locator(`#${ariaControls}`);
|
|
@@ -962,14 +1029,20 @@ async function setRadioGroup({ el }, value, options) {
|
|
|
962
1029
|
const ariaRadio = el.locator(`[role="radio"][value="${stringValue}"]`);
|
|
963
1030
|
if (await ariaRadio.count() >= 1) {
|
|
964
1031
|
const item = ariaRadio.first();
|
|
965
|
-
const ariaChecked = await item.getAttribute("aria-checked", options).catch(
|
|
1032
|
+
const ariaChecked = await item.getAttribute("aria-checked", options).catch(
|
|
1033
|
+
/* v8 ignore next */
|
|
1034
|
+
() => null
|
|
1035
|
+
);
|
|
966
1036
|
if (ariaChecked !== "true") await item.click(options);
|
|
967
1037
|
return true;
|
|
968
1038
|
}
|
|
969
1039
|
const ariaRadioByLabel = el.getByLabel(stringValue, { exact: true }).locator('[role="radio"]');
|
|
970
1040
|
if (await ariaRadioByLabel.count() >= 1) {
|
|
971
1041
|
const item = ariaRadioByLabel.first();
|
|
972
|
-
const ariaChecked = await item.getAttribute("aria-checked", options).catch(
|
|
1042
|
+
const ariaChecked = await item.getAttribute("aria-checked", options).catch(
|
|
1043
|
+
/* v8 ignore next */
|
|
1044
|
+
() => null
|
|
1045
|
+
);
|
|
973
1046
|
if (ariaChecked !== "true") await item.click(options);
|
|
974
1047
|
return true;
|
|
975
1048
|
}
|
|
@@ -1093,9 +1166,15 @@ async function setSliderByKeyboard(slider, initialValue, targetValue, options) {
|
|
|
1093
1166
|
// src/field/toggle.ts
|
|
1094
1167
|
async function setToggle({ el }, value, options) {
|
|
1095
1168
|
if (typeof value !== "boolean" && value !== null) return false;
|
|
1096
|
-
const role = await el.getAttribute("role", options).catch(
|
|
1169
|
+
const role = await el.getAttribute("role", options).catch(
|
|
1170
|
+
/* v8 ignore next */
|
|
1171
|
+
() => null
|
|
1172
|
+
);
|
|
1097
1173
|
if (role !== "checkbox" && role !== "switch") return false;
|
|
1098
|
-
const ariaChecked = await el.getAttribute("aria-checked", options).catch(
|
|
1174
|
+
const ariaChecked = await el.getAttribute("aria-checked", options).catch(
|
|
1175
|
+
/* v8 ignore next */
|
|
1176
|
+
() => null
|
|
1177
|
+
);
|
|
1099
1178
|
const isChecked = ariaChecked === "true";
|
|
1100
1179
|
if (Boolean(value) !== isChecked) await el.click(options);
|
|
1101
1180
|
return true;
|
|
@@ -1134,7 +1213,10 @@ async function setFieldValue(el, value, options) {
|
|
|
1134
1213
|
el = await pickFieldElement(el);
|
|
1135
1214
|
}
|
|
1136
1215
|
const tag = await el.evaluate((e) => e.tagName.toLowerCase(), options);
|
|
1137
|
-
const type = await el.getAttribute("type", options).then((s) => s && s.toLowerCase()).catch(
|
|
1216
|
+
const type = await el.getAttribute("type", options).then((s) => s && s.toLowerCase()).catch(
|
|
1217
|
+
/* v8 ignore next */
|
|
1218
|
+
() => null
|
|
1219
|
+
);
|
|
1138
1220
|
const loc = { el, tag, type };
|
|
1139
1221
|
await setValue(loc, value, options);
|
|
1140
1222
|
}
|
|
@@ -1146,47 +1228,51 @@ async function formatHtml(page) {
|
|
|
1146
1228
|
|
|
1147
1229
|
// src/fuzzy-locator.ts
|
|
1148
1230
|
async function fuzzyLocator(page, selector) {
|
|
1149
|
-
const primary = page.locator(selector)
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1231
|
+
const primary = page.locator(selector);
|
|
1232
|
+
const candidates = [
|
|
1233
|
+
tryRelaxNameToHasText(page, selector),
|
|
1234
|
+
tryTagInsteadOfRole(page, selector),
|
|
1235
|
+
tryRoleNameProximity(page, selector),
|
|
1236
|
+
tryFieldAlternative(page, selector),
|
|
1237
|
+
tryAsField(page, selector)
|
|
1238
|
+
];
|
|
1239
|
+
let combined = primary;
|
|
1240
|
+
for (const candidate of candidates) {
|
|
1241
|
+
if (!candidate) continue;
|
|
1242
|
+
combined = combined.or(candidate);
|
|
1157
1243
|
}
|
|
1158
|
-
return
|
|
1244
|
+
return combined.first();
|
|
1159
1245
|
}
|
|
1160
|
-
|
|
1246
|
+
function tryRelaxNameToHasText(page, selector) {
|
|
1161
1247
|
const matchAnyNameFull = selector.match(/^(role=.*)\[name="([^"]+)"i?](.*)$/i);
|
|
1162
1248
|
if (!matchAnyNameFull) return null;
|
|
1163
1249
|
const [, pre, nameText, post] = matchAnyNameFull;
|
|
1164
1250
|
const containsSelector = `${pre}${post}`;
|
|
1165
|
-
return
|
|
1251
|
+
return page.locator(containsSelector, { hasText: nameText });
|
|
1166
1252
|
}
|
|
1167
|
-
|
|
1253
|
+
function tryTagInsteadOfRole(page, selector) {
|
|
1168
1254
|
const matchAnyNameFull = selector.match(/^role=(link|button|option)\s*\[name="([^"]+)"i?](.*)$/i);
|
|
1169
1255
|
if (!matchAnyNameFull) return null;
|
|
1170
1256
|
const [, role, nameText, post] = matchAnyNameFull;
|
|
1171
1257
|
const tag = role === "link" ? "a" : role;
|
|
1172
1258
|
const containsSelector = `css=${tag}${post}`;
|
|
1173
|
-
return
|
|
1259
|
+
return page.locator(containsSelector, { hasText: nameText });
|
|
1174
1260
|
}
|
|
1175
|
-
|
|
1261
|
+
function tryRoleNameProximity(page, selector) {
|
|
1176
1262
|
const matchRole = selector.match(/^role=(\w+)\s*\[name="([^"]+)"i?](.*)$/i);
|
|
1177
1263
|
if (!matchRole) return null;
|
|
1178
1264
|
const [, role, name, rest] = matchRole;
|
|
1179
1265
|
const proximitySelector = `text=${name} >> .. >> role=${role}${rest}`;
|
|
1180
|
-
return
|
|
1266
|
+
return page.locator(proximitySelector);
|
|
1181
1267
|
}
|
|
1182
|
-
|
|
1268
|
+
function tryFieldAlternative(page, selector) {
|
|
1183
1269
|
const matchField = selector.match(/^field="([^"]+)"i?$/i);
|
|
1184
1270
|
if (!matchField) return null;
|
|
1185
1271
|
const [, field] = matchField;
|
|
1186
1272
|
if (!/^[a-zA-Z0-9_-]+$/.test(field)) return null;
|
|
1187
|
-
return
|
|
1273
|
+
return page.locator(`#${field} > input`);
|
|
1188
1274
|
}
|
|
1189
|
-
|
|
1275
|
+
function tryAsField(page, selector) {
|
|
1190
1276
|
const matchRole = selector.match(/^role=(\w+)\s*\[name="([^"]+)"i?](.*)$/i);
|
|
1191
1277
|
if (!matchRole) return null;
|
|
1192
1278
|
const [, role, name, rest] = matchRole;
|
|
@@ -1206,7 +1292,7 @@ async function tryAsField(page, selector) {
|
|
|
1206
1292
|
"option"
|
|
1207
1293
|
]);
|
|
1208
1294
|
if (!fieldRoles.has(role.toLowerCase())) return null;
|
|
1209
|
-
return
|
|
1295
|
+
return page.locator(`field=${name}${rest}`);
|
|
1210
1296
|
}
|
|
1211
1297
|
|
|
1212
1298
|
// src/selector/date-selector.ts
|
|
@@ -1573,12 +1659,336 @@ async function screenshotWithMask(page, options) {
|
|
|
1573
1659
|
}
|
|
1574
1660
|
}
|
|
1575
1661
|
|
|
1662
|
+
// src/utils/type-check.ts
|
|
1663
|
+
function isPage(page) {
|
|
1664
|
+
return typeof page.content === "function" && typeof page.url === "function" && typeof page.screenshot === "function";
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/scrub-html.ts
|
|
1668
|
+
var HTML_MIN_ATTR_THRESHOLD = 25e4;
|
|
1669
|
+
var HTML_LIMIT_LISTS_THRESHOLD = 4e5;
|
|
1670
|
+
var HTML_MAIN_ONLY_THRESHOLD = 6e5;
|
|
1671
|
+
function getDefaults(contentLength) {
|
|
1672
|
+
return {
|
|
1673
|
+
dropHidden: true,
|
|
1674
|
+
dropHead: true,
|
|
1675
|
+
dropSvg: false,
|
|
1676
|
+
pickMain: contentLength >= HTML_MAIN_ONLY_THRESHOLD,
|
|
1677
|
+
stripAttributes: contentLength >= HTML_MIN_ATTR_THRESHOLD ? 2 : 1,
|
|
1678
|
+
normalizeWhitespace: true,
|
|
1679
|
+
dropComments: true,
|
|
1680
|
+
replaceBrInHeadings: true,
|
|
1681
|
+
limitLists: contentLength >= HTML_LIMIT_LISTS_THRESHOLD ? 20 : -1,
|
|
1682
|
+
dropUtilityClasses: false
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
var ALLOWED_ATTRS = {
|
|
1686
|
+
match: /* @__PURE__ */ new Set([
|
|
1687
|
+
// identity/semantics
|
|
1688
|
+
"id",
|
|
1689
|
+
"class",
|
|
1690
|
+
"role",
|
|
1691
|
+
// internationalization
|
|
1692
|
+
"lang",
|
|
1693
|
+
"dir",
|
|
1694
|
+
// anchors & media
|
|
1695
|
+
"href",
|
|
1696
|
+
"title",
|
|
1697
|
+
"target",
|
|
1698
|
+
"rel",
|
|
1699
|
+
"src",
|
|
1700
|
+
"alt",
|
|
1701
|
+
"width",
|
|
1702
|
+
"height",
|
|
1703
|
+
"loading",
|
|
1704
|
+
// tables
|
|
1705
|
+
"scope",
|
|
1706
|
+
"headers",
|
|
1707
|
+
"colspan",
|
|
1708
|
+
"rowspan",
|
|
1709
|
+
// forms (pure semantics—doesn’t change structure)
|
|
1710
|
+
"name",
|
|
1711
|
+
"value",
|
|
1712
|
+
"type",
|
|
1713
|
+
"for",
|
|
1714
|
+
"placeholder",
|
|
1715
|
+
"checked",
|
|
1716
|
+
"selected",
|
|
1717
|
+
"multiple",
|
|
1718
|
+
"method",
|
|
1719
|
+
"action",
|
|
1720
|
+
// time, figure, etc.
|
|
1721
|
+
"datetime"
|
|
1722
|
+
]),
|
|
1723
|
+
regexp: /^aria-[\w-]+|^data-[\w-]+$/i
|
|
1724
|
+
// ARIA attributes & data-* attributes
|
|
1725
|
+
};
|
|
1726
|
+
var ALLOWED_ATTRS_AGGRESSIVE = {
|
|
1727
|
+
match: /* @__PURE__ */ new Set([
|
|
1728
|
+
// structuur / algemene selectors
|
|
1729
|
+
"id",
|
|
1730
|
+
"class",
|
|
1731
|
+
"role",
|
|
1732
|
+
// links / media
|
|
1733
|
+
"href",
|
|
1734
|
+
"src",
|
|
1735
|
+
"alt",
|
|
1736
|
+
"title",
|
|
1737
|
+
// tables
|
|
1738
|
+
"scope",
|
|
1739
|
+
// forms / velden
|
|
1740
|
+
"name",
|
|
1741
|
+
"type",
|
|
1742
|
+
"for",
|
|
1743
|
+
"placeholder",
|
|
1744
|
+
"value",
|
|
1745
|
+
"checked",
|
|
1746
|
+
"selected",
|
|
1747
|
+
// ARIA voor Playwright getByRole/getByLabel
|
|
1748
|
+
"aria-label",
|
|
1749
|
+
"aria-labelledby",
|
|
1750
|
+
"aria-describedby",
|
|
1751
|
+
// veelgebruikte test selectors
|
|
1752
|
+
"data-testid",
|
|
1753
|
+
"data-test-id",
|
|
1754
|
+
"data-cy",
|
|
1755
|
+
"data-qa"
|
|
1756
|
+
]),
|
|
1757
|
+
regexp: null
|
|
1758
|
+
};
|
|
1759
|
+
var HIDDEN_SELECTORS = [
|
|
1760
|
+
"[hidden]",
|
|
1761
|
+
"[inert]",
|
|
1762
|
+
'[aria-hidden="true"]',
|
|
1763
|
+
'[style*="display:none"]',
|
|
1764
|
+
'[style*="visibility:hidden"]',
|
|
1765
|
+
'[style*="opacity:0"]'
|
|
1766
|
+
].join(",");
|
|
1767
|
+
var ALWAYS_DROP = [
|
|
1768
|
+
"script",
|
|
1769
|
+
"style",
|
|
1770
|
+
"template",
|
|
1771
|
+
"noscript",
|
|
1772
|
+
"slot",
|
|
1773
|
+
"object",
|
|
1774
|
+
"embed"
|
|
1775
|
+
];
|
|
1776
|
+
async function scrubHtml(page, opts = {}) {
|
|
1777
|
+
if (isPage(page)) page = { html: await page.content(), url: page.url() };
|
|
1778
|
+
return await memoizedScrubHtml(page, opts);
|
|
1779
|
+
}
|
|
1780
|
+
var memoizedScrubHtml = memoize(realScrubHtml, {
|
|
1781
|
+
max: 16,
|
|
1782
|
+
ttl: 10 * 6e4,
|
|
1783
|
+
cacheKey: (args) => stringify({ html: args[0].html, url: args[0].url, ...args[1] })
|
|
1784
|
+
});
|
|
1785
|
+
async function realScrubHtml({ html, url }, opts = {}) {
|
|
1786
|
+
const o = { ...getDefaults(html.length), ...opts };
|
|
1787
|
+
const dom = new JSDOM(html, { url });
|
|
1788
|
+
const doc = dom.window.document;
|
|
1789
|
+
if (o.pickMain) pickMain(doc);
|
|
1790
|
+
dropInfraAndSvg(doc, !!o.dropSvg);
|
|
1791
|
+
if (o.dropHidden) dropHiddenTrees(doc);
|
|
1792
|
+
if (o.stripAttributes) stripAttributesAndSanitize(doc, o.stripAttributes);
|
|
1793
|
+
if (o.dropComments) dropHtmlComments(doc);
|
|
1794
|
+
if (o.replaceBrInHeadings) replaceBrsInHeadings(doc);
|
|
1795
|
+
if (o.limitLists >= 0) limitListsAndRows(doc, o.limitLists);
|
|
1796
|
+
if (o.dropUtilityClasses) stripUtilityClasses(doc);
|
|
1797
|
+
if (o.normalizeWhitespace) normalizeWhitespace(doc.body);
|
|
1798
|
+
return doc.body.innerHTML;
|
|
1799
|
+
}
|
|
1800
|
+
function hasHiddenAncestor(el) {
|
|
1801
|
+
let p = el.parentElement;
|
|
1802
|
+
while (p) {
|
|
1803
|
+
if (p.hasAttribute("hidden") || p.hasAttribute("inert") || p.getAttribute("aria-hidden") === "true") return true;
|
|
1804
|
+
const style = p.getAttribute("style") || "";
|
|
1805
|
+
if (/\bdisplay\s*:\s*none\b/i.test(style)) return true;
|
|
1806
|
+
if (/\bvisibility\s*:\s*hidden\b/i.test(style)) return true;
|
|
1807
|
+
if (/\bopacity\s*:\s*0(?:\D|$)/i.test(style)) return true;
|
|
1808
|
+
p = p.parentElement;
|
|
1809
|
+
}
|
|
1810
|
+
return false;
|
|
1811
|
+
}
|
|
1812
|
+
function normalizeWhitespace(root) {
|
|
1813
|
+
const preLike = /* @__PURE__ */ new Set(["PRE", "CODE", "SAMP", "KBD"]);
|
|
1814
|
+
const doc = root.ownerDocument;
|
|
1815
|
+
const walker = doc.createTreeWalker(
|
|
1816
|
+
root,
|
|
1817
|
+
4
|
|
1818
|
+
/*NodeFilter.SHOW_TEXT*/
|
|
1819
|
+
);
|
|
1820
|
+
const changes = [];
|
|
1821
|
+
let node;
|
|
1822
|
+
while (node = walker.nextNode()) {
|
|
1823
|
+
const text = node;
|
|
1824
|
+
const parent = text.parentElement;
|
|
1825
|
+
if (!parent) continue;
|
|
1826
|
+
if (preLike.has(parent.tagName)) continue;
|
|
1827
|
+
const v = text.nodeValue ?? "";
|
|
1828
|
+
const collapsed = v.replace(/\s+/g, " ");
|
|
1829
|
+
if (collapsed !== v) changes.push(text);
|
|
1830
|
+
}
|
|
1831
|
+
for (const t of changes) {
|
|
1832
|
+
const parent = t.parentElement;
|
|
1833
|
+
const isBlockish = /^(P|LI|DIV|SECTION|ARTICLE|ASIDE|HEADER|FOOTER|MAIN|NAV|H[1-6]|BLOCKQUOTE|FIGCAPTION|TD|TH)$/i.test(parent.tagName);
|
|
1834
|
+
t.nodeValue = (t.nodeValue || "").replace(/\s+/g, " ");
|
|
1835
|
+
if (isBlockish) t.nodeValue = (t.nodeValue || "").trim();
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function pickMain(doc) {
|
|
1839
|
+
const main = doc.querySelector("main");
|
|
1840
|
+
if (!main) return false;
|
|
1841
|
+
const clone = main.cloneNode(true);
|
|
1842
|
+
doc.body.innerHTML = "";
|
|
1843
|
+
doc.body.appendChild(clone);
|
|
1844
|
+
return true;
|
|
1845
|
+
}
|
|
1846
|
+
function dropInfraAndSvg(doc, dropSvg) {
|
|
1847
|
+
const toDrop = [...ALWAYS_DROP, dropSvg ? "svg" : ""].filter(Boolean).join(",");
|
|
1848
|
+
if (!toDrop) return;
|
|
1849
|
+
doc.querySelectorAll(toDrop).forEach((el) => el.remove());
|
|
1850
|
+
}
|
|
1851
|
+
function dropHiddenTrees(doc) {
|
|
1852
|
+
doc.querySelectorAll(HIDDEN_SELECTORS).forEach((el) => el.remove());
|
|
1853
|
+
const all = [...doc.body.querySelectorAll("*")];
|
|
1854
|
+
for (const el of all) {
|
|
1855
|
+
if (!el.isConnected) continue;
|
|
1856
|
+
if (hasHiddenAncestor(el)) el.remove();
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function stripAttributesAndSanitize(doc, level) {
|
|
1860
|
+
if (!level) return;
|
|
1861
|
+
const all = [...doc.body.querySelectorAll("*")];
|
|
1862
|
+
for (const el of all) {
|
|
1863
|
+
const isSvg = el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
1864
|
+
for (const { name } of [...el.attributes]) {
|
|
1865
|
+
const lower = name.toLowerCase();
|
|
1866
|
+
if (lower.startsWith("on")) {
|
|
1867
|
+
el.removeAttribute(name);
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
if (lower === "style") {
|
|
1871
|
+
el.removeAttribute(name);
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
if (isSvg) continue;
|
|
1875
|
+
const allowed = level === 1 ? ALLOWED_ATTRS : ALLOWED_ATTRS_AGGRESSIVE;
|
|
1876
|
+
if (!allowed.match.has(lower) && !allowed.regexp?.test(name)) {
|
|
1877
|
+
el.removeAttribute(name);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
doc.querySelectorAll("a[href]").forEach((a) => {
|
|
1882
|
+
const href = a.getAttribute("href") || "";
|
|
1883
|
+
if (/^\s*javascript:/i.test(href)) a.removeAttribute("href");
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
function dropHtmlComments(doc) {
|
|
1887
|
+
const nf = doc.defaultView?.NodeFilter;
|
|
1888
|
+
const SHOW_COMMENT = nf?.SHOW_COMMENT ?? 128;
|
|
1889
|
+
const walker = doc.createTreeWalker(doc, SHOW_COMMENT);
|
|
1890
|
+
const toRemove = [];
|
|
1891
|
+
let n;
|
|
1892
|
+
while (n = walker.nextNode()) toRemove.push(n);
|
|
1893
|
+
toRemove.forEach((c) => c.parentNode?.removeChild(c));
|
|
1894
|
+
}
|
|
1895
|
+
function replaceBrsInHeadings(doc) {
|
|
1896
|
+
doc.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
|
|
1897
|
+
h.querySelectorAll("br").forEach((br) => {
|
|
1898
|
+
const space = doc.createTextNode(" ");
|
|
1899
|
+
br.replaceWith(space);
|
|
1900
|
+
});
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
var UTILITY_VARIANT_RE = /:/;
|
|
1904
|
+
var UTILITY_PREFIX_RE = /^-?(?:p[xytblrse]?|m[xytblrse]?|gap|space-[xy]|w|h|min-w|min-h|max-w|max-h|size|basis|inset|top|right|bottom|left|start|end|z|text|bg|border|ring|shadow|outline|fill|stroke|divide|accent|caret|from|via|to|decoration|font|leading|tracking|indent|line-clamp|columns|aspect|object|opacity|rotate|scale|translate|skew|transition|duration|ease|delay|animate|rounded|overflow|overscroll|scroll|snap|touch|cursor|pointer-events|select|resize|flex|grid|col|row|order|auto-cols|auto-rows|items|justify|content|self|place|float|clear|list|whitespace|break|hyphens|mix-blend|bg-blend|backdrop|d|g|fs|fw|lh|align|position)-/i;
|
|
1905
|
+
var UTILITY_STANDALONE = /* @__PURE__ */ new Set([
|
|
1906
|
+
"flex",
|
|
1907
|
+
"grid",
|
|
1908
|
+
"block",
|
|
1909
|
+
"hidden",
|
|
1910
|
+
"inline",
|
|
1911
|
+
"inline-block",
|
|
1912
|
+
"inline-flex",
|
|
1913
|
+
"inline-grid",
|
|
1914
|
+
"contents",
|
|
1915
|
+
"flow-root",
|
|
1916
|
+
"list-item",
|
|
1917
|
+
"table",
|
|
1918
|
+
"container",
|
|
1919
|
+
"truncate",
|
|
1920
|
+
"grow",
|
|
1921
|
+
"shrink",
|
|
1922
|
+
"static",
|
|
1923
|
+
"relative",
|
|
1924
|
+
"absolute",
|
|
1925
|
+
"fixed",
|
|
1926
|
+
"sticky",
|
|
1927
|
+
"visible",
|
|
1928
|
+
"invisible",
|
|
1929
|
+
"collapse",
|
|
1930
|
+
"isolate",
|
|
1931
|
+
"underline",
|
|
1932
|
+
"overline",
|
|
1933
|
+
"line-through",
|
|
1934
|
+
"no-underline",
|
|
1935
|
+
"uppercase",
|
|
1936
|
+
"lowercase",
|
|
1937
|
+
"capitalize",
|
|
1938
|
+
"normal-case",
|
|
1939
|
+
"italic",
|
|
1940
|
+
"not-italic",
|
|
1941
|
+
"antialiased",
|
|
1942
|
+
"subpixel-antialiased",
|
|
1943
|
+
"sr-only",
|
|
1944
|
+
"not-sr-only",
|
|
1945
|
+
"clearfix",
|
|
1946
|
+
"row",
|
|
1947
|
+
"col"
|
|
1948
|
+
]);
|
|
1949
|
+
function isUtilityClass(token) {
|
|
1950
|
+
if (UTILITY_VARIANT_RE.test(token)) return true;
|
|
1951
|
+
const base = token.startsWith("-") ? token.slice(1) : token;
|
|
1952
|
+
if (UTILITY_STANDALONE.has(base)) return true;
|
|
1953
|
+
return UTILITY_PREFIX_RE.test(token);
|
|
1954
|
+
}
|
|
1955
|
+
function stripUtilityClasses(doc) {
|
|
1956
|
+
for (const el of doc.body.querySelectorAll("[class]")) {
|
|
1957
|
+
const kept = el.className.split(/\s+/).filter((t) => t && !isUtilityClass(t));
|
|
1958
|
+
if (kept.length === 0) el.removeAttribute("class");
|
|
1959
|
+
else el.className = kept.join(" ");
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function limitListsAndRows(doc, limit) {
|
|
1963
|
+
doc.querySelectorAll("ul, ol").forEach((list) => {
|
|
1964
|
+
const items = Array.from(list.children).filter((c) => c.tagName === "LI");
|
|
1965
|
+
for (let i = limit; i < items.length; i++) items[i].remove();
|
|
1966
|
+
});
|
|
1967
|
+
const rowContainers = doc.querySelectorAll("table, thead, tbody, tfoot");
|
|
1968
|
+
rowContainers.forEach((container) => {
|
|
1969
|
+
const rows = Array.from(container.children).filter((c) => c.tagName === "TR");
|
|
1970
|
+
for (let i = limit; i < rows.length; i++) rows[i].remove();
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1576
1974
|
// src/snapshot.ts
|
|
1577
|
-
async function snapshot(page) {
|
|
1975
|
+
async function snapshot(page, opts = {}) {
|
|
1578
1976
|
await sleep(500);
|
|
1579
1977
|
await waitForDomIdle(page);
|
|
1580
1978
|
const [url, html, file] = await Promise.all([page.url(), getContentWithMarkedHidden(page), screenshot(page)]);
|
|
1581
|
-
|
|
1979
|
+
const finalHtml = opts.dropUtilityClasses ? await realScrubHtml({ html, url }, {
|
|
1980
|
+
dropHidden: false,
|
|
1981
|
+
dropHead: false,
|
|
1982
|
+
dropSvg: false,
|
|
1983
|
+
pickMain: false,
|
|
1984
|
+
stripAttributes: 0,
|
|
1985
|
+
normalizeWhitespace: false,
|
|
1986
|
+
dropComments: false,
|
|
1987
|
+
replaceBrInHeadings: false,
|
|
1988
|
+
limitLists: -1,
|
|
1989
|
+
dropUtilityClasses: true
|
|
1990
|
+
}) : html;
|
|
1991
|
+
return { url, html: finalHtml, screenshot: file };
|
|
1582
1992
|
}
|
|
1583
1993
|
async function getContentWithMarkedHidden(page) {
|
|
1584
1994
|
try {
|
|
@@ -2934,8 +3344,12 @@ async function tryClick(page, selectors, _label) {
|
|
|
2934
3344
|
return false;
|
|
2935
3345
|
}
|
|
2936
3346
|
async function closeNativeJsAlerts(page) {
|
|
2937
|
-
page.on(
|
|
2938
|
-
|
|
3347
|
+
page.on(
|
|
3348
|
+
"dialog",
|
|
3349
|
+
/* v8 ignore next */
|
|
3350
|
+
(d) => d.accept().catch(() => {
|
|
3351
|
+
})
|
|
3352
|
+
);
|
|
2939
3353
|
}
|
|
2940
3354
|
async function sweepKnownCMPs(page, preferReject) {
|
|
2941
3355
|
for (const cmp of knownCMPSelectors) {
|
|
@@ -3014,6 +3428,7 @@ async function sweepOverlays(page, regex) {
|
|
|
3014
3428
|
const acceptRxSource = regex.accept.source;
|
|
3015
3429
|
const acceptRxFlags = regex.accept.flags;
|
|
3016
3430
|
return await page.evaluate(
|
|
3431
|
+
/* v8 ignore start */
|
|
3017
3432
|
([source, flags]) => {
|
|
3018
3433
|
const acceptRx = new RegExp(source, flags);
|
|
3019
3434
|
const isBig = (el) => {
|
|
@@ -3029,7 +3444,7 @@ async function sweepOverlays(page, regex) {
|
|
|
3029
3444
|
}).slice(0, 5);
|
|
3030
3445
|
for (const el of candidates) {
|
|
3031
3446
|
const btn = el.querySelector(
|
|
3032
|
-
|
|
3447
|
+
"[aria-label*=\u201Dclose\u201D i], button[aria-label*=\u201Dclose\u201D i], button:has(svg), .close, [data-close], .btn-close"
|
|
3033
3448
|
);
|
|
3034
3449
|
if (btn) {
|
|
3035
3450
|
btn.click();
|
|
@@ -3054,8 +3469,12 @@ async function sweepOverlays(page, regex) {
|
|
|
3054
3469
|
}
|
|
3055
3470
|
return false;
|
|
3056
3471
|
},
|
|
3472
|
+
/* v8 ignore stop */
|
|
3057
3473
|
[acceptRxSource, acceptRxFlags]
|
|
3058
|
-
).catch(
|
|
3474
|
+
).catch(
|
|
3475
|
+
/* v8 ignore next */
|
|
3476
|
+
() => false
|
|
3477
|
+
);
|
|
3059
3478
|
}
|
|
3060
3479
|
async function suppressInterferences(page, opts = {}) {
|
|
3061
3480
|
const timeoutMs = opts.timeoutMs ?? 4e3;
|
|
@@ -3096,258 +3515,17 @@ async function suppressInterferences(page, opts = {}) {
|
|
|
3096
3515
|
await sleep(pollIntervalMs);
|
|
3097
3516
|
}
|
|
3098
3517
|
}
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
return typeof page.content === "function" && typeof page.url === "function" && typeof page.screenshot === "function";
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
// src/scrub-html.ts
|
|
3106
|
-
var HTML_MIN_ATTR_THRESHOLD = 25e4;
|
|
3107
|
-
var HTML_LIMIT_LISTS_THRESHOLD = 4e5;
|
|
3108
|
-
var HTML_MAIN_ONLY_THRESHOLD = 6e5;
|
|
3109
|
-
function getDefaults(contentLength) {
|
|
3110
|
-
return {
|
|
3111
|
-
dropHidden: true,
|
|
3112
|
-
dropHead: true,
|
|
3113
|
-
dropSvg: false,
|
|
3114
|
-
pickMain: contentLength >= HTML_MAIN_ONLY_THRESHOLD,
|
|
3115
|
-
stripAttributes: contentLength >= HTML_MIN_ATTR_THRESHOLD ? 2 : 1,
|
|
3116
|
-
normalizeWhitespace: true,
|
|
3117
|
-
dropComments: true,
|
|
3118
|
-
replaceBrInHeadings: true,
|
|
3119
|
-
limitLists: contentLength >= HTML_LIMIT_LISTS_THRESHOLD ? 20 : -1
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
|
-
var ALLOWED_ATTRS = {
|
|
3123
|
-
match: /* @__PURE__ */ new Set([
|
|
3124
|
-
// identity/semantics
|
|
3125
|
-
"id",
|
|
3126
|
-
"class",
|
|
3127
|
-
"role",
|
|
3128
|
-
// internationalization
|
|
3129
|
-
"lang",
|
|
3130
|
-
"dir",
|
|
3131
|
-
// anchors & media
|
|
3132
|
-
"href",
|
|
3133
|
-
"title",
|
|
3134
|
-
"target",
|
|
3135
|
-
"rel",
|
|
3136
|
-
"src",
|
|
3137
|
-
"alt",
|
|
3138
|
-
"width",
|
|
3139
|
-
"height",
|
|
3140
|
-
"loading",
|
|
3141
|
-
// tables
|
|
3142
|
-
"scope",
|
|
3143
|
-
"headers",
|
|
3144
|
-
"colspan",
|
|
3145
|
-
"rowspan",
|
|
3146
|
-
// forms (pure semantics—doesn’t change structure)
|
|
3147
|
-
"name",
|
|
3148
|
-
"value",
|
|
3149
|
-
"type",
|
|
3150
|
-
"for",
|
|
3151
|
-
"placeholder",
|
|
3152
|
-
"checked",
|
|
3153
|
-
"selected",
|
|
3154
|
-
"multiple",
|
|
3155
|
-
"method",
|
|
3156
|
-
"action",
|
|
3157
|
-
// time, figure, etc.
|
|
3158
|
-
"datetime"
|
|
3159
|
-
]),
|
|
3160
|
-
regexp: /^aria-[\w-]+|^data-[\w-]+$/i
|
|
3161
|
-
// ARIA attributes & data-* attributes
|
|
3162
|
-
};
|
|
3163
|
-
var ALLOWED_ATTRS_AGGRESSIVE = {
|
|
3164
|
-
match: /* @__PURE__ */ new Set([
|
|
3165
|
-
// structuur / algemene selectors
|
|
3166
|
-
"id",
|
|
3167
|
-
"class",
|
|
3168
|
-
"role",
|
|
3169
|
-
// links / media
|
|
3170
|
-
"href",
|
|
3171
|
-
"src",
|
|
3172
|
-
"alt",
|
|
3173
|
-
"title",
|
|
3174
|
-
// tables
|
|
3175
|
-
"scope",
|
|
3176
|
-
// forms / velden
|
|
3177
|
-
"name",
|
|
3178
|
-
"type",
|
|
3179
|
-
"for",
|
|
3180
|
-
"placeholder",
|
|
3181
|
-
"value",
|
|
3182
|
-
"checked",
|
|
3183
|
-
"selected",
|
|
3184
|
-
// ARIA voor Playwright getByRole/getByLabel
|
|
3185
|
-
"aria-label",
|
|
3186
|
-
"aria-labelledby",
|
|
3187
|
-
"aria-describedby",
|
|
3188
|
-
// veelgebruikte test selectors
|
|
3189
|
-
"data-testid",
|
|
3190
|
-
"data-test-id",
|
|
3191
|
-
"data-cy",
|
|
3192
|
-
"data-qa"
|
|
3193
|
-
]),
|
|
3194
|
-
regexp: null
|
|
3195
|
-
};
|
|
3196
|
-
var HIDDEN_SELECTORS = [
|
|
3197
|
-
"[hidden]",
|
|
3198
|
-
"[inert]",
|
|
3199
|
-
'[aria-hidden="true"]',
|
|
3200
|
-
'[style*="display:none"]',
|
|
3201
|
-
'[style*="visibility:hidden"]',
|
|
3202
|
-
'[style*="opacity:0"]'
|
|
3203
|
-
].join(",");
|
|
3204
|
-
var ALWAYS_DROP = [
|
|
3205
|
-
"script",
|
|
3206
|
-
"style",
|
|
3207
|
-
"template",
|
|
3208
|
-
"noscript",
|
|
3209
|
-
"slot",
|
|
3210
|
-
"object",
|
|
3211
|
-
"embed"
|
|
3212
|
-
];
|
|
3213
|
-
async function scrubHtml(page, opts = {}) {
|
|
3214
|
-
if (isPage(page)) page = { html: await page.content(), url: page.url() };
|
|
3215
|
-
return await memoizedScrubHtml(page, opts);
|
|
3216
|
-
}
|
|
3217
|
-
var memoizedScrubHtml = memoize(realScrubHtml, {
|
|
3218
|
-
max: 16,
|
|
3219
|
-
ttl: 10 * 6e4,
|
|
3220
|
-
cacheKey: (args) => stringify({ html: args[0].html, url: args[0].url, ...args[1] })
|
|
3221
|
-
});
|
|
3222
|
-
async function realScrubHtml({ html, url }, opts = {}) {
|
|
3223
|
-
const o = { ...getDefaults(html.length), ...opts };
|
|
3224
|
-
const dom = new JSDOM(html, { url });
|
|
3225
|
-
const doc = dom.window.document;
|
|
3226
|
-
if (o.pickMain) pickMain(doc);
|
|
3227
|
-
dropInfraAndSvg(doc, !!o.dropSvg);
|
|
3228
|
-
if (o.dropHidden) dropHiddenTrees(doc);
|
|
3229
|
-
if (o.stripAttributes) stripAttributesAndSanitize(doc, o.stripAttributes);
|
|
3230
|
-
if (o.dropComments) dropHtmlComments(doc);
|
|
3231
|
-
if (o.replaceBrInHeadings) replaceBrsInHeadings(doc);
|
|
3232
|
-
if (o.limitLists >= 0) limitListsAndRows(doc, o.limitLists);
|
|
3233
|
-
if (o.normalizeWhitespace) normalizeWhitespace(doc.body);
|
|
3234
|
-
return doc.body.innerHTML;
|
|
3235
|
-
}
|
|
3236
|
-
function hasHiddenAncestor(el) {
|
|
3237
|
-
let p = el.parentElement;
|
|
3238
|
-
while (p) {
|
|
3239
|
-
if (p.hasAttribute("hidden") || p.hasAttribute("inert") || p.getAttribute("aria-hidden") === "true") return true;
|
|
3240
|
-
const style = p.getAttribute("style") || "";
|
|
3241
|
-
if (/\bdisplay\s*:\s*none\b/i.test(style)) return true;
|
|
3242
|
-
if (/\bvisibility\s*:\s*hidden\b/i.test(style)) return true;
|
|
3243
|
-
if (/\bopacity\s*:\s*0(?:\D|$)/i.test(style)) return true;
|
|
3244
|
-
p = p.parentElement;
|
|
3245
|
-
}
|
|
3246
|
-
return false;
|
|
3247
|
-
}
|
|
3248
|
-
function normalizeWhitespace(root) {
|
|
3249
|
-
const preLike = /* @__PURE__ */ new Set(["PRE", "CODE", "SAMP", "KBD"]);
|
|
3250
|
-
const doc = root.ownerDocument;
|
|
3251
|
-
const walker = doc.createTreeWalker(
|
|
3252
|
-
root,
|
|
3253
|
-
4
|
|
3254
|
-
/*NodeFilter.SHOW_TEXT*/
|
|
3255
|
-
);
|
|
3256
|
-
const changes = [];
|
|
3257
|
-
let node;
|
|
3258
|
-
while (node = walker.nextNode()) {
|
|
3259
|
-
const text = node;
|
|
3260
|
-
const parent = text.parentElement;
|
|
3261
|
-
if (!parent) continue;
|
|
3262
|
-
if (preLike.has(parent.tagName)) continue;
|
|
3263
|
-
const v = text.nodeValue ?? "";
|
|
3264
|
-
const collapsed = v.replace(/\s+/g, " ");
|
|
3265
|
-
if (collapsed !== v) changes.push(text);
|
|
3266
|
-
}
|
|
3267
|
-
for (const t of changes) {
|
|
3268
|
-
const parent = t.parentElement;
|
|
3269
|
-
const isBlockish = /^(P|LI|DIV|SECTION|ARTICLE|ASIDE|HEADER|FOOTER|MAIN|NAV|H[1-6]|BLOCKQUOTE|FIGCAPTION|TD|TH)$/i.test(parent.tagName);
|
|
3270
|
-
t.nodeValue = (t.nodeValue || "").replace(/\s+/g, " ");
|
|
3271
|
-
if (isBlockish) t.nodeValue = (t.nodeValue || "").trim();
|
|
3272
|
-
}
|
|
3273
|
-
}
|
|
3274
|
-
function pickMain(doc) {
|
|
3275
|
-
const main = doc.querySelector("main");
|
|
3276
|
-
if (!main) return false;
|
|
3277
|
-
const clone = main.cloneNode(true);
|
|
3278
|
-
doc.body.innerHTML = "";
|
|
3279
|
-
doc.body.appendChild(clone);
|
|
3280
|
-
return true;
|
|
3281
|
-
}
|
|
3282
|
-
function dropInfraAndSvg(doc, dropSvg) {
|
|
3283
|
-
const toDrop = [...ALWAYS_DROP, dropSvg ? "svg" : ""].filter(Boolean).join(",");
|
|
3284
|
-
if (!toDrop) return;
|
|
3285
|
-
doc.querySelectorAll(toDrop).forEach((el) => el.remove());
|
|
3286
|
-
}
|
|
3287
|
-
function dropHiddenTrees(doc) {
|
|
3288
|
-
doc.querySelectorAll(HIDDEN_SELECTORS).forEach((el) => el.remove());
|
|
3289
|
-
const all = [...doc.body.querySelectorAll("*")];
|
|
3290
|
-
for (const el of all) {
|
|
3291
|
-
if (!el.isConnected) continue;
|
|
3292
|
-
if (hasHiddenAncestor(el)) el.remove();
|
|
3293
|
-
}
|
|
3294
|
-
}
|
|
3295
|
-
function stripAttributesAndSanitize(doc, level) {
|
|
3296
|
-
if (!level) return;
|
|
3297
|
-
const all = [...doc.body.querySelectorAll("*")];
|
|
3298
|
-
for (const el of all) {
|
|
3299
|
-
const isSvg = el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
3300
|
-
for (const { name } of [...el.attributes]) {
|
|
3301
|
-
const lower = name.toLowerCase();
|
|
3302
|
-
if (lower.startsWith("on")) {
|
|
3303
|
-
el.removeAttribute(name);
|
|
3304
|
-
continue;
|
|
3305
|
-
}
|
|
3306
|
-
if (lower === "style") {
|
|
3307
|
-
el.removeAttribute(name);
|
|
3308
|
-
continue;
|
|
3309
|
-
}
|
|
3310
|
-
if (isSvg) continue;
|
|
3311
|
-
const allowed = level === 1 ? ALLOWED_ATTRS : ALLOWED_ATTRS_AGGRESSIVE;
|
|
3312
|
-
if (!allowed.match.has(lower) && !allowed.regexp?.test(name)) {
|
|
3313
|
-
el.removeAttribute(name);
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
doc.querySelectorAll("a[href]").forEach((a) => {
|
|
3318
|
-
const href = a.getAttribute("href") || "";
|
|
3319
|
-
if (/^\s*javascript:/i.test(href)) a.removeAttribute("href");
|
|
3320
|
-
});
|
|
3321
|
-
}
|
|
3322
|
-
function dropHtmlComments(doc) {
|
|
3323
|
-
const nf = doc.defaultView?.NodeFilter;
|
|
3324
|
-
const SHOW_COMMENT = nf?.SHOW_COMMENT ?? 128;
|
|
3325
|
-
const walker = doc.createTreeWalker(doc, SHOW_COMMENT);
|
|
3326
|
-
const toRemove = [];
|
|
3327
|
-
let n;
|
|
3328
|
-
while (n = walker.nextNode()) toRemove.push(n);
|
|
3329
|
-
toRemove.forEach((c) => c.parentNode?.removeChild(c));
|
|
3330
|
-
}
|
|
3331
|
-
function replaceBrsInHeadings(doc) {
|
|
3332
|
-
doc.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
|
|
3333
|
-
h.querySelectorAll("br").forEach((br) => {
|
|
3334
|
-
const space = doc.createTextNode(" ");
|
|
3335
|
-
br.replaceWith(space);
|
|
3336
|
-
});
|
|
3337
|
-
});
|
|
3518
|
+
async function format(rawHtml, url) {
|
|
3519
|
+
const html = await scrubHtml({ html: rawHtml, url });
|
|
3520
|
+
return await formatHtml(html);
|
|
3338
3521
|
}
|
|
3339
|
-
function
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
const rowContainers = doc.querySelectorAll("table, thead, tbody, tfoot");
|
|
3345
|
-
rowContainers.forEach((container) => {
|
|
3346
|
-
const rows = Array.from(container.children).filter((c) => c.tagName === "TR");
|
|
3347
|
-
for (let i = limit; i < rows.length; i++) rows[i].remove();
|
|
3348
|
-
});
|
|
3522
|
+
async function unifiedHtmlDiff(old, current) {
|
|
3523
|
+
if (isPage(old)) old = { html: await old.content(), url: old.url() };
|
|
3524
|
+
if (isPage(current)) current = { html: await current.content(), url: current.url() };
|
|
3525
|
+
const [a, b] = await Promise.all([format(old.html, old.url), format(current.html, current.url)]);
|
|
3526
|
+
return Diff.createTwoFilesPatch("before.html", "after.html", a, b);
|
|
3349
3527
|
}
|
|
3350
3528
|
|
|
3351
|
-
export { browse, createDateEngine, createFieldEngine, formatDate, formatDateForInput, formatHtml, fuzzyLocator, getMonthNames, realScrubHtml, screenshot, screenshotElement, scrollToCenter, scrubHtml, setFieldValue, snapshot, suppressInterferences, waitAfterInteraction, waitForAnimationsToFinish, waitForDomIdle, waitForIdle, waitForMeta, waitForUrlChange, waitUntilEnabled };
|
|
3529
|
+
export { browse, createDateEngine, createFieldEngine, formatDate, formatDateForInput, formatHtml, fuzzyLocator, getMonthNames, realScrubHtml, screenshot, screenshotElement, scrollToCenter, scrubHtml, setFieldValue, snapshot, suppressInterferences, unifiedHtmlDiff, waitAfterInteraction, waitForAnimationsToFinish, waitForDomIdle, waitForIdle, waitForMeta, waitForUrlChange, waitUntilEnabled };
|
|
3352
3530
|
//# sourceMappingURL=index.js.map
|
|
3353
3531
|
//# sourceMappingURL=index.js.map
|