@letsrunit/playwright 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letsrunit/playwright",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Playwright extensions and utilities for letsrunit",
5
5
  "keywords": [
6
6
  "testing",
@@ -38,13 +38,11 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "../../node_modules/.bin/tsup",
41
- "test": "vitest run",
42
- "test:cov": "vitest run --coverage",
43
41
  "typecheck": "tsc --noEmit"
44
42
  },
45
43
  "packageManager": "yarn@4.10.3",
46
44
  "dependencies": {
47
- "@letsrunit/utils": "0.6.0",
45
+ "@letsrunit/utils": "0.7.1",
48
46
  "@playwright/test": "^1.57.0",
49
47
  "case": "^1.6.3",
50
48
  "diff": "^8.0.3",
@@ -62,8 +60,7 @@
62
60
  "unified": "^11.0.5"
63
61
  },
64
62
  "devDependencies": {
65
- "@types/jsdom": "^27.0.0",
66
- "vitest": "^4.0.17"
63
+ "@types/jsdom": "^27.0.0"
67
64
  },
68
65
  "module": "./dist/index.js",
69
66
  "types": "./dist/index.d.ts",
package/src/browser.ts CHANGED
@@ -11,7 +11,8 @@ export async function browse(browser: Browser, options: BrowserContextOptions =
11
11
  });
12
12
 
13
13
  // Safety net against bundler-injected helpers inside page.evaluate
