@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,52 +1,65 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
 
3
3
  // Mock AxeBuilder — must be hoisted since vi.mock is hoisted
4
- const { mockWithTags, mockExclude, mockOptions, mockAnalyze, MockAxeBuilder } = vi.hoisted(() => {
5
- const mockWithTags = vi.fn().mockReturnThis()
6
- const mockExclude = vi.fn().mockReturnThis()
7
- const mockOptions = vi.fn().mockReturnThis()
8
- const mockAnalyze = vi.fn()
9
-
10
- class MockAxeBuilder {
11
- withTags = mockWithTags
12
- exclude = mockExclude
13
- options = mockOptions
14
- analyze = mockAnalyze
15
- constructor(_args: any) {}
16
- }
4
+ const { mockWithTags, mockExclude, mockOptions, mockAnalyze, MockAxeBuilder } =
5
+ vi.hoisted(() => {
6
+ const mockWithTags = vi.fn().mockReturnThis();
7
+ const mockExclude = vi.fn().mockReturnThis();
8
+ const mockOptions = vi.fn().mockReturnThis();
9
+ const mockAnalyze = vi.fn();
10
+
11
+ class MockAxeBuilder {
12
+ withTags = mockWithTags;
13
+ exclude = mockExclude;
14
+ options = mockOptions;
15
+ analyze = mockAnalyze;
16
+ }
17
17
 
18
- return { mockWithTags, mockExclude, mockOptions, mockAnalyze, MockAxeBuilder }
19
- })
18
+ return {
19
+ mockWithTags,
20
+ mockExclude,
21
+ mockOptions,
22
+ mockAnalyze,
23
+ MockAxeBuilder,
24
+ };
25
+ });
20
26
 
21
- vi.mock('@axe-core/playwright', () => ({
27
+ vi.mock("@axe-core/playwright", () => ({
22
28
  default: MockAxeBuilder,
23
- }))
29
+ }));
24
30
 
25
31
  // Mock @playwright/test — use vi.hoisted to define values before the hoisted vi.mock call
26
- const { mockToMatchSnapshot, mockToHaveScreenshot, mockExpectSoft, mockExpectHard } = vi.hoisted(() => {
27
- const mockToMatchSnapshot = vi.fn()
28
- const mockToHaveScreenshot = vi.fn()
29
- const mockExpectSoft = vi.fn(() => ({
30
- toMatchSnapshot: mockToMatchSnapshot,
31
- toHaveScreenshot: mockToHaveScreenshot,
32
- }))
33
- const mockExpectHard = vi.fn(() => ({
34
- toMatchSnapshot: mockToMatchSnapshot,
35
- }))
36
- return { mockToMatchSnapshot, mockToHaveScreenshot, mockExpectSoft, mockExpectHard }
37
- })
38
-
39
- vi.mock('@playwright/test', () => {
32
+ const { mockToHaveScreenshot, mockExpectSoft, mockExpectHard } = vi.hoisted(
33
+ () => {
34
+ const mockToMatchSnapshot = vi.fn();
35
+ const mockToHaveScreenshot = vi.fn();
36
+ const mockExpectSoft = vi.fn(() => ({
37
+ toMatchSnapshot: mockToMatchSnapshot,
38
+ toHaveScreenshot: mockToHaveScreenshot,
39
+ }));
40
+ const mockExpectHard = vi.fn(() => ({
41
+ toMatchSnapshot: mockToMatchSnapshot,
42
+ }));
43
+ return {
44
+ mockToMatchSnapshot,
45
+ mockToHaveScreenshot,
46
+ mockExpectSoft,
47
+ mockExpectHard,
48
+ };
49
+ },
50
+ );
51
+
52
+ vi.mock("@playwright/test", () => {
40
53
  const expectFn = Object.assign(mockExpectHard, {
41
54
  soft: mockExpectSoft,
42
- })
55
+ });
43
56
  return {
44
57
  expect: expectFn,
45
58
  Locator: {},
46
59
  Page: {},
47
60
  TestInfo: {},
48
- }
49
- })
61
+ };
62
+ });
50
63
 
51
64
  const {
52
65
  mockWaitForAllImages,
@@ -66,532 +79,647 @@ const {
66
79
  mockBlurActiveElement: vi.fn(),
67
80
  mockClearHover: vi.fn(),
68
81
  mockRemoveHoverShield: vi.fn(),
69
- }))
82
+ }));
70
83
 
71
- vi.mock('./images', () => ({ waitForAllImages: mockWaitForAllImages }))
72
- vi.mock('./frames', () => ({ waitForFrames: mockWaitForFrames }))
73
- vi.mock('./fonts', () => ({ waitForFonts: mockWaitForFonts }))
74
- vi.mock('./videos', () => ({
84
+ vi.mock("./images", () => ({ waitForAllImages: mockWaitForAllImages }));
85
+ vi.mock("./frames", () => ({ waitForFrames: mockWaitForFrames }));
86
+ vi.mock("./fonts", () => ({ waitForFonts: mockWaitForFonts }));
87
+ vi.mock("./videos", () => ({
75
88
  waitForVideos: mockWaitForVideos,
76
89
  restoreVideoPlayback: mockRestoreVideoPlayback,
77
- }))
78
- vi.mock('./focus', () => ({ blurActiveElement: mockBlurActiveElement }))
79
- vi.mock('./hover', () => ({ clearHover: mockClearHover }))
80
-
81
- import { checkAccessibility, takeAccessibleScreenshot } from './accessible-screenshot'
82
- import AxeBuilder from '@axe-core/playwright'
83
-
84
- function makeAxeResults(overrides?: Partial<{ violations: any[], passes: any[] }>) {
90
+ }));
91
+ vi.mock("./focus", () => ({ blurActiveElement: mockBlurActiveElement }));
92
+ vi.mock("./hover", () => ({ clearHover: mockClearHover }));
93
+
94
+ import {
95
+ checkAccessibility,
96
+ takeAccessibleScreenshot,
97
+ } from "./accessible-screenshot";
98
+
99
+ function makeAxeResults(
100
+ overrides?: Partial<{ violations: any[]; passes: any[] }>,
101
+ ) {
85
102
  return {
86
103
  violations: [],
87
104
  passes: [],
88
105
  incomplete: [],
89
106
  inapplicable: [],
90
107
  ...overrides,
91
- }
108
+ };
92
109
  }
93
110
 
