@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.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
- window.__name = window.__name || ((fn) => fn);
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, format) {
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 format.replace("DD", dd).replace("MM", mm).replace("YYYY", yyyy);
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((u) => location.href !== u, prevUrl, { timeout });
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((el) => el.tagName.toLowerCase(), null, { timeout: PROBE }).catch(() => "");
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((node) => {
471
- const e = node;
472
- const attrs = {};
473
- for (const attr of e.attributes) {
474
- attrs[attr.name] = attr.value;
475
- }
476
- let options2 = [];
477
- if (e.tagName.toLowerCase() === "select") {
478
- options2 = Array.from(e.options).map((o) => ({
479
- value: o.value,
480
- text: o.text
481
- }));
482
- }
483
- return {
484
- tag: e.tagName.toLowerCase(),
485
- type: e.getAttribute("type"),
486
- name: e.getAttribute("name"),
487
- id: e.getAttribute("id"),
488
- ariaLabel: e.getAttribute("aria-label"),
489
- placeholder: e.getAttribute("placeholder"),
490
- min: e.getAttribute("min"),
491
- max: e.getAttribute("max"),
492
- inputMode: e.getAttribute("inputmode"),
493
- attrs,
494
- options: options2
495
- };
496
- }, options);
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((node) => {
533
- const e = node;
534
- const old = e.value;
535
- e.value = "31";
536
- const valid = e.checkValidity();
537
- e.value = old;
538
- return valid;
539
- }, options);
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((node) => {
542
- const e = node;
543
- const old = e.value;
544
- e.value = "32";
545
- const valid = !e.checkValidity();
546
- e.value = old;
547
- return valid;
548
- }, options);
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((node) => {
551
- const e = node;
552
- const old = e.value;
553
- e.value = "12";
554
- const valid = e.checkValidity();
555
- e.value = old;
556
- return valid;
557
- }, options);
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((node) => {
560
- const e = node;
561
- const old = e.value;
562
- e.value = "13";
563
- const valid = !e.checkValidity();
564
- e.value = old;
565
- return valid;
566
- }, options);
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((node) => {
569
- const e = node;
570
- const old = e.value;
571
- e.value = "2024";
572
- const valid = e.checkValidity();
573
- e.value = old;
574
- return valid;
575
- }, options);
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
- const lang = document.documentElement.getAttribute("lang") || navigator.language || "en-US";
693
- const dtf = new Intl.DateTimeFormat(lang, { year: "numeric", month: "2-digit", day: "2-digit" });
694
- const parts = dtf.formatToParts(new Date(2033, 10, 22));
695
- const order = [];
696
- let sep = "/";
697
- for (const p of parts) {
698
- if (p.type === "day" || p.type === "month" || p.type === "year") order.push(p.type);
699
- if (p.type === "literal") {
700
- const lit = p.value.trim();
701
- if (lit) sep = lit;
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
- const finalOrder = order.length === 3 ? order : ["day", "month", "year"];
705
- return { locale: lang, order: finalOrder, sep };
706
- }, options);
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((el2) => el2.blur(), options);
762
+ await el.evaluate(
763
+ /* v8 ignore next */
764
+ (el2) => el2.blur(),
765
+ options
766
+ );
715
767
  }
716
- await el.evaluate(() => new Promise(requestAnimationFrame));
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((e) => {
857
- const style = window.getComputedStyle(e);
858
- return style.display !== "none" && style.visibility !== "hidden" && e.getAttribute("type") !== "hidden";
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(() => null);
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(() => null);
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(() => null);
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(() => null);
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(() => null);
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(() => null);
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(() => null);
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(() => null);
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).first();
1150
- if (await primary.count()) return primary;
1151
- return await tryRelaxNameToHasText(page, selector) || await tryTagInsteadOfRole(page, selector) || await tryRoleNameProximity(page, selector) || await tryFieldAlternative(page, selector) || await tryAsField(page, selector) || primary;
1152
- }
1153
- async function firstMatch(page, sel, opts = {}) {
1154
- for (const selector of Array.isArray(sel) ? sel : [sel]) {
1155
- const loc = page.locator(selector, opts).first();
1156
- if (await loc.count()) return loc;
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 null;
1244
+ return combined.first();
1159
1245
  }
1160
- async function tryRelaxNameToHasText(page, selector) {
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 firstMatch(page, containsSelector, { hasText: nameText });
1251
+ return page.locator(containsSelector, { hasText: nameText });
1166
1252
  }
1167
- async function tryTagInsteadOfRole(page, selector) {
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 firstMatch(page, containsSelector, { hasText: nameText });
1259
+ return page.locator(containsSelector, { hasText: nameText });
1174
1260
  }
1175
- async function tryRoleNameProximity(page, selector) {
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 firstMatch(page, proximitySelector);
1266
+ return page.locator(proximitySelector);
1181
1267
  }
1182
- async function tryFieldAlternative(page, selector) {
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 firstMatch(page, `#${field} > input`);
1273
+ return page.locator(`#${field} > input`);
1188
1274
  }
1189
- async function tryAsField(page, selector) {
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 firstMatch(page, `field=${name}${rest}`);
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
- return { url, html, screenshot: file };
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("dialog", (d) => d.accept().catch(() => {
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
- '[aria-label*="close" i], button[aria-label*="close" i], button:has(svg), .close, [data-close], .btn-close'
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(() => false);
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
- // src/utils/type-check.ts
3101
- function isPage(page) {
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 limitListsAndRows(doc, limit) {
3340
- doc.querySelectorAll("ul, ol").forEach((list) => {
3341
- const items = Array.from(list.children).filter((c) => c.tagName === "LI");
3342
- for (let i = limit; i < items.length; i++) items[i].remove();
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