@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
@@ -1,16 +1,16 @@
1
- import { promises as fs } from 'fs'
2
- import path from 'path'
3
- import type { TestInfo } from '@playwright/test'
4
- import type { AccessibilityBaselineEntry } from './accessibility-baseline.js'
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import type { TestInfo } from "@playwright/test";
4
+ import type { AccessibilityBaselineEntry } from "./accessibility-baseline.js";
5
5
 
6
- export type ScanKind = 'wcag' | 'best-practice'
6
+ export type ScanKind = "wcag" | "best-practice";
7
7
 
8
8
  export interface OnDiskBaselineFile {
9
- note: string
10
- violations: AccessibilityBaselineEntry[]
9
+ note: string;
10
+ violations: AccessibilityBaselineEntry[];
11
11
  }
12
12
 
13
- const scanInvocationCounters = new WeakMap<object, Map<ScanKind, number>>()
13
+ const scanInvocationCounters = new WeakMap<object, Map<ScanKind, number>>();
14
14
 
15
15
  /**
16
16
  * Increment and return the 1-indexed invocation number for this scan kind
@@ -26,22 +26,22 @@ const scanInvocationCounters = new WeakMap<object, Map<ScanKind, number>>()
26
26
  * suite that reuses a mocked TestInfo.
27
27
  */
28
28
  export function nextAccessibilityScanCount(
29
- testInfo: Pick<TestInfo, 'testId'> | object,
29
+ testInfo: Pick<TestInfo, "testId"> | object,
30
30
  scan: ScanKind,
31
31
  ): number {
32
- const key = testInfo as object
33
- let counts = scanInvocationCounters.get(key)
32
+ const key = testInfo as object;
33
+ let counts = scanInvocationCounters.get(key);
34
34
  if (!counts) {
35
- counts = new Map()
36
- scanInvocationCounters.set(key, counts)
35
+ counts = new Map();
36
+ scanInvocationCounters.set(key, counts);
37
37
  }
38
- const n = (counts.get(scan) ?? 0) + 1
39
- counts.set(scan, n)
40
- return n
38
+ const n = (counts.get(scan) ?? 0) + 1;
39
+ counts.set(scan, n);
40
+ return n;
41
41
  }
42
42
 
43
43
  export function resetAccessibilityScanCounts(testInfo: object): void {
44
- scanInvocationCounters.delete(testInfo)
44
+ scanInvocationCounters.delete(testInfo);
45
45
  }
46
46
 
47
47
  /**
@@ -53,30 +53,30 @@ export function resetAccessibilityScanCounts(testInfo: object): void {
53
53
  * polynomial backtracking on library-supplied input (CodeQL
54
54
  * js/polynomial-redos).
55
55
  */