94
111
  function makeTestInfo() {
95
112
  return {
96
113
  testId: `mocked-${Math.random()}`,
97
- title: 'mocked test',
98
- titlePath: ['file.spec.ts', 'mocked test'],
99
- annotations: [] as Array<{ type: string, description?: string }>,
114
+ title: "mocked test",
115
+ titlePath: ["file.spec.ts", "mocked test"],
116
+ annotations: [] as Array<{ type: string; description?: string }>,
100
117
  attach: vi.fn().mockResolvedValue(undefined),
101
- snapshotPath: (...segs: string[]) => `/tmp/__a11y_test_snapshots__/${segs.join('/')}`,
102
- config: { updateSnapshots: 'all' as const },
103
- project: { name: 'desktop chromium' },
104
- }
118
+ snapshotPath: (...segs: string[]) =>
119
+ `/tmp/__a11y_test_snapshots__/${segs.join("/")}`,
120
+ config: { updateSnapshots: "all" as const },
121
+ project: { name: "desktop chromium" },
122
+ };
105
123
  }
106
124
 
107
- describe('checkAccessibility', () => {
125
+ describe("checkAccessibility", () => {
108
126
  beforeEach(() => {
109
- vi.clearAllMocks()
110
- mockAnalyze.mockResolvedValue(makeAxeResults())
111
- mockClearHover.mockResolvedValue(mockRemoveHoverShield)
112
- })
113
-
114
- it('uses default WCAG tags when none provided', async () => {
115
- const testInfo = makeTestInfo()
116
- await checkAccessibility({} as any, testInfo as any)
117
-
118
- expect(mockWithTags).toHaveBeenCalledWith(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
119
- })
120
-
121
- it('respects custom wcagTags', async () => {
122
- const testInfo = makeTestInfo()
127
+ vi.clearAllMocks();
128
+ mockAnalyze.mockResolvedValue(makeAxeResults());
129
+ mockClearHover.mockResolvedValue(mockRemoveHoverShield);
130
+ });
131
+
132
+ it("uses default WCAG tags when none provided", async () => {
133
+ const testInfo = makeTestInfo();
134
+ await checkAccessibility({} as any, testInfo as any);
135
+
136
+ expect(mockWithTags).toHaveBeenCalledWith([
137
+ "wcag2a",
138
+ "wcag2aa",
139
+ "wcag21a",
140
+ "wcag21aa",
141
+ ]);
142
+ });
143
+
144
+ it("respects custom wcagTags", async () => {
145
+ const testInfo = makeTestInfo();
123
146
  await checkAccessibility({} as any, testInfo as any, {
124
- wcagTags: ['wcag22aa'],
125
- })
147
+ wcagTags: ["wcag22aa"],
148
+ });
126
149
 
127
- expect(mockWithTags).toHaveBeenCalledWith(['wcag22aa'])
128
- })
150
+ expect(mockWithTags).toHaveBeenCalledWith(["wcag22aa"]);
151
+ });
129
152
 
130
- it('respects exclude option — adds extra .exclude() calls', async () => {
131
- const testInfo = makeTestInfo()
153
+ it("respects exclude option — adds extra .exclude() calls", async () => {
154
+ const testInfo = makeTestInfo();
132
155
  await checkAccessibility({} as any, testInfo as any, {
133
- exclude: ['.my-selector', '#another'],
134
- })
156
+ exclude: [".my-selector", "#another"],
157
+ });
135
158
 
136
- expect(mockExclude).toHaveBeenCalledWith('.my-selector')
137
- expect(mockExclude).toHaveBeenCalledWith('#another')
138
- })
159
+ expect(mockExclude).toHaveBeenCalledWith(".my-selector");
160
+ expect(mockExclude).toHaveBeenCalledWith("#another");
161
+ });
139
162
 
140
- it('skips best-practice scan when bestPracticeMode is off', async () => {
141
- const testInfo = makeTestInfo()
163
+ it("skips best-practice scan when bestPracticeMode is off", async () => {
164
+ const testInfo = makeTestInfo();
142
165
  await checkAccessibility({} as any, testInfo as any, {
143
- bestPracticeMode: 'off',
144
- })
166
+ bestPracticeMode: "off",
167
+ });
145
168
 
146
169
  // withTags should only be called once (for the WCAG scan, not best-practice)
147
- expect(mockWithTags).toHaveBeenCalledTimes(1)
148
- expect(mockWithTags).not.toHaveBeenCalledWith(['best-practice'])
149
- expect(mockWithTags).toHaveBeenCalledWith(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
150
- })
151
-
152
- it('always uses expect.soft() for best-practice so WCAG scan runs even in hard mode', async () => {
153
- const testInfo = makeTestInfo()
170
+ expect(mockWithTags).toHaveBeenCalledTimes(1);
171
+ expect(mockWithTags).not.toHaveBeenCalledWith(["best-practice"]);
172
+ expect(mockWithTags).toHaveBeenCalledWith([
173
+ "wcag2a",
174
+ "wcag2aa",
175
+ "wcag21a",
176
+ "wcag21aa",
177
+ ]);
178
+ });
179
+
180
+ it("always uses expect.soft() for best-practice so WCAG scan runs even in hard mode", async () => {
181
+ const testInfo = makeTestInfo();
154
182
  await checkAccessibility({} as any, testInfo as any, {
155
- bestPracticeMode: 'hard',
156
- })
183
+ bestPracticeMode: "hard",
184
+ });
157
185
 
158
186
  // Best-practice always uses expect.soft() so the WCAG scan is never blocked
159
- expect(mockExpectSoft).toHaveBeenCalled()
187
+ expect(mockExpectSoft).toHaveBeenCalled();
160
188
  // WCAG uses expect() (hard)
161
- expect(mockExpectHard).toHaveBeenCalled()
162
- })
189
+ expect(mockExpectHard).toHaveBeenCalled();
190
+ });
163
191
 
164
- it('uses expect.soft() when bestPracticeMode is soft (default)', async () => {
165
- const testInfo = makeTestInfo()
166
- await checkAccessibility({} as any, testInfo as any)
192
+ it("uses expect.soft() when bestPracticeMode is soft (default)", async () => {
193
+ const testInfo = makeTestInfo();
194
+ await checkAccessibility({} as any, testInfo as any);
167
195
 
168
196
  // Best-practice scan should use expect.soft()
169
- expect(mockExpectSoft).toHaveBeenCalled()
170
- })
197
+ expect(mockExpectSoft).toHaveBeenCalled();
198
+ });
171
199
 
172
- it('has no framework-specific exclusions by default', async () => {
173
- const testInfo = makeTestInfo()
174
- await checkAccessibility({} as any, testInfo as any)
200
+ it("has no framework-specific exclusions by default", async () => {
201
+ const testInfo = makeTestInfo();
202
+ await checkAccessibility({} as any, testInfo as any);
175
203
 
176
- expect(mockExclude).not.toHaveBeenCalled()
177
- })
204
+ expect(mockExclude).not.toHaveBeenCalled();
205
+ });
178
206
 