14
- await context.addInitScript(() => {
14
+ // v8 ignore next — callback runs in browser context, not Node
15
+ await context.addInitScript(/* v8 ignore next */ () => {
15
16
  // define __name as a no-op if present
16
17
  (window as any).__name = (window as any).__name || ((fn: any) => fn);
17
18
  });
@@ -3,13 +3,13 @@ import type { Loc, SetOptions, Value } from './types';
3
3
  export async function selectAria({ el }: Loc, value: Value, options?: SetOptions): Promise<boolean> {
4
4
  if (typeof value !== 'string' && typeof value !== 'number') return false;
5
5
 
6
- const role = await el.getAttribute('role', options).catch(() => null);
6
+ const role = await el.getAttribute('role', options).catch(/* v8 ignore next */ () => null);
7
7
  if (role !== 'combobox') return false;
8
8
 
9
- const ariaControls = await el.getAttribute('aria-controls', options).catch(() => null);
9
+ const ariaControls = await el.getAttribute('aria-controls', options).catch(/* v8 ignore next */ () => null);
10
10
  if (!ariaControls) return false;
11
11
 
12
- const ariaExpanded = await el.getAttribute('aria-expanded', options).catch(() => null);
12
+ const ariaExpanded = await el.getAttribute('aria-expanded', options).catch(/* v8 ignore next */ () => null);
13
13
  if (ariaExpanded !== 'true') await el.click(options);
14
14
 
15
15
  const stringValue = String(value);
@@ -52,9 +52,11 @@ export async function getCalendar(
52
52
  (await root.locator(gridSelector).count()) > 0 ? root : await getDialog(root, options);
53
53
 
54
54
  // Fallback: Check if the root itself is a valid grid (e.g. inline MUI DateCalendar)
55
+ /* v8 ignore start */
55
56
  if (!container && (await isCalendarGrid(root))) {
56
57
  return { calendar: root, tables: [root] };
57
58
  }
59
+ /* v8 ignore stop */
58
60
  if (!container) return null;
59
61
 
60
62
  // 2. Find all valid calendar grids within the container
@@ -63,9 +65,8 @@ export async function getCalendar(
63
65
  const tables: Locator[] = [];
64
66
 
65
67
  // If the container itself matches the selector, check it first
66
- if (await isCalendarGrid(container)) {
67
- tables.push(container);
68
- }
68
+ /* v8 ignore next */
69
+ if (await isCalendarGrid(container)) tables.push(container);
69
70
 
70
71
  for (const grid of found) {
71
72
  if (await isCalendarGrid(grid)) {
@@ -98,11 +99,7 @@ function inferYearForMonth(target: Date, month: number): number {
98
99
  }
99
100
 
100
101
  export async function getCurrentMonthsAndYears(root: Locator, target: Date): Promise<MonthYear[]> {
101
- const lang = await root
102
- .page()
103
- .locator('html')
104
- .getAttribute('lang')
105
- .catch(() => undefined);
102
+ const lang = await root.page().locator('html').getAttribute('lang').catch(() => undefined);
106
103
  const locales = lang && !lang.startsWith('en') ? [lang, 'en-US'] : ['en-US'];
107
104
 
108
105
  const text = await root.innerText();
@@ -187,12 +184,14 @@ async function navigateToMonth(
187
184
  const afterMonths = await getCurrentMonthsAndYears(root, target);
188
185
  if (afterMonths.some((m) => m.year * 12 + m.month === targetTotal)) return true;
189
186
 
187
+ /* v8 ignore start */
190
188
  if ((options?.retry ?? 0) < 3) {
191
189
  const retry = (options?.retry ?? 0) + 1;
192
190
  return await navigateToMonth(root, target, { ...options, wait: 50 + 50 * retry, retry }); // Retry max 3x
193
191
  }
194
192
 
195
193
  return false;
194
+ /* v8 ignore stop */
196
195
  }
197
196
 
198
197
  async function setDayByAriaLabel(table: Locator, value: Date, options?: SetOptions): Promise<boolean> {
@@ -210,10 +209,12 @@ async function setDayByAriaLabel(table: Locator, value: Date, options?: SetOptio
210
209
 
211
210
  if (cells.length === 0) return false;
212
211
 
212
+ /* v8 ignore start */
213
213
  if (cells.length === 1) {
214
214
  await cells[0].click(options);
215
215
  return true;
216
216
  }
217
+ /* v8 ignore stop */
217
218
 
218
219
  // Disambiguate using day 22 in case of dd/mm/yyyy vs mm/dd/yyyy
219
220
  const probeDate = new Date(value.getFullYear(), value.getMonth(), 22);
@@ -25,7 +25,7 @@ async function getCandidateLocs(el: Locator, options?: SetOptions): Promise<Cand
25
25
 
26
26
  return Promise.all(
27
27
  candidates.map(async (c) => {
28
- const info = await c.evaluate((node) => {
28
+ const info = await c.evaluate(/* v8 ignore start */ (node) => {
29
29
  const e = node as HTMLInputElement | HTMLSelectElement;
30
30
  const attrs: Record<string, string> = {};
31
31
  for (const attr of e.attributes) {
@@ -52,7 +52,7 @@ async function getCandidateLocs(el: Locator, options?: SetOptions): Promise<Cand
52
52
  inputMode: e.getAttribute('inputmode'),
53
53
  attrs,
54
54
  options,
55
- };
55
+ }; /* v8 ignore stop */
56
56
  }, options);
57
57
  return { el: c, ...info };
58
58
  }),
@@ -100,53 +100,53 @@ async function behavioralProbe(candidateLocs: CandidateInfo[], scores: Score[],
100
100
  for (let i = 0; i < candidateLocs.length; i++) {
101
101
  const loc = candidateLocs[i];
102
102
  if (loc.tag === 'input') {
103
- const can_be_day = await loc.el.evaluate((node) => {
103
+ const can_be_day = await loc.el.evaluate(/* v8 ignore start */ (node) => {
104
104
  const e = node as HTMLInputElement;
105
105
  const old = e.value;
106
106
  e.value = '31';
107
107
  const valid = e.checkValidity();
108
108
  e.value = old;
109
- return valid;
109
+ return valid; /* v8 ignore stop */
110
110
  }, options);
111
111
  if (can_be_day) scores[i].day += 1;
112
112
 
113
- const cannot_be_day = await loc.el.evaluate((node) => {
113
+ const cannot_be_day = await loc.el.evaluate(/* v8 ignore start */ (node) => {
114
114
  const e = node as HTMLInputElement;
115
115
  const old = e.value;
116
116
  e.value = '32';
117
117
  const valid = !e.checkValidity();
118
118
  e.value = old;
119
- return valid;
119
+ return valid; /* v8 ignore stop */
120
120
  }, options);
121
121
  if (cannot_be_day) scores[i].day += 1;
122
122
 
123
- const can_be_month = await loc.el.evaluate((node) => {
123
+ const can_be_month = await loc.el.evaluate(/* v8 ignore start */ (node) => {
124
124
  const e = node as HTMLInputElement;
125
125
  const old = e.value;
126
126
  e.value = '12';
127
127
  const valid = e.checkValidity();
128
128
  e.value = old;
129
- return valid;
129
+ return valid; /* v8 ignore stop */
130
130
  }, options);
131
131
  if (can_be_month) scores[i].month += 1;
132
132
 
133
- const cannot_be_month = await loc.el.evaluate((node) => {
133
+ const cannot_be_month = await loc.el.evaluate(/* v8 ignore start */ (node) => {
134
134
  const e = node as HTMLInputElement;
135
135
  const old = e.value;
136
136
  e.value = '13';
137
137
  const valid = !e.checkValidity();
138
138
  e.value = old;
139
- return valid;
139
+ return valid; /* v8 ignore stop */
140
140
  }, options);
141
141
  if (cannot_be_month) scores[i].month += 1;
142
142
 
143
- const can_be_year = await loc.el.evaluate((node) => {
143
+ const can_be_year = await loc.el.evaluate(/* v8 ignore start */ (node) => {
144
144
  const e = node as HTMLInputElement;
145
145
  const old = e.value;
146
146
  e.value = '2024';
147
147
  const valid = e.checkValidity();
148
148
  e.value = old;
149
- return valid;
149
+ return valid; /* v8 ignore stop */
150
150
  }, options);
151
151
  if (can_be_year) scores[i].year += 1;
152
152
  }
@@ -24,13 +24,13 @@ function buildDateString(date: Date, order: DateOrder, sep: string, pad: boolean
24
24
 
25
25
  function parseDateString(value: string, order: DateOrder, sep: string): Date | null {
26
26
  const raw = value.trim();
27
- if (!raw) return null;
27
+ /* v8 ignore next */ if (!raw) return null;
28
28
 
29
29
  const tokens = raw.split(sep);
30
- if (tokens.length !== 3) return null;
30
+ /* v8 ignore next */ if (tokens.length !== 3) return null;
31
31
 
32
32
  const nums = tokens.map((t) => Number(t));
33
- if (nums.some((n) => !Number.isFinite(n))) return null;
33
+ /* v8 ignore next */ if (nums.some((n) => !Number.isFinite(n))) return null;
34
34
 
35
35
  const map: Record<'day' | 'month' | 'year', number> = { day: 0, month: 0, year: 0 };
36
36
  for (let i = 0; i < 3; i++) map[order[i]] = nums[i];
@@ -39,11 +39,11 @@ function parseDateString(value: string, order: DateOrder, sep: string): Date | n
39
39
  const month = map.month;
40
40
  const day = map.day;
41
41
 
42
- if (month < 1 || month > 12) return null;
43
- if (day < 1 || day > 31) return null;
42
+ /* v8 ignore next */ if (month < 1 || month > 12) return null;
43
+ /* v8 ignore next */ if (day < 1 || day > 31) return null;
44
44
 
45
45
  const dt = new Date(year, month - 1, day);
46
- if (dt.getFullYear() !== year || dt.getMonth() !== month - 1 || dt.getDate() !== day) return null;
46
+ /* v8 ignore next */ if (dt.getFullYear() !== year || dt.getMonth() !== month - 1 || dt.getDate() !== day) return null;
47
47
 
48
48
  return dt;
49
49
  }
@@ -53,7 +53,7 @@ async function inferLocaleAndPattern(el: Locator, options?: SetOptions): Promise
53
53
  order: DateOrder;
54
54
  sep: string;
55
55
  }> {
56
- return el.evaluate(() => {
56
+ return el.evaluate(/* v8 ignore start */ () => {
57
57
  const lang = document.documentElement.getAttribute('lang') || navigator.language || 'en-US';
58
58
  const dtf = new Intl.DateTimeFormat(lang, { year: 'numeric', month: '2-digit', day: '2-digit' });
59
59
 
@@ -72,7 +72,7 @@ async function inferLocaleAndPattern(el: Locator, options?: SetOptions): Promise
72
72
  // Fallback if Intl returns something odd
73
73
  const finalOrder = order.length === 3 ? order : (['day', 'month', 'year'] as const);
74
74
 
75
- return { locale: lang, order: finalOrder as any, sep };
75
+ return { locale: lang, order: finalOrder as any, sep }; /* v8 ignore stop */
76
76
  }, options);
77
77
  }
78
78
 
@@ -83,11 +83,11 @@ async function fillAndReadBack(el: Locator, s: string, options?: SetOptions, nex
83
83
  if (nextInput) {
84
84
  await nextInput.focus(options);
85
85
  } else {
86
- await el.evaluate((el) => el.blur(), options);
86
+ await el.evaluate(/* v8 ignore next */ (el) => el.blur(), options);
87
87
  }
88
88
 
89
89
  // Some frameworks need a tiny tick.
90
- await el.evaluate(() => new Promise(requestAnimationFrame));
90
+ await el.evaluate(/* v8 ignore next */ () => new Promise(requestAnimationFrame));
91
91
 
92
92
  return await el.inputValue(options);
93
93
  }
@@ -199,8 +199,10 @@ async function setInputValue(
199
199
  value2?: Date,
200
200
  options?: SetOptions,
201
201
  ): Promise<boolean> {
202
+ /* v8 ignore start */
202
203
  if (await el.evaluate((el) => (el as HTMLInputElement).readOnly, options)) return false;
203
204
  if (el2 && (await el2.evaluate((el) => (el as HTMLInputElement).readOnly, options))) return false;
205
+ /* v8 ignore stop */
204
206
 
205
207
  const { combinations, localeSep } = await formatCombinations(el, options);
206
208
  let fallbackMatch: [DateOrder, string, boolean] | null = null;
@@ -234,10 +236,12 @@ async function setInputValue(
234
236
  if (fallbackMatch) {
235
237
  const [order, sep, pad] = fallbackMatch;
236
238
  const success = await setDateValue(el, value, order, sep, pad, options, el2);
239
+ /* v8 ignore start */
237
240
  if (el2 && value2) {
238
241
  const success2 = await setDateValue(el2, value2, order, sep, pad, options);
239
242
  return success && success2;
240
243
  }
244
+ /* v8 ignore stop */
241
245
  return success;
242
246
  }
243
247
 
@@ -46,15 +46,17 @@ export async function setFieldValue(el: Locator, value: Value, options?: SetOpti
46
46
  setFallback,
47
47
  );
48
48
 
49
+ /* v8 ignore start */
49
50
  if ((await el.count()) > 1) {
50
51
  el = await pickFieldElement(el);
51
52
  }
53
+ /* v8 ignore stop */
52
54
 
53
55
  const tag = await el.evaluate((e) => e.tagName.toLowerCase(), options);
54
56
  const type = await el
55
57
  .getAttribute('type', options)
56
58
  .then((s) => s && s.toLowerCase())
57
- .catch(() => null);
59
+ .catch(/* v8 ignore next */ () => null);
58
60
  const loc = { el, tag, type };
59
61
 
60
62
  await setValue(loc, value, options);
@@ -11,7 +11,7 @@ async function setSingleDate({ el, tag, type }: Loc, value: Date, options?: SetO
11
11
  'input[type=date], input[type=datetime-local], input[type=month], input[type=week], input[type=time]',
12
12
  );
13
13
  if ((await inputs.count()) === 1) {
14
- const isVisible = await inputs.evaluate((e) => {
14
+ const isVisible = await inputs.evaluate(/* v8 ignore next */ (e) => {
15
15
  const style = window.getComputedStyle(e);
16
16
  return style.display !== 'none' && style.visibility !== 'hidden' && e.getAttribute('type') !== 'hidden';
17
17
  });
@@ -28,7 +28,7 @@ export async function setRadioGroup(
28
28
  const ariaRadio = el.locator(`[role="radio"][value="${stringValue}"]`);
29
29
  if ((await ariaRadio.count()) >= 1) {
30
30
  const item = ariaRadio.first();
31
- const ariaChecked = await item.getAttribute('aria-checked', options).catch(() => null);
31
+ const ariaChecked = await item.getAttribute('aria-checked', options).catch(/* v8 ignore next */ () => null);
32
32
  if (ariaChecked !== 'true') await item.click(options);
33
33
  return true;
34
34
  }
@@ -37,7 +37,7 @@ export async function setRadioGroup(
37
37
  const ariaRadioByLabel = el.getByLabel(stringValue, { exact: true }).locator('[role="radio"]');
38
38
  if ((await ariaRadioByLabel.count()) >= 1) {
39
39
  const item = ariaRadioByLabel.first();
40
- const ariaChecked = await item.getAttribute('aria-checked', options).catch(() => null);
40
+ const ariaChecked = await item.getAttribute('aria-checked', options).catch(/* v8 ignore next */ () => null);
41
41
  if (ariaChecked !== 'true') await item.click(options);
42
42
  return true;
43
43
  }
@@ -3,10 +3,10 @@ import type { Loc, SetOptions, Value } from './types';
3
3
  export async function setToggle({ el }: Loc, value: Value, options?: SetOptions): Promise<boolean> {
4
4
  if (typeof value !== 'boolean' && value !== null) return false;
5
5
 
6
- const role = await el.getAttribute('role', options).catch(() => null);
6
+ const role = await el.getAttribute('role', options).catch(/* v8 ignore next */ () => null);
7
7
  if (role !== 'checkbox' && role !== 'switch') return false;
8
8
 
9
- const ariaChecked = await el.getAttribute('aria-checked', options).catch(() => null);
9
+ const ariaChecked = await el.getAttribute('aria-checked', options).catch(/* v8 ignore next */ () => null);
10
10
  const isChecked = ariaChecked === 'true';
11
11
 
12
12
  if (Boolean(value) !== isChecked) await el.click(options);
@@ -1,31 +1,26 @@
1
1
  import { Locator, Page } from '@playwright/test';
2
2
 
3
- type LocatorOptions = Parameters<Page['locator']>[1];
4
-
5
3
  /**
6
- * Locates an element using Playwright selectors, with fallbacks.
4
+ * Locates an element using Playwright selectors, with lazy fallbacks.
7
5
  */
8
6
  export async function fuzzyLocator(page: Page, selector: string): Promise<Locator> {
9
- const primary = page.locator(selector).first();
10
- if (await primary.count()) return primary;
7
+ const primary = page.locator(selector);
8
+ const candidates = [
9
+ tryRelaxNameToHasText(page, selector),
10
+ tryTagInsteadOfRole(page, selector),
11
+ tryRoleNameProximity(page, selector),
12
+ tryFieldAlternative(page, selector),
13
+ tryAsField(page, selector),
14
+ ];
11
15
 
12
- return (
13
- (await tryRelaxNameToHasText(page, selector)) ||
14
- (await tryTagInsteadOfRole(page, selector)) ||
15
- (await tryRoleNameProximity(page, selector)) ||
16
- (await tryFieldAlternative(page, selector)) ||
17
- (await tryAsField(page, selector)) ||
18
- primary
19
- ); // Nothing found, return the original locator (so caller can still wait/assert)
20
- }
16
+ let combined = primary;
21
17
 
22
- async function firstMatch(page: Page, sel: string | string[], opts: LocatorOptions = {}): Promise<Locator | null> {
23
- for (const selector of Array.isArray(sel) ? sel : [sel]) {
24
- const loc = page.locator(selector, opts).first();
25
- if (await loc.count()) return loc;
18
+ for (const candidate of candidates) {
19
+ if (!candidate) continue;
20
+ combined = combined.or(candidate);
26
21
  }
27
22
 
28
- return null;
23
+ return combined.first();
29
24
  }
30
25
 
31
26
  // Preserve the selector but relax [name="..."] to [has-text="..."]
@@ -34,51 +29,51 @@ async function firstMatch(page: Page, sel: string | string[], opts: LocatorOptio
34
29
  // - role=link[name="Foo"] → role=link:has-text="Foo"
35
30
  // - css=button[name="Save"i]:visible → css=button:visible:has-text="Save"
36
31
  // - [name="Hello"] → :has-text="Hello"
37
- async function tryRelaxNameToHasText(page: Page, selector: string): Promise<Locator | null> {
32
+ function tryRelaxNameToHasText(page: Page, selector: string): Locator | null {
38
33
  const matchAnyNameFull = selector.match(/^(role=.*)\[name="([^"]+)"i?](.*)$/i);
39
34
  if (!matchAnyNameFull) return null;
40
35
  const [, pre, nameText, post] = matchAnyNameFull;
41
36
  const containsSelector = `${pre}${post}`;
42
- return firstMatch(page, containsSelector, { hasText: nameText });
37
+ return page.locator(containsSelector, { hasText: nameText });
43
38
  }
44
39
 
45
40
  // Try using the tag name for `link`, `button` and `option` instead fo the aria role.
46
41
  // This keeps all other parts of the selector intact (prefix/suffix, additional filters).
47
42
  // Examples:
48
43
  // - role=button[name="Foo"] → css=button:has-text="Save"
49
- async function tryTagInsteadOfRole(page: Page, selector: string): Promise<Locator | null> {
44
+ function tryTagInsteadOfRole(page: Page, selector: string): Locator | null {
50
45
  const matchAnyNameFull = selector.match(/^role=(link|button|option)\s*\[name="([^"]+)"i?](.*)$/i);
51
46
  if (!matchAnyNameFull) return null;
52
47
  const [, role, nameText, post] = matchAnyNameFull;
53
48
  const tag = role === 'link' ? 'a' : role;
54
49
  const containsSelector = `css=${tag}${post}`;
55
- return firstMatch(page, containsSelector, { hasText: nameText });
50
+ return page.locator(containsSelector, { hasText: nameText });
56
51
  }
57
52
 
58
53
  // If a role selector with a name filter fails, try proximity-based fallback while
59
54
  // preserving the role and any remainder of the selector.
60
55
  // Example: role=switch[name="Adres tonen"i] → text=Adres tonen >> .. >> role=switch
61
- async function tryRoleNameProximity(page: Page, selector: string): Promise<Locator | null> {
56
+ function tryRoleNameProximity(page: Page, selector: string): Locator | null {
62
57
  const matchRole = selector.match(/^role=(\w+)\s*\[name="([^"]+)"i?](.*)$/i);
63
58
  if (!matchRole) return null;
64
59
  const [, role, name, rest] = matchRole;
65
60
  const proximitySelector = `text=${name} >> .. >> role=${role}${rest}`;
66
- return firstMatch(page, proximitySelector);
61
+ return page.locator(proximitySelector);
67
62
  }
68
63
 
69
64
  // Try alternatives if field is not found
70
65
  // field="foo" → #foo > input (only when name is a valid CSS identifier)
71
- async function tryFieldAlternative(page: Page, selector: string): Promise<Locator | null> {
66
+ function tryFieldAlternative(page: Page, selector: string): Locator | null {
72
67
  const matchField = selector.match(/^field="([^"]+)"i?$/i);
73
68
  if (!matchField) return null;
74
69
  const [, field] = matchField;
75
70
  // Skip if the name contains characters invalid in a CSS ID selector
76
71
  if (!/^[a-zA-Z0-9_-]+$/.test(field)) return null;
77
- return firstMatch(page, `#${field} > input`);
72
+ return page.locator(`#${field} > input`);
78
73
  }
79
74
 
80
75
  // Try matching using the field selector in case of role mismatch
81
- async function tryAsField(page: Page, selector: string): Promise<Locator | null> {
76
+ function tryAsField(page: Page, selector: string): Locator | null {
82
77
  const matchRole = selector.match(/^role=(\w+)\s*\[name="([^"]+)"i?](.*)$/i);
83
78
  if (!matchRole) return null;
84
79
 
@@ -102,5 +97,5 @@ async function tryAsField(page: Page, selector: string): Promise<Locator | null>
102
97
 
103
98
  if (!fieldRoles.has(role.toLowerCase())) return null;
104
99
 
105
- return firstMatch(page, `field=${name}${rest}`);
100
+ return page.locator(`field=${name}${rest}`);
106
101
  }
package/src/scrub-html.ts CHANGED
@@ -193,11 +193,8 @@ export async function realScrubHtml(
193
193
  function hasHiddenAncestor(el: Element): boolean {
194
194
  let p: Element | null = el.parentElement;
195
195
  while (p) {
196
- if (
197
- p.hasAttribute('hidden') ||
198
- p.hasAttribute('inert') ||
199
- p.getAttribute('aria-hidden') === 'true'
200
- ) return true;
196
+ /* v8 ignore next */
197
+ if (p.hasAttribute('hidden') || p.hasAttribute('inert') || p.getAttribute('aria-hidden') === 'true') return true;
201
198
 
202
199
  const style = p.getAttribute('style') || '';
203
200
  if (/\bdisplay\s*:\s*none\b/i.test(style)) return true;
@@ -79,7 +79,7 @@ async function tryClick(page: Page, selectors: string[], _label: string) {
79
79
  }
80
80
 
81
81
  async function closeNativeJsAlerts(page: Page) {
82
- page.on('dialog', (d) => d.accept().catch(() => {}));
82
+ page.on('dialog', /* v8 ignore next */ (d) => d.accept().catch(() => {}));
83
83
  }
84
84
 
85
85
  // 1) Known cookie CMPs (quick wins)
@@ -155,6 +155,7 @@ async function sweepNewsletter(page: Page, regex: TrRegExps): Promise<boolean> {
155
155
  } catch {}
156
156
  }
157
157
  // click outside the modal (overlay)
158
+ /* v8 ignore start */
158
159
  const overlay = page.locator('div[role="presentation"], .modal-backdrop, .overlay, .ReactModal__Overlay');
159
160
  if (await overlay.count().catch(() => 0)) {
160
161
  try {
@@ -162,6 +163,7 @@ async function sweepNewsletter(page: Page, regex: TrRegExps): Promise<boolean> {
162
163
  return true;
163
164
  } catch {}
164
165
  }
166
+ /* v8 ignore stop */
165
167
  }
166
168
  return false;
167
169
  }
@@ -173,6 +175,7 @@ async function sweepOverlays(page: Page, regex: TrRegExps): Promise<boolean> {
173
175
 
174
176
  return await page
175
177
  .evaluate(
178
+ /* v8 ignore start */
176
179
  ([source, flags]) => {
177
180
  const acceptRx = new RegExp(source, flags);
178
181
  const isBig = (el: Element) => {
@@ -192,7 +195,7 @@ async function sweepOverlays(page: Page, regex: TrRegExps): Promise<boolean> {
192
195
  for (const el of candidates) {
193
196
  // try a close button inside
194
197
  const btn = el.querySelector<HTMLElement>(
195
- '[aria-label*="close" i], button[aria-label*="close" i], button:has(svg), .close, [data-close], .btn-close',
198
+ '[aria-label*=”close i], button[aria-label*=”close i], button:has(svg), .close, [data-close], .btn-close',
196
199
  );
197
200
 
198
201
  if (btn) {
@@ -221,9 +224,10 @@ async function sweepOverlays(page: Page, regex: TrRegExps): Promise<boolean> {
221
224
  }
222
225
  return false;
223
226
  },
227
+ /* v8 ignore stop */
224
228
  [acceptRxSource, acceptRxFlags],
225
229
  )
226
- .catch(() => false);
230
+ .catch(/* v8 ignore next */ () => false);
227
231
  }
228
232
 
229
233
  export async function suppressInterferences(page: Page, opts: Options = {}) {
@@ -264,7 +268,9 @@ export async function suppressInterferences(page: Page, opts: Options = {}) {
264
268
 
265
269
  // Optional: bail if we are hitting a loop of re-spawning popups
266
270
  if (actions >= maxActions) {
271
+ /* v8 ignore start */
267
272
  if (opts.verbose) console.log(`[suppressInterferences] Max actions ${maxActions} reached, stopping.`);
273
+ /* v8 ignore stop */
268
274
  break;
269
275
  }
270
276
 
@@ -278,7 +284,9 @@ export async function suppressInterferences(page: Page, opts: Options = {}) {
278
284
  const ranFor = Date.now() - startedAt;
279
285
 
280
286
  if (quietFor >= quietPeriodMs && ranFor >= minSweepMs) {
287
+ /* v8 ignore start */
281
288
  if (opts.verbose) console.log(`[suppressInterferences] Quiet for ${quietFor}ms, stopping early.`);
289
+ /* v8 ignore stop */
282
290
  break;
283
291
  }
284
292
 
package/src/wait.ts CHANGED
@@ -15,6 +15,7 @@ export async function waitForMeta(page: Page, timeout = 2500) {
15
15
 
16
16
  await page
17
17
  .waitForFunction(
18
+ /* v8 ignore start */
18
19
  () => {
19
20
  const head = document.head;
20
21
  if (!head) return false;
@@ -26,6 +27,7 @@ export async function waitForMeta(page: Page, timeout = 2500) {
26
27
  head.querySelector('script[type="application/ld+json"]'),
27
28
  );
28
29
  },
30
+ /* v8 ignore stop */
29
31
  { timeout },
30
32
  )
31
33
  .catch(() => {});
@@ -37,6 +39,7 @@ export async function waitForDomIdle(
37
39
  { quiet = 500, timeout = 10_000 }: { quiet?: number; timeout?: number } = {},
38
40
  ) {
39
41
  await page.waitForFunction(
42
+ /* v8 ignore start */
40
43
  (q) =>
41
44
  new Promise<boolean>((resolve) => {
42
45
  let last = performance.now();
@@ -59,6 +62,7 @@ export async function waitForDomIdle(
59
62
  };
60
63
  tick();
61
64
  }),
65
+ /* v8 ignore stop */
62
66
  quiet,
63
67
  { timeout },
64
68
  );
@@ -68,19 +72,22 @@ export async function waitForDomIdle(
68
72
  // This helps with libraries that slide months using WAAPI (common in React UI libs).
69
73
  export async function waitForAnimationsToFinish(root: Locator) {
70
74
  await root.page().waitForFunction(
75
+ /* v8 ignore start */
71
76
  (el) => {
72
77
  const animations = (el as HTMLElement).getAnimations?.({ subtree: true }) ?? [];
73
78
  return animations.every((a) => a.playState !== 'running');
74
79
  },
80
+ /* v8 ignore stop */
75
81
  await root.elementHandle(),
76
82
  );
77
83
 
84
+ /* v8 ignore next */
78
85
  await root.evaluate(() => new Promise<void>((r) => requestAnimationFrame(() => requestAnimationFrame(() => r()))));
79
86
  }
80
87
 
81
88
  export async function waitForUrlChange(page: Page, prevUrl: string, timeout: number) {
82
89
  try {
83
- await page.waitForFunction((u) => location.href !== u, prevUrl, { timeout });
90
+ await page.waitForFunction(/* v8 ignore next */ (u) => location.href !== u, prevUrl, { timeout });
84
91
  return true;
85
92
  } catch {
86
93
  return false;
@@ -95,6 +102,7 @@ export async function waitUntilEnabled(page: Page, target: Locator, timeout: num
95
102
 
96
103
  await page
97
104
  .waitForFunction(
105
+ /* v8 ignore start */
98
106
  (el) => {
99
107
  if (!el || !(el as Element).isConnected) return true; // detached → treat as settled
100
108
  const aria = (el as HTMLElement).getAttribute('aria-disabled');
@@ -104,6 +112,7 @@ export async function waitUntilEnabled(page: Page, target: Locator, timeout: num
104
112
  (el as HTMLElement).getAttribute('disabled') !== null;
105
113
  return !disabled;
106
114
  },
115
+ /* v8 ignore stop */
107
116
  handle,
108
117
  { timeout },
109
118
  )
@@ -158,7 +167,7 @@ async function elementKind(target: Locator): Promise<'link' | 'button' | 'other'
158
167
  if (role === 'link') return 'link';
159
168
  if (role === 'button') return 'button';
160
169
 
161
- const tag = await target.evaluate((el) => el.tagName.toLowerCase(), null, { timeout: PROBE }).catch(() => '');
170
+ const tag = await target.evaluate(/* v8 ignore next */ (el) => el.tagName.toLowerCase(), null, { timeout: PROBE }).catch(() => '');
162
171
  if (tag === 'a') return 'link';
163
172
 
164
173
  if (tag === 'button') return 'button';