@lullabot/playwright-testing 1.0.0 → 1.1.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.
Files changed (68) hide show
  1. package/bin/github-a11y-summary +2 -2
  2. package/bin/github-failure-summary +4 -4
  3. package/lib/accessibility-baseline-file.d.ts +6 -6
  4. package/lib/accessibility-baseline-file.js +29 -23
  5. package/lib/accessibility-baseline.js +5 -3
  6. package/lib/accessible-screenshot.d.ts +4 -4
  7. package/lib/accessible-screenshot.js +110 -88
  8. package/lib/focus.js +4 -1
  9. package/lib/frames.js +4 -4
  10. package/lib/github/a11y-summary.js +74 -79
  11. package/lib/github/attachments.d.ts +1 -1
  12. package/lib/github/attachments.js +36 -34
  13. package/lib/github/failure-summary.d.ts +4 -4
  14. package/lib/github/failure-summary.js +140 -108
  15. package/lib/github/index.d.ts +6 -6
  16. package/lib/github/report-paths.js +17 -9
  17. package/lib/hover.js +17 -17
  18. package/lib/images.js +15 -12
  19. package/lib/index.d.ts +13 -13
  20. package/lib/interaction-states.d.ts +2 -2
  21. package/lib/interaction-states.js +7 -7
  22. package/lib/mock/index.d.ts +1 -1
  23. package/lib/mock/youtube.d.ts +1 -1
  24. package/lib/mock/youtube.js +1 -1
  25. package/lib/pseudo-state.d.ts +2 -2
  26. package/lib/pseudo-state.js +15 -7
  27. package/lib/videos.js +21 -10
  28. package/lib/visualdiff.d.ts +4 -4
  29. package/lib/visualdiff.js +35 -27
  30. package/package.json +3 -3
  31. package/src/accessibility-baseline-file.test.ts +190 -154
  32. package/src/accessibility-baseline-file.ts +107 -83
  33. package/src/accessibility-baseline.test.ts +869 -433
  34. package/src/accessibility-baseline.ts +41 -21
  35. package/src/accessible-screenshot.test.ts +551 -423
  36. package/src/accessible-screenshot.ts +358 -251
  37. package/src/focus.test.ts +23 -23
  38. package/src/focus.ts +7 -2
  39. package/src/fonts.test.ts +13 -13
  40. package/src/fonts.ts +1 -1
  41. package/src/frames.test.ts +10 -8
  42. package/src/frames.ts +5 -5
  43. package/src/github/a11y-summary.test.ts +436 -332
  44. package/src/github/a11y-summary.ts +189 -179
  45. package/src/github/attachments.test.ts +228 -167
  46. package/src/github/attachments.ts +144 -121
  47. package/src/github/failure-summary.test.ts +660 -445
  48. package/src/github/failure-summary.ts +376 -274
  49. package/src/github/index.test.ts +14 -14
  50. package/src/github/index.ts +6 -6
  51. package/src/github/report-paths.test.ts +211 -156
  52. package/src/github/report-paths.ts +75 -61
  53. package/src/hover.test.ts +50 -46
  54. package/src/hover.ts +18 -18
  55. package/src/images.test.ts +251 -206
  56. package/src/images.ts +52 -43
  57. package/src/index.ts +13 -13
  58. package/src/interaction-states.test.ts +72 -42
  59. package/src/interaction-states.ts +43 -29
  60. package/src/mock/index.ts +1 -1
  61. package/src/mock/youtube.test.ts +26 -26
  62. package/src/mock/youtube.ts +4 -6
  63. package/src/pseudo-state.test.ts +58 -51
  64. package/src/pseudo-state.ts +32 -27
  65. package/src/videos.test.ts +559 -399
  66. package/src/videos.ts +68 -44
  67. package/src/visualdiff.test.ts +416 -315
  68. package/src/visualdiff.ts +140 -99