179
- it('supports scan-specific explicit exclusions', async () => {
180
- const testInfo = makeTestInfo()
207
+ it("supports scan-specific explicit exclusions", async () => {
208
+ const testInfo = makeTestInfo();
181
209
  await checkAccessibility({} as any, testInfo as any, {
182
- bestPracticeExclude: ['.best-practice-only'],
183
- wcagExclude: ['.wcag-only'],
184
- })
210
+ bestPracticeExclude: [".best-practice-only"],
211
+ wcagExclude: [".wcag-only"],
212
+ });
185
213
 
186
- expect(mockExclude).toHaveBeenCalledWith('.best-practice-only')
187
- expect(mockExclude).toHaveBeenCalledWith('.wcag-only')
188
- })
214
+ expect(mockExclude).toHaveBeenCalledWith(".best-practice-only");
215
+ expect(mockExclude).toHaveBeenCalledWith(".wcag-only");
216
+ });
189
217
 
190
- it('passes rules to AxeBuilder .options()', async () => {
191
- const testInfo = makeTestInfo()
192
- const rules = { 'color-contrast': { enabled: false } }
193
- await checkAccessibility({} as any, testInfo as any, { rules })
218
+ it("passes rules to AxeBuilder .options()", async () => {
219
+ const testInfo = makeTestInfo();
220
+ const rules = { "color-contrast": { enabled: false } };
221
+ await checkAccessibility({} as any, testInfo as any, { rules });
194
222
 
195
- expect(mockOptions).toHaveBeenCalledWith({ rules })
223
+ expect(mockOptions).toHaveBeenCalledWith({ rules });
196
224
  // Called twice: once for best-practice, once for WCAG
197
- expect(mockOptions).toHaveBeenCalledTimes(2)
198
- })
225
+ expect(mockOptions).toHaveBeenCalledTimes(2);
226
+ });
199
227
 
200
- it('pushes @a11y annotation and deduplicates on second call', async () => {
201
- const testInfo = makeTestInfo()
228
+ it("pushes @a11y annotation and deduplicates on second call", async () => {
229
+ const testInfo = makeTestInfo();
202
230
 
203
- await checkAccessibility({} as any, testInfo as any)
204
- const a11yAnnotations = testInfo.annotations.filter((a: any) => a.type === '@a11y')
205
- expect(a11yAnnotations).toHaveLength(1)
231
+ await checkAccessibility({} as any, testInfo as any);
232
+ const a11yAnnotations = testInfo.annotations.filter(
233
+ (a: any) => a.type === "@a11y",
234
+ );
235
+ expect(a11yAnnotations).toHaveLength(1);
206
236
 
207
237
  // Call again — should not duplicate
208
- await checkAccessibility({} as any, testInfo as any)
209
- const a11yAnnotationsAfter = testInfo.annotations.filter((a: any) => a.type === '@a11y')
210
- expect(a11yAnnotationsAfter).toHaveLength(1)
211
- })
212
-
213
- it('pushes summary annotations after each scan', async () => {
238
+ await checkAccessibility({} as any, testInfo as any);
239
+ const a11yAnnotationsAfter = testInfo.annotations.filter(
240
+ (a: any) => a.type === "@a11y",
241
+ );
242
+ expect(a11yAnnotationsAfter).toHaveLength(1);
243
+ });
244
+
245
+ it("pushes summary annotations after each scan", async () => {
214
246
  const bestPracticeResults = makeAxeResults({
215
- violations: [{ id: 'rule1', nodes: [] }],
216
- passes: [{ id: 'rule2' }, { id: 'rule3' }],
217
- })
247
+ violations: [{ id: "rule1", nodes: [] }],
248
+ passes: [{ id: "rule2" }, { id: "rule3" }],
249
+ });
218
250
  const wcagResults = makeAxeResults({
219
251
  violations: [],
220
- passes: [{ id: 'rule4' }],
221
- })
252
+ passes: [{ id: "rule4" }],
253
+ });
222
254
 
223
255
  mockAnalyze
224
256
  .mockResolvedValueOnce(bestPracticeResults)
225
- .mockResolvedValueOnce(wcagResults)
226
-
227
- const testInfo = makeTestInfo()
228
- await checkAccessibility({} as any, testInfo as any)
229
-
230
- const accessibilityAnnotations = testInfo.annotations.filter((a: any) => a.type === 'Accessibility')
231
- expect(accessibilityAnnotations).toHaveLength(2)
232
- expect(accessibilityAnnotations[0].description).toBe('Best-practice scan: 1 violations (2 rules passed)')
233
- expect(accessibilityAnnotations[1].description).toBe('WCAG scan: 0 violations (1 rules passed)')
234
- })
235
-
236
- it('skips best-practice summary annotation when bestPracticeMode is off', async () => {
237
- const testInfo = makeTestInfo()
257
+ .mockResolvedValueOnce(wcagResults);
258
+
259
+ const testInfo = makeTestInfo();
260
+ await checkAccessibility({} as any, testInfo as any);
261
+
262
+ const accessibilityAnnotations = testInfo.annotations.filter(
263
+ (a: any) => a.type === "Accessibility",
264
+ );
265
+ expect(accessibilityAnnotations).toHaveLength(2);
266
+ expect(accessibilityAnnotations[0].description).toBe(
267
+ "Best-practice scan: 1 violations (2 rules passed)",
268
+ );
269
+ expect(accessibilityAnnotations[1].description).toBe(
270
+ "WCAG scan: 0 violations (1 rules passed)",
271
+ );
272
+ });
273
+
274
+ it("skips best-practice summary annotation when bestPracticeMode is off", async () => {
275
+ const testInfo = makeTestInfo();
238
276
  await checkAccessibility({} as any, testInfo as any, {
239
- bestPracticeMode: 'off',
240
- })
277
+ bestPracticeMode: "off",
278
+ });
241
279
 
242
- const accessibilityAnnotations = testInfo.annotations.filter((a: any) => a.type === 'Accessibility')
243
- expect(accessibilityAnnotations).toHaveLength(1)
244
- expect(accessibilityAnnotations[0].description).toContain('WCAG scan')
245
- })
280
+ const accessibilityAnnotations = testInfo.annotations.filter(
281
+ (a: any) => a.type === "Accessibility",
282
+ );
283
+ expect(accessibilityAnnotations).toHaveLength(1);
284
+ expect(accessibilityAnnotations[0].description).toContain("WCAG scan");
285
+ });
246
286
 