56
- function slugifyTitle(testInfo: Pick<TestInfo, 'titlePath' | 'title'>): string {
57
- const segments = testInfo.titlePath?.slice(1) ?? []
58
- const raw = segments.length > 0 ? segments.join(' ') : testInfo.title
59
- let out = ''
60
- let lastWasHyphen = true // suppresses leading hyphen
56
+ function slugifyTitle(testInfo: Pick<TestInfo, "titlePath" | "title">): string {
57
+ const segments = testInfo.titlePath?.slice(1) ?? [];
58
+ const raw = segments.length > 0 ? segments.join(" ") : testInfo.title;
59
+ let out = "";
60
+ let lastWasHyphen = true; // suppresses leading hyphen
61
61
  for (let i = 0; i < raw.length; i++) {
62
- const code = raw.charCodeAt(i)
63
- const isLowerAlpha = code >= 97 && code <= 122 // a-z
64
- const isUpperAlpha = code >= 65 && code <= 90 // A-Z
65
- const isDigit = code >= 48 && code <= 57 // 0-9
62
+ const code = raw.charCodeAt(i);
63
+ const isLowerAlpha = code >= 97 && code <= 122; // a-z
64
+ const isUpperAlpha = code >= 65 && code <= 90; // A-Z
65
+ const isDigit = code >= 48 && code <= 57; // 0-9
66
66
  if (isLowerAlpha || isDigit) {
67
- out += raw[i]
68
- lastWasHyphen = false
67
+ out += raw[i];
68
+ lastWasHyphen = false;
69
69
  } else if (isUpperAlpha) {
70
- out += String.fromCharCode(code + 32)
71
- lastWasHyphen = false
70
+ out += String.fromCharCode(code + 32);
71
+ lastWasHyphen = false;
72
72
  } else if (!lastWasHyphen) {
73
- out += '-'
74
- lastWasHyphen = true
73
+ out += "-";
74
+ lastWasHyphen = true;
75
75
  }
76
76
  }
77
77
  // Trim trailing hyphen.
78
- if (out.endsWith('-')) out = out.slice(0, -1)
79
- return out
78
+ if (out.endsWith("-")) out = out.slice(0, -1);
79
+ return out;
80
80
  }
81
81
 
82
82
  /**
@@ -88,28 +88,30 @@ function slugifyTitle(testInfo: Pick<TestInfo, 'titlePath' | 'title'>): string {
88
88
  * `sanitizeForFilePath()`. Very long titles, which Playwright truncates
89
89
  * and hashes, are not handled.
90
90
  */
91
- function playwrightSnapshotStem(testInfo: Pick<TestInfo, 'titlePath' | 'title'>): string {
92
- const segments = testInfo.titlePath?.slice(1) ?? []
93
- const raw = segments.length > 0 ? segments.join(' ') : testInfo.title
94
- let out = ''
95
- let lastWasReplaced = false
91
+ function playwrightSnapshotStem(
92
+ testInfo: Pick<TestInfo, "titlePath" | "title">,
93
+ ): string {
94
+ const segments = testInfo.titlePath?.slice(1) ?? [];
95
+ const raw = segments.length > 0 ? segments.join(" ") : testInfo.title;
96
+ let out = "";
97
+ let lastWasReplaced = false;
96
98
  for (let i = 0; i < raw.length; i++) {
97
- const code = raw.charCodeAt(i)
99
+ const code = raw.charCodeAt(i);
98
100
  const replaced =
99
101
  code <= 0x2c ||
100
102
  (code >= 0x2e && code <= 0x2f) ||
101
103
  (code >= 0x3a && code <= 0x40) ||
102
104
  (code >= 0x5b && code <= 0x60) ||
103
- (code >= 0x7b && code <= 0x7f)
105
+ (code >= 0x7b && code <= 0x7f);
104
106
  if (replaced) {
105
- if (!lastWasReplaced) out += '-'
106
- lastWasReplaced = true
107
+ if (!lastWasReplaced) out += "-";
108
+ lastWasReplaced = true;
107
109
  } else {
108
- out += raw[i]
109
- lastWasReplaced = false
110
+ out += raw[i];
111
+ lastWasReplaced = false;
110
112
  }
111
113
  }
112
- return out
114
+ return out;
113
115
  }
114
116
 
115
117
  /**
@@ -127,13 +129,14 @@ function playwrightSnapshotStem(testInfo: Pick<TestInfo, 'titlePath' | 'title'>)
127
129
  * for its auto-counter naming — so we build the stem ourselves.
128
130
  */
129
131
  export function baselineFilePath(
130
- testInfo: Pick<TestInfo, 'snapshotPath' | 'titlePath' | 'title'>,
132
+ testInfo: Pick<TestInfo, "snapshotPath" | "titlePath" | "title">,
131
133
  scan: ScanKind,
132
134
  callCount: number,
133
135
  ): string {
134
- const slug = slugifyTitle(testInfo)
135
- const suffix = scan === 'wcag' ? 'a11y-baseline' : 'a11y-baseline-best-practice'
136
- return testInfo.snapshotPath(`${slug}-${callCount}.${suffix}.json`)
136
+ const slug = slugifyTitle(testInfo);
137
+ const suffix =
138
+ scan === "wcag" ? "a11y-baseline" : "a11y-baseline-best-practice";
139
+ return testInfo.snapshotPath(`${slug}-${callCount}.${suffix}.json`);
137
140
  }
138
141
 
139
142
  /**
@@ -143,66 +146,87 @@ export function baselineFilePath(
143
146
  * defaulting new (snapshotless) tests into on-disk baseline mode.
144
147
  */
145
148
  export async function snapshotExists(
146
- testInfo: Pick<TestInfo, 'snapshotPath' | 'titlePath' | 'title'>,
149
+ testInfo: Pick<TestInfo, "snapshotPath" | "titlePath" | "title">,
147
150
  ): Promise<boolean> {
148
- const dir = path.dirname(testInfo.snapshotPath('a11y-baseline-probe'))
149
- let entries: string[]
151
+ const dir = path.dirname(testInfo.snapshotPath("a11y-baseline-probe"));
152
+ let entries: string[];
150
153
  try {
151
- entries = await fs.readdir(dir)
154
+ entries = await fs.readdir(dir);
152
155
  } catch (err: any) {
153
- if (err?.code === 'ENOENT') return false
154
- throw err
156
+ if (err?.code === "ENOENT") return false;
157
+ throw err;
155
158
  }
156
159
  // Playwright names snapshots `<stem>-<counter>[-<project>-<platform>].txt`.
157
160
  // Require the counter so a title that merely extends this one (e.g.
158
161
  // "Video" vs "Video Promo") is not mistaken for this test's snapshot.
159
- const prefix = `${playwrightSnapshotStem(testInfo)}-`
160
- return entries.some(name => {
161
- if (!name.startsWith(prefix) || !name.endsWith('.txt')) return false
162
- let i = prefix.length
163
- while (i < name.length && name.charCodeAt(i) >= 48 && name.charCodeAt(i) <= 57) i++
164
- return i > prefix.length && (name[i] === '-' || name[i] === '.')
165
- })
162
+ const prefix = `${playwrightSnapshotStem(testInfo)}-`;
163
+ return entries.some((name) => {
164
+ if (!name.startsWith(prefix) || !name.endsWith(".txt")) return false;
165
+ let i = prefix.length;
166
+ while (
167
+ i < name.length &&
168
+ name.charCodeAt(i) >= 48 &&
169
+ name.charCodeAt(i) <= 57
170
+ )
171
+ i++;
172
+ return i > prefix.length && (name[i] === "-" || name[i] === ".");
173
+ });
166
174
  }
167
175
 
168
- export async function readBaselineFile(filePath: string): Promise<OnDiskBaselineFile | null> {
169
- let raw: string
176
+ export async function readBaselineFile(
177
+ filePath: string,
178
+ ): Promise<OnDiskBaselineFile | null> {
179
+ let raw: string;
170
180
  try {
171
- raw = await fs.readFile(filePath, 'utf8')
181
+ raw = await fs.readFile(filePath, "utf8");
172
182
  } catch (err: any) {
173
- if (err?.code === 'ENOENT') return null
174
- throw err
183
+ if (err?.code === "ENOENT") return null;
184
+ throw err;
175
185
  }
176
186
  try {
177
- const parsed = JSON.parse(raw)
178
- if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.violations)) {
179
- throw new Error('expected an object with a "violations" array')
187
+ const parsed = JSON.parse(raw);
188
+ if (
189
+ !parsed ||
190
+ typeof parsed !== "object" ||
191
+ !Array.isArray(parsed.violations)
192
+ ) {
193
+ throw new Error('expected an object with a "violations" array');
180
194
  }
181
195
  return {
182
- note: typeof parsed.note === 'string' ? parsed.note : '',
196
+ note: typeof parsed.note === "string" ? parsed.note : "",
183
197
  violations: parsed.violations as AccessibilityBaselineEntry[],
184
- }
198
+ };
185
199
  } catch (err) {
186
- throw new Error(`Malformed baseline file at ${filePath}: ${(err as Error).message}`)
200
+ throw new Error(
201
+ `Malformed baseline file at ${filePath}: ${(err as Error).message}`,
202
+ { cause: err },
203
+ );
187
204
  }
188
205
  }
189
206
 
190
- export async function writeBaselineFile(filePath: string, data: OnDiskBaselineFile): Promise<void> {
191
- await fs.mkdir(path.dirname(filePath), { recursive: true })
207
+ export async function writeBaselineFile(
208
+ filePath: string,
209
+ data: OnDiskBaselineFile,
210
+ ): Promise<void> {
211
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
192
212
  try {
193
- await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', { flag: 'wx' })
213
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", {
214
+ flag: "wx",
215
+ });
194
216
  } catch (err: any) {
195
- if (err?.code === 'EEXIST') return
196
- throw err
217
+ if (err?.code === "EEXIST") return;
218
+ throw err;
197
219
  }
198
220
  }
199
221
 
200
- export function buildSeed(violations: AccessibilityBaselineEntry[]): OnDiskBaselineFile {
222
+ export function buildSeed(
223
+ violations: AccessibilityBaselineEntry[],
224
+ ): OnDiskBaselineFile {
201
225
  if (violations.length === 0) {
202
- return { note: 'No accessibility violations found', violations: [] }
226
+ return { note: "No accessibility violations found", violations: [] };
203
227
  }
204
228
  return {
205
- note: 'TODO: fill in reason and willBeFixedIn for each entry before committing.',
229
+ note: "TODO: fill in reason and willBeFixedIn for each entry before committing.",
206
230
  violations,
207
- }
231
+ };
208
232
  }