@@ -29,16 +29,19 @@ let a11yActionHintShown = false;
29
29
  * @param options Accessibility options to customise the scan.
30
30
  */
31
31
  async function checkAccessibility(page, testInfo, options) {
32
- const { wcagTags = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'], exclude = [], bestPracticeExclude = [], wcagExclude = [], bestPracticeMode = 'soft', rules, baseline, screenshotViolations = true, } = options ?? {};
32
+ const { wcagTags = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"], exclude = [], bestPracticeExclude = [], wcagExclude = [], bestPracticeMode = "soft", rules, baseline, screenshotViolations = true, } = options ?? {};
33
33
  if (process.env.CI && !a11yActionHintShown) {
34
- console.log('Tip: Use @lullabot/playwright-testing/github to surface accessibility violations in pull requests.');
34
+ console.log("Tip: Use @lullabot/playwright-testing/github to surface accessibility violations in pull requests.");
35
35
  a11yActionHintShown = true;
36
36
  }
37
37
  // Add @a11y annotation (deduplicated).
38
- if (!testInfo.annotations.some(a => a.type === '@a11y')) {
39
- testInfo.annotations.push({ type: '@a11y' });
38
+ if (!testInfo.annotations.some((a) => a.type === "@a11y")) {
39
+ testInfo.annotations.push({ type: "@a11y" });
40
40
  }
41
- if (bestPracticeMode !== 'off') {
41
+ // An in-code baseline is shared by both scans. Track matches across this
42
+ // check so that a rule detected by either scan is not reported as stale.
43
+ const sharedBaselineMatches = new Set();
44
+ if (bestPracticeMode !== "off") {
42
45
  const bpResults = await runBestPracticeScan(page, testInfo, {
43
46
  exclude: [...exclude, ...bestPracticeExclude],
44
47
  rules,
@@ -49,10 +52,10 @@ async function checkAccessibility(page, testInfo, options) {
49
52
  await dispatchAssertion({
50
53
  testInfo,
51
54
  results: bpResults,
52
- scan: 'best-practice',
55
+ scan: "best-practice",
53
56
  expectFn: test_1.expect.soft,
54
- scanLabel: 'Best-practice scan',
55
- }, baseline);
57
+ scanLabel: "Best-practice scan",
58
+ }, baseline, sharedBaselineMatches);
56
59
  }
57
60
  const wcagScanResults = await runWcagScan(page, testInfo, {
58
61
  wcagTags,
@@ -65,10 +68,10 @@ async function checkAccessibility(page, testInfo, options) {
65
68
  await dispatchAssertion({
66
69
  testInfo,
67
70
  results: wcagScanResults,
68
- scan: 'wcag',
71
+ scan: "wcag",
69
72
  expectFn: test_1.expect,
70
- scanLabel: 'WCAG scan',
71
- }, baseline);
73
+ scanLabel: "WCAG scan",
74
+ }, baseline, sharedBaselineMatches);
72
75
  }
73
76
  /**
74
77
  * Dispatch the assertion for one scan to the correct mode:
@@ -85,15 +88,15 @@ async function checkAccessibility(page, testInfo, options) {
85
88
  * test (matching Playwright's missing-snapshot behaviour); locally,
86
89
  * seeding passes so the first run is green.
87
90
  */
88
- async function dispatchAssertion(ctx, inCodeBaseline) {
91
+ async function dispatchAssertion(ctx, inCodeBaseline, sharedBaselineMatches) {
89
92
  if (inCodeBaseline) {
90
- return assertBaseline(ctx, inCodeBaseline);
93
+ return assertBaseline(ctx, inCodeBaseline, sharedBaselineMatches);
91
94
  }
92
95
  if (await (0, accessibility_baseline_file_js_1.snapshotExists)(ctx.testInfo)) {
93
96
  return assertSnapshot(ctx);
94
97
  }
95
98
  const update = ctx.testInfo.config?.updateSnapshots;
96
- if (update === 'all' || update === 'changed') {
99
+ if (update === "all" || update === "changed") {
97
100
  return assertSnapshot(ctx);
98
101
  }
99
102
  const callCount = (0, accessibility_baseline_file_js_1.nextAccessibilityScanCount)(ctx.testInfo, ctx.scan);
@@ -104,25 +107,25 @@ async function dispatchAssertion(ctx, inCodeBaseline) {
104
107
  }
105
108
  // Seed and either pass (local) or fail (CI).
106
109
  const normalized = extractNormalizedViolations(ctx.results);
107
- const seedViolations = normalized.map(v => ({
110
+ const seedViolations = normalized.map((v) => ({
108
111
  rule: v.rule,
109
112
  targets: v.targets,
110
- reason: 'TODO',
111
- willBeFixedIn: 'TODO',
113
+ reason: "TODO",
114
+ willBeFixedIn: "TODO",
112
115
  }));
113
116
  const seed = (0, accessibility_baseline_file_js_1.buildSeed)(seedViolations);
114
117
  await (0, accessibility_baseline_file_js_1.writeBaselineFile)(filePath, seed);
115
118
  await ctx.testInfo.attach(`a11y-${ctx.scan}-baseline-seed`, {
116
119
  path: filePath,
117
- contentType: 'application/json',
120
+ contentType: "application/json",
118
121
  });
119
122
  if (process.env.CI) {
120
123
  const message = `${ctx.scanLabel}: a11y baseline file was missing for this test. Seeded to ${filePath} — download the attached file from CI artifacts (or re-run locally) and commit it before merging.`;
121
- ctx.expectFn(null, message).toBe('a11y baseline file present');
124
+ ctx.expectFn(null, message).toBe("a11y baseline file present");
122
125
  return;
123
126
  }
124
127
  ctx.testInfo.annotations.push({
125
- type: 'Accessibility',
128
+ type: "Accessibility",
126
129
  description: seed.violations.length === 0
127
130
  ? `${ctx.scanLabel}: a11y baseline seeded at ${filePath} (no violations).`
128
131
  : `${ctx.scanLabel}: a11y baseline seeded at ${filePath} with ${seed.violations.length} entries — fill in reason/willBeFixedIn before committing.`,
@@ -137,8 +140,7 @@ async function dispatchAssertion(ctx, inCodeBaseline) {
137
140
  * option drives soft- vs hard-failure behaviour.
138
141
  */
139
142
  async function runBestPracticeScan(page, testInfo, opts) {
140
- const builder = new playwright_1.default({ page })
141
- .withTags(['best-practice']);
143
+ const builder = new playwright_1.default({ page }).withTags(["best-practice"]);
142
144
  for (const selector of opts.exclude) {
143
145
  builder.exclude(selector);
144
146
  }
@@ -146,13 +148,13 @@ async function runBestPracticeScan(page, testInfo, opts) {
146
148
  builder.options({ rules: opts.rules });
147
149
  }
148
150
  const results = await builder.analyze();
149
- await testInfo.attach('a11y-best-practice-scan-results', {
151
+ await testInfo.attach("a11y-best-practice-scan-results", {
150
152
  body: JSON.stringify(results, null, 2),
151
- contentType: 'application/json'
153
+ contentType: "application/json",
152
154
  });
153
155
  testInfo.annotations.push({
154
- type: 'Accessibility',
155
- description: `Best-practice scan: ${results.violations.length} violations (${results.passes.length} rules passed)`
156
+ type: "Accessibility",
157
+ description: `Best-practice scan: ${results.violations.length} violations (${results.passes.length} rules passed)`,
156
158
  });
157
159
  return results;
158
160
  }
@@ -160,8 +162,7 @@ async function runBestPracticeScan(page, testInfo, opts) {
160
162
  * Run the WCAG axe scan, attach results, and return them for assertion.
161
163
  */
162
164
  async function runWcagScan(page, testInfo, opts) {
163
- const builder = new playwright_1.default({ page })
164
- .withTags(opts.wcagTags);
165
+ const builder = new playwright_1.default({ page }).withTags(opts.wcagTags);
165
166
  for (const selector of opts.exclude) {
166
167
  builder.exclude(selector);
167
168
  }
@@ -169,9 +170,9 @@ async function runWcagScan(page, testInfo, opts) {
169
170
  builder.options({ rules: opts.rules });
170
171
  }
171
172
  const results = await builder.analyze();
172
- await testInfo.attach('a11y-wcag-scan-results', {
173
+ await testInfo.attach("a11y-wcag-scan-results", {
173
174
  body: JSON.stringify(results, null, 2),
174
- contentType: 'application/json'
175
+ contentType: "application/json",
175
176
  });
176
177
  return results;
177
178
  }
@@ -182,39 +183,41 @@ async function runWcagScan(page, testInfo, opts) {
182
183
  async function screenshotViolatingElements(page, testInfo, results) {
183
184
  // Collect all raw CSS selectors from violation nodes.
184
185
  const selectors = results.violations
185
- .flatMap(v => v.nodes)
186
- .flatMap(n => n.target)
187
- .filter((t) => typeof t === 'string');
186
+ .flatMap((v) => v.nodes)
187
+ .flatMap((n) => n.target)
188
+ .filter((t) => typeof t === "string");
188
189
  if (selectors.length === 0)
189
190
  return;
190
191
  // Inject highlight outlines on all violating elements.
191
192
  await page.evaluate((sels) => {
192
- const style = document.createElement('style');
193
- style.setAttribute('data-a11y-highlight', 'true');
193
+ const style = document.createElement("style");
194
+ style.setAttribute("data-a11y-highlight", "true");
194
195
  // Use a CSS rule for each selector so the outline persists even if
195
196
  // elements are repositioned during the screenshot.
196
- const rules = sels.map(s => `${s} { outline: 3px solid #e53e3e !important; outline-offset: 2px !important; }`).join('\n');
197
+ const rules = sels
198
+ .map((s) => `${s} { outline: 3px solid #e53e3e !important; outline-offset: 2px !important; }`)
199
+ .join("\n");
197
200
  style.textContent = rules;
198
201
  document.head.appendChild(style);
199
202
  }, selectors);
200
203
  try {
201
204
  const screenshot = await page.screenshot({ fullPage: true });
202
- await testInfo.attach('a11y-violation-screenshot', {
205
+ await testInfo.attach("a11y-violation-screenshot", {
203
206
  body: screenshot,
204
- contentType: 'image/png',
207
+ contentType: "image/png",
205
208
  });
206
209
  }
207
210
  finally {
208
211
  // Remove the injected styles even if capture or report attachment fails.
209
212
  await page.evaluate(() => {
210
- document.querySelector('style[data-a11y-highlight]')?.remove();
213
+ document.querySelector("style[data-a11y-highlight]")?.remove();
211
214
  });
212
215
  }
213
216
  }
214
217
  /**
215
218
  * Assert violations against a baseline allowlist (in-code or on-disk).
216
219
  */
217
- function assertBaseline(ctx, baseline) {
220
+ function assertBaseline(ctx, baseline, sharedBaselineMatches) {
218
221
  (0, accessibility_baseline_js_1.validateAccessibilityBaseline)(baseline);
219
222
  const { testInfo, results, scanLabel, expectFn } = ctx;
220
223
  const allViolations = extractNormalizedViolations(results);
@@ -225,13 +228,14 @@ function assertBaseline(ctx, baseline) {
225
228
  if (entry.rule !== violation.rule)
226
229
  return false;
227
230
  // Check for at least one overlapping normalized target.
228
- return entry.targets.some(baselineTarget => violation.targets.some(violationTarget => violationTarget === baselineTarget));
231
+ return entry.targets.some((baselineTarget) => violation.targets.some((violationTarget) => violationTarget === baselineTarget));
229
232
  });
230
233
  if (baselineIndex >= 0) {
231
234
  matchedBaselineIndices.add(baselineIndex);
235
+ sharedBaselineMatches?.add(baselineIndex);
232
236
  const entry = baseline[baselineIndex];
233
237
  testInfo.annotations.push({
234
- type: 'Baselined a11y violation',
238
+ type: "Baselined a11y violation",
235
239
  description: `${entry.rule}: ${entry.reason} — ${entry.willBeFixedIn}`,
236
240
  });
237
241
  }
@@ -239,25 +243,31 @@ function assertBaseline(ctx, baseline) {
239
243
  unmatchedViolations.push(violation);
240
244
  }
241
245
  }
242
- // Report stale baseline entries.
243
- baseline.forEach((entry, idx) => {
244
- if (!matchedBaselineIndices.has(idx)) {
245
- testInfo.annotations.push({
246
- type: 'Stale a11y baseline entry',
247
- description: `${entry.rule} on ${entry.targets.join(', ')} — no longer detected`,
248
- });
249
- }
250
- });
246
+ // The WCAG scan always runs last, even when best-practice is disabled.
247
+ // Report shared entries once after both scans have contributed matches,
248
+ // before the hard assertion can throw. On-disk baselines belong to one
249
+ // scan and continue to report stale entries independently.
250
+ if (!sharedBaselineMatches || ctx.scan === "wcag") {
251
+ const matches = sharedBaselineMatches ?? matchedBaselineIndices;
252
+ baseline.forEach((entry, idx) => {
253
+ if (!matches.has(idx)) {
254
+ testInfo.annotations.push({
255
+ type: "Stale a11y baseline entry",
256
+ description: `${entry.rule} on ${entry.targets.join(", ")} — no longer detected`,
257
+ });
258
+ }
259
+ });
260
+ }
251
261
  // Summary annotation for baseline mode.
252
262
  const baselinedCount = matchedBaselineIndices.size;
253
263
  testInfo.annotations.push({
254
- type: 'Accessibility',
264
+ type: "Accessibility",
255
265
  description: `${scanLabel}: ${unmatchedViolations.length} new violations (${baselinedCount} baselined)`,
256
266
  });
257
267
  // Fail on unmatched violations with detailed output.
258
268
  if (unmatchedViolations.length > 0) {
259
269
  const details = formatViolationDetails(results, unmatchedViolations);
260
- expectFn(null, details).toBe('no accessibility violations');
270
+ expectFn(null, details).toBe("no accessibility violations");
261
271
  }
262
272
  }
263
273
  /**
@@ -267,22 +277,24 @@ async function assertSnapshot(ctx) {
267
277
  const { testInfo, results, scan, expectFn } = ctx;
268
278
  // Match the legacy summary annotation phrasing for WCAG; best-practice's
269
279
  // pre-existing summary annotation is emitted in runBestPracticeScan.
270
- if (scan === 'wcag') {
280
+ if (scan === "wcag") {
271
281
  testInfo.annotations.push({
272
- type: 'Accessibility',
273
- description: `WCAG scan: ${results.violations.length} violations (${results.passes.length} rules passed)`
282
+ type: "Accessibility",
283
+ description: `WCAG scan: ${results.violations.length} violations (${results.passes.length} rules passed)`,
274
284
  });
275
285
  // If there are violations, attach baseline suggestions and push annotation.
276
286
  if (results.violations.length > 0) {
277
287
  const allViolations = extractNormalizedViolations(results);
278
- const suggestions = allViolations.map(v => formatBaselineSuggestion(v)).join('\n');
279
- await testInfo.attach('a11y-baseline-suggestions', {
288
+ const suggestions = allViolations
289
+ .map((v) => formatBaselineSuggestion(v))
290
+ .join("\n");
291
+ await testInfo.attach("a11y-baseline-suggestions", {
280
292
  body: suggestions,
281
- contentType: 'text/plain',
293
+ contentType: "text/plain",
282
294
  });
283
295
  testInfo.annotations.push({
284
- type: 'Accessibility',
285
- description: 'To manage violations explicitly, switch to baseline mode. See a11y-baseline-suggestions attachment.',
296
+ type: "Accessibility",
297
+ description: "To manage violations explicitly, switch to baseline mode. See a11y-baseline-suggestions attachment.",
286
298
  });
287
299
  }
288
300
  }
@@ -299,11 +311,13 @@ async function assertSnapshot(ctx) {
299
311
  */
300
312
  async function takeAccessibleScreenshot(page, testInfo, options, scrollLocator, locator) {
301
313
  const screenshotOptions = options ?? {};
302
- if (screenshotOptions.clipLocator && (screenshotOptions.clip || (locator && locator !== page))) {
303
- throw new Error('clipLocator cannot be combined with clip or a locator screenshot target.');
314
+ if (screenshotOptions.clipLocator &&
315
+ (screenshotOptions.clip || (locator && locator !== page))) {
316
+ throw new Error("clipLocator cannot be combined with clip or a locator screenshot target.");
304
317
  }
305
- if (screenshotOptions.clipLocator && screenshotOptions.clipLocator.page() !== page) {
306
- throw new Error('clipLocator must belong to the screenshot page.');
318
+ if (screenshotOptions.clipLocator &&
319
+ screenshotOptions.clipLocator.page() !== page) {
320
+ throw new Error("clipLocator must belong to the screenshot page.");
307
321
  }
308
322
  // The default is 5 seconds. However, even on a fast machine it can take
309
323
  // longer than 5 seconds for large pages like node forms to stabilize. This
@@ -347,7 +361,7 @@ async function takeAccessibleScreenshot(page, testInfo, options, scrollLocator,
347
361
  // A hover action cannot hit its target through the transparent shield used
348
362
  // to clear incidental hover. Remove it only after all stability waits, then
349
363
  // apply real hover before real focus so focusing cannot disturb the pointer.
350
- const hasHover = interactionStates.some(({ states }) => states.includes('hover'));
364
+ const hasHover = interactionStates.some(({ states }) => states.includes("hover"));
351
365
  if (hasHover) {
352
366
  await removeHoverShield?.();
353
367
  removeHoverShield = undefined;
@@ -369,15 +383,17 @@ async function takeAccessibleScreenshot(page, testInfo, options, scrollLocator,
369
383
  // Scrolling can expose lazy content. Await fonts and two paint frames
370
384
  // before measuring; the existing waits have already loaded page media.
371
385
  await (0, fonts_js_1.waitForFonts)(page);
372
- await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
386
+ await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
373
387
  const bounds = await screenshotOptions.clipLocator.boundingBox();
374
388
  if (!bounds)
375
- throw new Error('clipLocator has no visible bounding box.');
389
+ throw new Error("clipLocator has no visible bounding box.");
376
390
  // boundingBox is in main-frame viewport coordinates, including for
377
391
  // locators inside frames. Read scroll offsets from the main page.
378
392
  const geometry = await page.evaluate(() => ({
379
- x: window.scrollX, y: window.scrollY,
380
- width: window.innerWidth, height: window.innerHeight,
393
+ x: window.scrollX,
394
+ y: window.scrollY,
395
+ width: window.innerWidth,
396
+ height: window.innerHeight,
381
397
  }));
382
398
  const clip = {
383
399
  x: Math.round(bounds.x + (screenshotOptions.fullPage ? geometry.x : 0)),
@@ -386,18 +402,24 @@ async function takeAccessibleScreenshot(page, testInfo, options, scrollLocator,
386
402
  height: Math.round(bounds.height),
387
403
  };
388
404
  if (clip.width <= 0 || clip.height <= 0) {
389
- throw new Error('clipLocator must have positive rounded dimensions.');
405
+ throw new Error("clipLocator must have positive rounded dimensions.");
390
406
  }
391
- if (!screenshotOptions.fullPage && (clip.x < 0 || clip.y < 0 || clip.x + clip.width > geometry.width || clip.y + clip.height > geometry.height)) {
392
- throw new Error('clipLocator does not fit in the viewport; use fullPage: true to capture the whole target.');
407
+ if (!screenshotOptions.fullPage &&
408
+ (clip.x < 0 ||
409
+ clip.y < 0 ||
410
+ clip.x + clip.width > geometry.width ||
411
+ clip.y + clip.height > geometry.height)) {
412
+ throw new Error("clipLocator does not fit in the viewport; use fullPage: true to capture the whole target.");
393
413
  }
394
414
  if (clip.x < 0 || clip.y < 0) {
395
- throw new Error('clipLocator must have non-negative document coordinates.');
415
+ throw new Error("clipLocator must have non-negative document coordinates.");
396
416
  }
397
417
  playwrightScreenshotOptions.clip = clip;
398
418
  }
399
419
  // Soft failure here so we can get accessibility violations too.
400
- await test_1.expect.soft(locatorToScreenshot).toHaveScreenshot(playwrightScreenshotOptions);
420
+ await test_1.expect
421
+ .soft(locatorToScreenshot)
422
+ .toHaveScreenshot(playwrightScreenshotOptions);
401
423
  // Settling a video pauses it, clears `autoplay` and rewinds it. That is only
402
424
  // wanted for the duration of the capture: a test that screenshots a page and
403
425
  // then asserts that a video is playing should still pass.
@@ -435,9 +457,9 @@ async function takeAccessibleScreenshot(page, testInfo, options, scrollLocator,
435
457
  function normalizeTarget(target) {
436
458
  const uniqueHtmlID = /(#[^#]*)--\d+/;
437
459
  const ariaLabelledById = /(aria-labelledby="[^"]+)--\d+"/;
438
- if (typeof target === 'string') {
460
+ if (typeof target === "string") {
439
461
  return target
440
- .replace(uniqueHtmlID, '$1--UNIQUE-ID')
462
+ .replace(uniqueHtmlID, "$1--UNIQUE-ID")
441
463
  .replace(ariaLabelledById, '$1--UNIQUE-ID"');
442
464
  }
443
465
  return target;
@@ -447,12 +469,12 @@ function normalizeTarget(target) {
447
469
  * flat, deduplicated CSS selector strings.
448
470
  */
449
471
  function extractNormalizedViolations(results) {
450
- return results.violations.map(violation => {
472
+ return results.violations.map((violation) => {
451
473
  const flatTargets = [];
452
474
  for (const node of violation.nodes) {
453
475
  for (const target of node.target) {
454
476
  const normalized = normalizeTarget(target);
455
- const str = typeof normalized === 'string' ? normalized : normalized.join(' ');
477
+ const str = typeof normalized === "string" ? normalized : normalized.join(" ");
456
478
  if (!flatTargets.includes(str)) {
457
479
  flatTargets.push(str);
458
480
  }
@@ -462,7 +484,7 @@ function extractNormalizedViolations(results) {
462
484
  rule: violation.id,
463
485
  targets: flatTargets,
464
486
  description: violation.description,
465
- impact: violation.impact ?? 'unknown',
487
+ impact: violation.impact ?? "unknown",
466
488
  helpUrl: violation.helpUrl,
467
489
  };
468
490
  });
@@ -471,7 +493,7 @@ function extractNormalizedViolations(results) {
471
493
  * Format a single violation as a copy-pasteable baseline entry.
472
494
  */
473
495
  function formatBaselineSuggestion(violation) {
474
- const targetsStr = violation.targets.map(t => `'${t}'`).join(', ');
496
+ const targetsStr = violation.targets.map((t) => `'${t}'`).join(", ");
475
497
  return `{
476
498
  rule: '${violation.rule}',
477
499
  targets: [${targetsStr}],
@@ -491,12 +513,12 @@ function formatViolationDetails(results, violations) {
491
513
  lines.push(` ${v.description}`);
492
514
  lines.push(` Help: ${v.helpUrl}`);
493
515
  lines.push(` Targets: ${targetsStr}`);
494
- lines.push('');
495
- lines.push(' Add to your baseline to accept this violation:');
496
- lines.push(' ' + formatBaselineSuggestion(v).split('\n').join('\n '));
497
- lines.push('');
516
+ lines.push("");
517
+ lines.push(" Add to your baseline to accept this violation:");
518
+ lines.push(" " + formatBaselineSuggestion(v).split("\n").join("\n "));
519
+ lines.push("");
498
520
  }
499
- return lines.join('\n');
521
+ return lines.join("\n");
500
522
  }
501
523
  /**
502
524
  * Filter violations down to stable elements.
@@ -507,11 +529,11 @@ function formatViolationDetails(results, violations) {
507
529
  * @param accessibilityScanResults
508
530
  */
509
531
  function violationFingerprints(accessibilityScanResults) {
510
- const violationFps = accessibilityScanResults.violations.map(violation => ({
532
+ const violationFps = accessibilityScanResults.violations.map((violation) => ({
511
533
  rule: violation.id,
512
534
  // These are CSS selectors which uniquely identify each element with
513
535
  // a violation of the rule in question.
514
- targets: violation.nodes.map(node => node.target.map((target) => {
536
+ targets: violation.nodes.map((node) => node.target.map((target) => {
515
537
  return normalizeTarget(target);
516
538
  })),
517
539
  }));
package/lib/focus.js CHANGED
@@ -19,7 +19,10 @@ async function blurActiveElement(page) {
19
19
  // `document.activeElement` falls back to `<body>`/`<html>` when nothing is
20
20
  // focused. Blurring that paints no focus ring but can still fire `focusout`
21
21
  // handlers, so skip it and only blur a genuinely focused control.
22
- if (!el || el === document.body || el === document.documentElement || typeof el.blur !== "function") {
22
+ if (!el ||
23
+ el === document.body ||
24
+ el === document.documentElement ||
25
+ typeof el.blur !== "function") {
23
26
  return false;
24
27
  }
25
28
  el.blur();
package/lib/frames.js CHANGED
@@ -7,20 +7,20 @@ exports.waitForFrames = waitForFrames;
7
7
  * @param page
8
8
  */
9
9
  async function waitForFrames(page) {
10
- const locators = await page.locator('iframe');
10
+ const locators = await page.locator("iframe");
11
11
  // Trigger lazy-loading iframes. Since this should be fast, and we don't want to
12
12
  // have to deal with concurrency bugs, we do this in serial.
13
13
  for (const locator of await locators.all()) {
14
14
  // Ensure iframes are connected to the DOM before trying to scroll to them.
15
15
  // https://github.com/microsoft/playwright/issues/23758
16
- if (await locator.evaluate(iframe => iframe.isConnected)) {
16
+ if (await locator.evaluate((iframe) => iframe.isConnected)) {
17
17
  if (await locator.isVisible()) {
18
18
  await locator.scrollIntoViewIfNeeded();
19
19
  await locator.scrollIntoViewIfNeeded();
20
20
  const element = await locator.elementHandle();
21
21
  const frame = await element?.contentFrame();
22
- await frame?.waitForURL(new RegExp('.*/.*', 'i'));
23
- await frame?.waitForLoadState('load');
22
+ await frame?.waitForURL(new RegExp(".*/.*", "i"));
23
+ await frame?.waitForLoadState("load");
24
24
  }
25
25
  }
26
26
  }