247
- it('attaches a violation screenshot when screenshotViolations is true and violations exist', async () => {
287
+ it("attaches a violation screenshot when screenshotViolations is true and violations exist", async () => {
248
288
  const wcagResults = makeAxeResults({
249
- violations: [{
250
- id: 'color-contrast',
251
- description: 'test',
252
- impact: 'serious',
253
- helpUrl: 'https://example.com',
254
- nodes: [{ target: ['.bad-element'] }],
255
- }],
256
- })
289
+ violations: [
290
+ {
291
+ id: "color-contrast",
292
+ description: "test",
293
+ impact: "serious",
294
+ helpUrl: "https://example.com",
295
+ nodes: [{ target: [".bad-element"] }],
296
+ },
297
+ ],
298
+ });
257
299
 
258
300
  mockAnalyze
259
301
  .mockResolvedValueOnce(makeAxeResults()) // best-practice
260
- .mockResolvedValueOnce(wcagResults) // WCAG
302
+ .mockResolvedValueOnce(wcagResults); // WCAG
261
303
 
262
304
  const mockPage = {
263
305
  evaluate: vi.fn().mockResolvedValue(undefined),
264
- screenshot: vi.fn().mockResolvedValue(Buffer.from('fake-png')),
265
- }
266
- const testInfo = makeTestInfo()
306
+ screenshot: vi.fn().mockResolvedValue(Buffer.from("fake-png")),
307
+ };
308
+ const testInfo = makeTestInfo();
267
309
 
268
310
  await checkAccessibility(mockPage as any, testInfo as any, {
269
311
  screenshotViolations: true,
270
- })
312
+ });
271
313
 
272
314
  // Should have injected highlight styles and then removed them.
273
- expect(mockPage.evaluate).toHaveBeenCalledTimes(2)
315
+ expect(mockPage.evaluate).toHaveBeenCalledTimes(2);
274
316
  // Should have taken a full-page screenshot.
275
- expect(mockPage.screenshot).toHaveBeenCalledWith({ fullPage: true })
317
+ expect(mockPage.screenshot).toHaveBeenCalledWith({ fullPage: true });
276
318
  // Should have attached the screenshot.
277
- expect(testInfo.attach).toHaveBeenCalledWith('a11y-violation-screenshot', {
278
- body: Buffer.from('fake-png'),
279
- contentType: 'image/png',
280
- })
281
- })
319
+ expect(testInfo.attach).toHaveBeenCalledWith("a11y-violation-screenshot", {
320
+ body: Buffer.from("fake-png"),
321
+ contentType: "image/png",
322
+ });
323
+ });
282
324
 
283
- it('removes violation highlights when attaching the screenshot fails', async () => {
325
+ it("removes violation highlights when attaching the screenshot fails", async () => {
284
326
  const wcagResults = makeAxeResults({
285
- violations: [{
286
- id: 'image-alt',
287
- description: 'test',
288
- impact: 'serious',
289
- helpUrl: 'https://example.com',
290
- nodes: [{target: ['img']}],
291
- }],
292
- })
327
+ violations: [
328
+ {
329
+ id: "image-alt",
330
+ description: "test",
331
+ impact: "serious",
332
+ helpUrl: "https://example.com",
333
+ nodes: [{ target: ["img"] }],
334
+ },
335
+ ],
336
+ });
293
337
  mockAnalyze
294
338
  .mockResolvedValueOnce(makeAxeResults())
295
- .mockResolvedValueOnce(wcagResults)
339
+ .mockResolvedValueOnce(wcagResults);
296
340
  const page = {
297
341
  evaluate: vi.fn().mockResolvedValue(undefined),
298
- screenshot: vi.fn().mockResolvedValue(Buffer.from('fake-png')),
299
- }
300
- const testInfo = makeTestInfo()
342
+ screenshot: vi.fn().mockResolvedValue(Buffer.from("fake-png")),
343
+ };
344
+ const testInfo = makeTestInfo();
301
345
  testInfo.attach.mockImplementation((name: string) =>
302
- name === 'a11y-violation-screenshot'
303
- ? Promise.reject(new Error('attach failed'))
304
- : Promise.resolve()
305
- )
346
+ name === "a11y-violation-screenshot"
347
+ ? Promise.reject(new Error("attach failed"))
348
+ : Promise.resolve(),
349
+ );
306
350
 
307
- await expect(checkAccessibility(page as any, testInfo as any)).rejects.toThrow('attach failed')
351
+ await expect(
352
+ checkAccessibility(page as any, testInfo as any),
353
+ ).rejects.toThrow("attach failed");
308
354
 
309
- expect(page.evaluate).toHaveBeenCalledTimes(2)
310
- })
355
+ expect(page.evaluate).toHaveBeenCalledTimes(2);
356
+ });
311
357
 
312
- it('does not take a screenshot when screenshotViolations is true but no violations', async () => {
358
+ it("does not take a screenshot when screenshotViolations is true but no violations", async () => {
313
359
  const mockPage = {
314
360
  evaluate: vi.fn(),
315
361
  screenshot: vi.fn(),
316
- }
317
- const testInfo = makeTestInfo()
362
+ };
363
+ const testInfo = makeTestInfo();
318
364
 
319
365
  await checkAccessibility(mockPage as any, testInfo as any, {
320
366
  screenshotViolations: true,
321
- })
367
+ });
322
368
 
323
- expect(mockPage.screenshot).not.toHaveBeenCalled()
324
- })
369
+ expect(mockPage.screenshot).not.toHaveBeenCalled();
370
+ });
325
371
 
326
- it('takes a screenshot by default when violations exist', async () => {
372
+ it("takes a screenshot by default when violations exist", async () => {
327
373
  const wcagResults = makeAxeResults({
328
- violations: [{
329
- id: 'color-contrast',
330
- description: 'test',
331
- impact: 'serious',
332
- helpUrl: 'https://example.com',
333
- nodes: [{ target: ['.bad-element'] }],
334
- }],
335
- })
374
+ violations: [
375
+ {
376
+ id: "color-contrast",
377
+ description: "test",
378
+ impact: "serious",
379
+ helpUrl: "https://example.com",
380
+ nodes: [{ target: [".bad-element"] }],
381
+ },
382
+ ],
383
+ });
336
384
 
337
385
  mockAnalyze
338
386
  .mockResolvedValueOnce(makeAxeResults()) // best-practice
339
- .mockResolvedValueOnce(wcagResults) // WCAG
387
+ .mockResolvedValueOnce(wcagResults); // WCAG
340
388
 
341
389
  const mockPage = {
342
390
  evaluate: vi.fn().mockResolvedValue(undefined),
343
- screenshot: vi.fn().mockResolvedValue(Buffer.from('fake-png')),
344
- }
345
- const testInfo = makeTestInfo()
391
+ screenshot: vi.fn().mockResolvedValue(Buffer.from("fake-png")),
392
+ };
393
+ const testInfo = makeTestInfo();
346
394
 
347
- await checkAccessibility(mockPage as any, testInfo as any)
395
+ await checkAccessibility(mockPage as any, testInfo as any);
348
396
 
349
- expect(mockPage.screenshot).toHaveBeenCalledWith({ fullPage: true })
350
- })
397
+ expect(mockPage.screenshot).toHaveBeenCalledWith({ fullPage: true });
398
+ });
351
399
 
352
- it('does not take a screenshot when screenshotViolations is explicitly false', async () => {
400
+ it("does not take a screenshot when screenshotViolations is explicitly false", async () => {
353
401
  const wcagResults = makeAxeResults({
354
- violations: [{
355
- id: 'color-contrast',
356
- description: 'test',
357
- impact: 'serious',
358
- helpUrl: 'https://example.com',
359
- nodes: [{ target: ['.bad-element'] }],
360
- }],
361
- })
402
+ violations: [
403
+ {
404
+ id: "color-contrast",
405
+ description: "test",
406
+ impact: "serious",
407
+ helpUrl: "https://example.com",
408
+ nodes: [{ target: [".bad-element"] }],
409
+ },
410
+ ],
411
+ });
362
412
 
363
413
  mockAnalyze
364
414
  .mockResolvedValueOnce(makeAxeResults()) // best-practice
365
- .mockResolvedValueOnce(wcagResults) // WCAG
415
+ .mockResolvedValueOnce(wcagResults); // WCAG
366
416
 
367
417
  const mockPage = {
368
418
  evaluate: vi.fn(),
369
419
  screenshot: vi.fn(),
370
- }
371
- const testInfo = makeTestInfo()
420
+ };
421
+ const testInfo = makeTestInfo();
372
422
 
373
423
  await checkAccessibility(mockPage as any, testInfo as any, {
374
424
  screenshotViolations: false,
375
- })
425
+ });
376
426
 
377
- expect(mockPage.screenshot).not.toHaveBeenCalled()
378
- })
379
- })
427
+ expect(mockPage.screenshot).not.toHaveBeenCalled();
428
+ });
429
+ });
380
430
 
381
- describe('takeAccessibleScreenshot hover handling', () => {
431
+ describe("takeAccessibleScreenshot hover handling", () => {
382
432
  beforeEach(() => {
383
- vi.clearAllMocks()
384
- mockAnalyze.mockResolvedValue(makeAxeResults())
385
- mockClearHover.mockResolvedValue(mockRemoveHoverShield)
386
- })
387
-
388
- it('clears hover by default and removes the shield after capture', async () => {
389
- const page = {} as any
390
- const testInfo = makeTestInfo()
391
-
392
- await takeAccessibleScreenshot(page, testInfo as any)
433
+ vi.clearAllMocks();
434
+ mockAnalyze.mockResolvedValue(makeAxeResults());
435
+ mockClearHover.mockResolvedValue(mockRemoveHoverShield);
436
+ });
437
+
438
+ it("clears hover by default and removes the shield after capture", async () => {
439
+ const page = {} as any;
440
+ const testInfo = makeTestInfo();
441
+
442
+ await takeAccessibleScreenshot(page, testInfo as any);
443
+
444
+ expect(mockClearHover).toHaveBeenCalledWith(page);
445
+ expect(mockToHaveScreenshot).toHaveBeenCalledWith({ timeout: 10000 });
446
+ expect(mockRemoveHoverShield).toHaveBeenCalledOnce();
447
+ expect(mockRestoreVideoPlayback).toHaveBeenCalledWith(page);
448
+ expect(mockRemoveHoverShield.mock.invocationCallOrder[0]).toBeGreaterThan(
449
+ mockToHaveScreenshot.mock.invocationCallOrder[0],
450
+ );
451
+ expect(mockRemoveHoverShield.mock.invocationCallOrder[0]).toBeLessThan(
452
+ mockAnalyze.mock.invocationCallOrder[0],
453
+ );
454
+ });
455
+
456
+ it("preserves an intentional hover state when clearHover is false", async () => {
457
+ const page = {} as any;
458
+ const testInfo = makeTestInfo();
393
459
 
394
- expect(mockClearHover).toHaveBeenCalledWith(page)
395
- expect(mockToHaveScreenshot).toHaveBeenCalledWith({ timeout: 10000 })
396
- expect(mockRemoveHoverShield).toHaveBeenCalledOnce()
397
- expect(mockRestoreVideoPlayback).toHaveBeenCalledWith(page)
398
- expect(mockRemoveHoverShield.mock.invocationCallOrder[0])
399
- .toBeGreaterThan(mockToHaveScreenshot.mock.invocationCallOrder[0])
400
- expect(mockRemoveHoverShield.mock.invocationCallOrder[0])
401
- .toBeLessThan(mockAnalyze.mock.invocationCallOrder[0])
402
- })
403
-
404
- it('preserves an intentional hover state when clearHover is false', async () => {
405
- const page = {} as any
406
- const testInfo = makeTestInfo()
407
-
408
- await takeAccessibleScreenshot(page, testInfo as any, { clearHover: false })
460
+ await takeAccessibleScreenshot(page, testInfo as any, {
461
+ clearHover: false,
462
+ });
409
463
 
410
- expect(mockClearHover).not.toHaveBeenCalled()
411
- expect(mockRemoveHoverShield).not.toHaveBeenCalled()
412
- expect(mockToHaveScreenshot).toHaveBeenCalledWith({ timeout: 10000 })
413
- })
464
+ expect(mockClearHover).not.toHaveBeenCalled();
465
+ expect(mockRemoveHoverShield).not.toHaveBeenCalled();
466
+ expect(mockToHaveScreenshot).toHaveBeenCalledWith({ timeout: 10000 });
467
+ });
414
468
 
415
- it('does not infer screenshot thresholds from project names', async () => {
416
- const testInfo = makeTestInfo()
417
- testInfo.project.name = 'desktop safari'
469
+ it("does not infer screenshot thresholds from project names", async () => {
470
+ const testInfo = makeTestInfo();
471
+ testInfo.project.name = "desktop safari";
418
472
 
419
- await takeAccessibleScreenshot({} as any, testInfo as any)
473
+ await takeAccessibleScreenshot({} as any, testInfo as any);
420
474
 
421
- expect(mockToHaveScreenshot).toHaveBeenCalledWith({timeout: 10000})
422
- })
475
+ expect(mockToHaveScreenshot).toHaveBeenCalledWith({ timeout: 10000 });
476
+ });
423
477
 
424
- it('passes explicit stabilization configuration to image and video waits', async () => {
425
- const page = {} as any
426
- const images = {recoverErroredImages: true}
427
- const videos = {timeoutMs: 1234}
478
+ it("passes explicit stabilization configuration to image and video waits", async () => {
479
+ const page = {} as any;
480
+ const images = { recoverErroredImages: true };
481
+ const videos = { timeoutMs: 1234 };
428
482
 
429
483
  await takeAccessibleScreenshot(page, makeTestInfo() as any, {
430
- stabilization: {images, videos},
431
- })
432
-
433
- expect(mockWaitForAllImages).toHaveBeenCalledWith(page, images)
434
- expect(mockWaitForVideos).toHaveBeenCalledWith(page, videos)
435
- expect(mockToHaveScreenshot).toHaveBeenCalledWith({timeout: 10000})
436
- })
437
-
438
- it('removes the shield when capture throws', async () => {
439
- const page = {} as any
440
- const testInfo = makeTestInfo()
441
- mockToHaveScreenshot.mockRejectedValueOnce(new Error('capture failed'))
442
-
443
- await expect(takeAccessibleScreenshot(page, testInfo as any)).rejects.toThrow('capture failed')
444
-
445
- expect(mockRemoveHoverShield).toHaveBeenCalledOnce()
446
- expect(mockRestoreVideoPlayback).toHaveBeenCalledWith(page)
447
- })
448
-
449
- it('applies real hover and focus after settling and keeps them through the accessibility scan', async () => {
450
- const initialShieldCleanup = vi.fn()
451
- const interactionShieldCleanup = vi.fn()
484
+ stabilization: { images, videos },
485
+ });
486
+
487
+ expect(mockWaitForAllImages).toHaveBeenCalledWith(page, images);
488
+ expect(mockWaitForVideos).toHaveBeenCalledWith(page, videos);
489
+ expect(mockToHaveScreenshot).toHaveBeenCalledWith({ timeout: 10000 });
490
+ });
491
+
492
+ it("removes the shield when capture throws", async () => {
493
+ const page = {} as any;
494
+ const testInfo = makeTestInfo();
495
+ mockToHaveScreenshot.mockRejectedValueOnce(new Error("capture failed"));
496
+
497
+ await expect(
498
+ takeAccessibleScreenshot(page, testInfo as any),
499
+ ).rejects.toThrow("capture failed");
500
+
501
+ expect(mockRemoveHoverShield).toHaveBeenCalledOnce();
502
+ expect(mockRestoreVideoPlayback).toHaveBeenCalledWith(page);
503
+ });
504
+
505
+ it("applies real hover and focus after settling and keeps them through the accessibility scan", async () => {
506
+ const initialShieldCleanup = vi.fn();
507
+ const interactionShieldCleanup = vi.fn();
452
508
  mockClearHover
453
509
  .mockResolvedValueOnce(initialShieldCleanup)
454
- .mockResolvedValueOnce(interactionShieldCleanup)
455
- const hoverLocator = {hover: vi.fn()}
456
- const focusLocator = {focus: vi.fn(), blur: vi.fn()}
457
- const page = {} as any
458
- const testInfo = makeTestInfo()
510
+ .mockResolvedValueOnce(interactionShieldCleanup);
511
+ const hoverLocator = { hover: vi.fn() };
512
+ const focusLocator = { focus: vi.fn(), blur: vi.fn() };
513
+ const page = {} as any;
514
+ const testInfo = makeTestInfo();
459
515
 
460
516
  await takeAccessibleScreenshot(page, testInfo as any, {
461
517
  interactionStates: [
462
- {locator: hoverLocator as any, states: ['hover']},
463
- {locator: focusLocator as any, states: ['focus']},
518
+ { locator: hoverLocator as any, states: ["hover"] },
519
+ { locator: focusLocator as any, states: ["focus"] },
464
520
  ],
465
- })
466
-
467
- expect(initialShieldCleanup).toHaveBeenCalledOnce()
468
- expect(hoverLocator.hover).toHaveBeenCalledOnce()
469
- expect(focusLocator.focus).toHaveBeenCalledOnce()
470
- expect(mockToHaveScreenshot).toHaveBeenCalledWith({timeout: 10000})
471
- expect(initialShieldCleanup.mock.invocationCallOrder[0])
472
- .toBeGreaterThan(mockWaitForVideos.mock.invocationCallOrder[0])
473
- expect(hoverLocator.hover.mock.invocationCallOrder[0])
474
- .toBeLessThan(focusLocator.focus.mock.invocationCallOrder[0])
475
- expect(focusLocator.focus.mock.invocationCallOrder[0])
476
- .toBeLessThan(mockToHaveScreenshot.mock.invocationCallOrder[0])
477
- expect(mockBlurActiveElement).toHaveBeenCalledOnce()
478
- expect(focusLocator.blur).toHaveBeenCalledOnce()
479
- expect(focusLocator.blur.mock.invocationCallOrder[0])
480
- .toBeGreaterThan(mockAnalyze.mock.invocationCallOrder.at(-1)!)
481
- expect(interactionShieldCleanup.mock.invocationCallOrder[0])
482
- .toBeGreaterThan(mockAnalyze.mock.invocationCallOrder.at(-1)!)
483
- })
484
-
485
- it('cleans up real states when the accessibility scan throws', async () => {
486
- const initialShieldCleanup = vi.fn()
487
- const interactionShieldCleanup = vi.fn()
521
+ });
522
+
523
+ expect(initialShieldCleanup).toHaveBeenCalledOnce();
524
+ expect(hoverLocator.hover).toHaveBeenCalledOnce();
525
+ expect(focusLocator.focus).toHaveBeenCalledOnce();
526
+ expect(mockToHaveScreenshot).toHaveBeenCalledWith({ timeout: 10000 });
527
+ expect(initialShieldCleanup.mock.invocationCallOrder[0]).toBeGreaterThan(
528
+ mockWaitForVideos.mock.invocationCallOrder[0],
529
+ );
530
+ expect(hoverLocator.hover.mock.invocationCallOrder[0]).toBeLessThan(
531
+ focusLocator.focus.mock.invocationCallOrder[0],
532
+ );
533
+ expect(focusLocator.focus.mock.invocationCallOrder[0]).toBeLessThan(
534
+ mockToHaveScreenshot.mock.invocationCallOrder[0],
535
+ );
536
+ expect(mockBlurActiveElement).toHaveBeenCalledOnce();
537
+ expect(focusLocator.blur).toHaveBeenCalledOnce();
538
+ expect(focusLocator.blur.mock.invocationCallOrder[0]).toBeGreaterThan(
539
+ mockAnalyze.mock.invocationCallOrder.at(-1)!,
540
+ );
541
+ expect(
542
+ interactionShieldCleanup.mock.invocationCallOrder[0],
543
+ ).toBeGreaterThan(mockAnalyze.mock.invocationCallOrder.at(-1)!);
544
+ });
545
+
546
+ it("cleans up real states when the accessibility scan throws", async () => {
547
+ const initialShieldCleanup = vi.fn();
548
+ const interactionShieldCleanup = vi.fn();
488
549
  mockClearHover
489
550
  .mockResolvedValueOnce(initialShieldCleanup)
490
- .mockResolvedValueOnce(interactionShieldCleanup)
491
- const locator = {hover: vi.fn(), focus: vi.fn(), blur: vi.fn()}
492
- mockAnalyze.mockRejectedValueOnce(new Error('scan failed'))
493
-
494
- await expect(takeAccessibleScreenshot({} as any, makeTestInfo() as any, {
495
- interactionStates: [{locator: locator as any, states: ['hover', 'focus']}],
496
- })).rejects.toThrow('scan failed')
497
-
498
- expect(mockBlurActiveElement).toHaveBeenCalledOnce()
499
- expect(locator.blur).toHaveBeenCalledOnce()
500
- expect(interactionShieldCleanup).toHaveBeenCalledOnce()
501
- })
502
-
503
- it('cleans up when applying a real interaction state throws', async () => {
504
- const initialShieldCleanup = vi.fn()
505
- const interactionShieldCleanup = vi.fn()
551
+ .mockResolvedValueOnce(interactionShieldCleanup);
552
+ const locator = { hover: vi.fn(), focus: vi.fn(), blur: vi.fn() };
553
+ mockAnalyze.mockRejectedValueOnce(new Error("scan failed"));
554
+
555
+ await expect(
556
+ takeAccessibleScreenshot({} as any, makeTestInfo() as any, {
557
+ interactionStates: [
558
+ { locator: locator as any, states: ["hover", "focus"] },
559
+ ],
560
+ }),
561
+ ).rejects.toThrow("scan failed");
562
+
563
+ expect(mockBlurActiveElement).toHaveBeenCalledOnce();
564
+ expect(locator.blur).toHaveBeenCalledOnce();
565
+ expect(interactionShieldCleanup).toHaveBeenCalledOnce();
566
+ });
567
+
568
+ it("cleans up when applying a real interaction state throws", async () => {
569
+ const initialShieldCleanup = vi.fn();
570
+ const interactionShieldCleanup = vi.fn();
506
571
  mockClearHover
507
572
  .mockResolvedValueOnce(initialShieldCleanup)
508
- .mockResolvedValueOnce(interactionShieldCleanup)
573
+ .mockResolvedValueOnce(interactionShieldCleanup);
509
574
  const locator = {
510
- hover: vi.fn().mockRejectedValue(new Error('hover failed')),
575
+ hover: vi.fn().mockRejectedValue(new Error("hover failed")),
511
576
  focus: vi.fn(),
512
- }
513
-
514
- await expect(takeAccessibleScreenshot({} as any, makeTestInfo() as any, {
515
- interactionStates: [{locator: locator as any, states: ['hover', 'focus']}],
516
- })).rejects.toThrow('hover failed')
517
-
518
- expect(locator.focus).not.toHaveBeenCalled()
519
- expect(interactionShieldCleanup).toHaveBeenCalledOnce()
520
- })
521
-
522
- it('rejects interaction states that a real pointer cannot hold simultaneously', async () => {
523
- const first = {hover: vi.fn()}
524
- const second = {hover: vi.fn()}
525
-
526
- await expect(takeAccessibleScreenshot({} as any, makeTestInfo() as any, {
527
- interactionStates: [
528
- {locator: first as any, states: ['hover']},
529
- {locator: second as any, states: ['hover']},
530
- ],
531
- })).rejects.toThrow('can hover at most one locator')
532
-
533
- expect(mockClearHover).not.toHaveBeenCalled()
534
- expect(first.hover).not.toHaveBeenCalled()
535
- expect(second.hover).not.toHaveBeenCalled()
536
- })
537
- })
538
-
539
- describe('locator-derived clip', () => {
577
+ };
578
+
579
+ await expect(
580
+ takeAccessibleScreenshot({} as any, makeTestInfo() as any, {
581
+ interactionStates: [
582
+ { locator: locator as any, states: ["hover", "focus"] },
583
+ ],
584
+ }),
585
+ ).rejects.toThrow("hover failed");
586
+
587
+ expect(locator.focus).not.toHaveBeenCalled();
588
+ expect(interactionShieldCleanup).toHaveBeenCalledOnce();
589
+ });
590
+
591
+ it("rejects interaction states that a real pointer cannot hold simultaneously", async () => {
592
+ const first = { hover: vi.fn() };
593
+ const second = { hover: vi.fn() };
594
+
595
+ await expect(
596
+ takeAccessibleScreenshot({} as any, makeTestInfo() as any, {
597
+ interactionStates: [
598
+ { locator: first as any, states: ["hover"] },
599
+ { locator: second as any, states: ["hover"] },
600
+ ],
601
+ }),
602
+ ).rejects.toThrow("can hover at most one locator");
603
+
604
+ expect(mockClearHover).not.toHaveBeenCalled();
605
+ expect(first.hover).not.toHaveBeenCalled();
606
+ expect(second.hover).not.toHaveBeenCalled();
607
+ });
608
+ });
609
+
610
+ describe("locator-derived clip", () => {
540
611
  beforeEach(() => {
541
- vi.clearAllMocks()
542
- mockAnalyze.mockResolvedValue(makeAxeResults())
543
- mockClearHover.mockResolvedValue(mockRemoveHoverShield)
544
- })
612
+ vi.clearAllMocks();
613
+ mockAnalyze.mockResolvedValue(makeAxeResults());
614
+ mockClearHover.mockResolvedValue(mockRemoveHoverShield);
615
+ });
545
616
 
546
617
  function fixture() {
547
- const page = {evaluate: vi.fn().mockResolvedValueOnce(undefined).mockResolvedValue({x: 100, y: 800, width: 640, height: 480})}
548
- const locator = {page: () => page, scrollIntoViewIfNeeded: vi.fn(), boundingBox: vi.fn().mockResolvedValue({x: 10.2, y: 20.3, width: 200.4, height: 100.2})}
549
- return {page, locator}
618
+ const page = {
619
+ evaluate: vi
620
+ .fn()
621
+ .mockResolvedValueOnce(undefined)
622
+ .mockResolvedValue({ x: 100, y: 800, width: 640, height: 480 }),
623
+ };
624
+ const locator = {
625
+ page: () => page,
626
+ scrollIntoViewIfNeeded: vi.fn(),
627
+ boundingBox: vi
628
+ .fn()
629
+ .mockResolvedValue({ x: 10.2, y: 20.3, width: 200.4, height: 100.2 }),
630
+ };
631
+ return { page, locator };
550
632
  }
551
633
 
552
634
  for (const fullPage of [false, true]) {
553
635
  it(`measures after readiness and scroll, with fullPage=${fullPage}`, async () => {
554
- const {page, locator} = fixture()
555
- await takeAccessibleScreenshot(page as any, makeTestInfo() as any, {clipLocator: locator as any, fullPage})
556
- expect(mockToHaveScreenshot).toHaveBeenCalledWith({timeout: 10000, fullPage, clip: {x: fullPage ? 110 : 10, y: fullPage ? 820 : 20, width: 200, height: 100}})
557
- expect(mockExpectSoft).toHaveBeenCalledWith(page)
558
- expect(locator.scrollIntoViewIfNeeded.mock.invocationCallOrder[0]).toBeGreaterThan(mockWaitForVideos.mock.invocationCallOrder[0])
559
- expect(locator.boundingBox.mock.invocationCallOrder[0]).toBeGreaterThan(locator.scrollIntoViewIfNeeded.mock.invocationCallOrder[0])
560
- expect(mockAnalyze).toHaveBeenCalledTimes(2)
561
- })
636
+ const { page, locator } = fixture();
637
+ await takeAccessibleScreenshot(page as any, makeTestInfo() as any, {
638
+ clipLocator: locator as any,
639
+ fullPage,
640
+ });
641
+ expect(mockToHaveScreenshot).toHaveBeenCalledWith({
642
+ timeout: 10000,
643
+ fullPage,
644
+ clip: {
645
+ x: fullPage ? 110 : 10,
646
+ y: fullPage ? 820 : 20,
647
+ width: 200,
648
+ height: 100,
649
+ },
650
+ });
651
+ expect(mockExpectSoft).toHaveBeenCalledWith(page);
652
+ expect(
653
+ locator.scrollIntoViewIfNeeded.mock.invocationCallOrder[0],
654
+ ).toBeGreaterThan(mockWaitForVideos.mock.invocationCallOrder[0]);
655
+ expect(locator.boundingBox.mock.invocationCallOrder[0]).toBeGreaterThan(
656
+ locator.scrollIntoViewIfNeeded.mock.invocationCallOrder[0],
657
+ );
658
+ expect(mockAnalyze).toHaveBeenCalledTimes(2);
659
+ });
562
660
  }
563
661
 
564
- it('rejects conflicting capture modes before readiness', async () => {
565
- const {page, locator} = fixture()
566
- await expect(takeAccessibleScreenshot(page as any, makeTestInfo() as any, {clipLocator: locator as any, clip: {x: 0, y: 0, width: 1, height: 1}})).rejects.toThrow('cannot be combined')
567
- await expect(takeAccessibleScreenshot(page as any, makeTestInfo() as any, {clipLocator: locator as any}, undefined, locator as any)).rejects.toThrow('cannot be combined')
568
- expect(mockWaitForFrames).not.toHaveBeenCalled()
569
- })
570
-
571
- it('preserves existing raw clip and locator screenshot paths', async () => {
572
- const page = {} as any
573
- const locator = {} as any
574
- const clip = {x: 5, y: 10, width: 100, height: 80}
575
- await takeAccessibleScreenshot(page, makeTestInfo() as any, {clip})
576
- expect(mockToHaveScreenshot).toHaveBeenLastCalledWith({timeout: 10000, clip})
577
- await takeAccessibleScreenshot(page, makeTestInfo() as any, {}, undefined, locator)
578
- expect(mockExpectSoft).toHaveBeenCalledWith(locator)
579
- expect(mockToHaveScreenshot).toHaveBeenLastCalledWith({timeout: 10000})
580
- })
581
-
582
- it('rejects a clip locator from a different page', async () => {
583
- const {locator} = fixture()
584
- await expect(takeAccessibleScreenshot({} as any, makeTestInfo() as any, {clipLocator: locator as any})).rejects.toThrow('must belong')
585
- expect(mockWaitForFrames).not.toHaveBeenCalled()
586
- })
587
-
588
- it('rejects missing and zero-sized bounds and restores capture state', async () => {
589
- for (const bounds of [null, {x: 0, y: 0, width: 0.2, height: 1}]) {
590
- const {page, locator} = fixture()
591
- locator.boundingBox.mockResolvedValue(bounds as any)
592
- await expect(takeAccessibleScreenshot(page as any, makeTestInfo() as any, {clipLocator: locator as any})).rejects.toThrow(/bounding box|positive rounded/)
662
+ it("rejects conflicting capture modes before readiness", async () => {
663
+ const { page, locator } = fixture();
664
+ await expect(
665
+ takeAccessibleScreenshot(page as any, makeTestInfo() as any, {
666
+ clipLocator: locator as any,
667
+ clip: { x: 0, y: 0, width: 1, height: 1 },
668
+ }),
669
+ ).rejects.toThrow("cannot be combined");
670
+ await expect(
671
+ takeAccessibleScreenshot(
672
+ page as any,
673
+ makeTestInfo() as any,
674
+ { clipLocator: locator as any },
675
+ undefined,
676
+ locator as any,
677
+ ),
678
+ ).rejects.toThrow("cannot be combined");
679
+ expect(mockWaitForFrames).not.toHaveBeenCalled();
680
+ });
681
+
682
+ it("preserves existing raw clip and locator screenshot paths", async () => {
683
+ const page = {} as any;
684
+ const locator = {} as any;
685
+ const clip = { x: 5, y: 10, width: 100, height: 80 };
686
+ await takeAccessibleScreenshot(page, makeTestInfo() as any, { clip });
687
+ expect(mockToHaveScreenshot).toHaveBeenLastCalledWith({
688
+ timeout: 10000,
689
+ clip,
690
+ });
691
+ await takeAccessibleScreenshot(
692
+ page,
693
+ makeTestInfo() as any,
694
+ {},
695
+ undefined,
696
+ locator,
697
+ );
698
+ expect(mockExpectSoft).toHaveBeenCalledWith(locator);
699
+ expect(mockToHaveScreenshot).toHaveBeenLastCalledWith({ timeout: 10000 });
700
+ });
701
+
702
+ it("rejects a clip locator from a different page", async () => {
703
+ const { locator } = fixture();
704
+ await expect(
705
+ takeAccessibleScreenshot({} as any, makeTestInfo() as any, {
706
+ clipLocator: locator as any,
707
+ }),
708
+ ).rejects.toThrow("must belong");
709
+ expect(mockWaitForFrames).not.toHaveBeenCalled();
710
+ });
711
+
712
+ it("rejects missing and zero-sized bounds and restores capture state", async () => {
713
+ for (const bounds of [null, { x: 0, y: 0, width: 0.2, height: 1 }]) {
714
+ const { page, locator } = fixture();
715
+ locator.boundingBox.mockResolvedValue(bounds as any);
716
+ await expect(
717
+ takeAccessibleScreenshot(page as any, makeTestInfo() as any, {
718
+ clipLocator: locator as any,
719
+ }),
720
+ ).rejects.toThrow(/bounding box|positive rounded/);
593
721
  }
594
- expect(mockRestoreVideoPlayback).toHaveBeenCalledTimes(2)
595
- expect(mockToHaveScreenshot).not.toHaveBeenCalled()
596
- })
597
- })
722
+ expect(mockRestoreVideoPlayback).toHaveBeenCalledTimes(2);
723
+ expect(mockToHaveScreenshot).not.toHaveBeenCalled();
724
+ });
725
+ });