@lullabot/playwright-testing 0.1.0 → 1.0.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.
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 +88 -76
  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 +139 -107
  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 +633 -433
  34. package/src/accessibility-baseline.ts +41 -21
  35. package/src/accessible-screenshot.test.ts +551 -423
  36. package/src/accessible-screenshot.ts +334 -245
  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 +659 -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,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import {
3
3
  defaultTestFunction,
4
4
  defineVisualDiffConfig,
@@ -6,51 +6,56 @@ import {
6
6
  VisualDiffGroup,
7
7
  VisualDiffTestCases,
8
8
  VisualDiffUrlConfig,
9
- } from './visualdiff'
9
+ } from "./visualdiff";
10
10
 
11
- const {mockTest, mockDescribe, mockSkip} = vi.hoisted(() => {
12
- const mockTest = vi.fn()
13
- const mockDescribe = vi.fn((_name: string, callback: () => void) => callback())
14
- const mockSkip = vi.fn()
15
- return {mockTest, mockDescribe, mockSkip}
16
- })
11
+ const { mockTest, mockDescribe, mockSkip } = vi.hoisted(() => {
12
+ const mockTest = vi.fn();
13
+ const mockDescribe = vi.fn((_name: string, callback: () => void) =>
14
+ callback(),
15
+ );
16
+ const mockSkip = vi.fn();
17
+ return { mockTest, mockDescribe, mockSkip };
18
+ });
17
19
 
18
- vi.mock('@playwright/test', () => ({
19
- test: Object.assign(mockTest, {describe: mockDescribe, skip: mockSkip}),
20
- }))
20
+ vi.mock("@playwright/test", () => ({
21
+ test: Object.assign(mockTest, { describe: mockDescribe, skip: mockSkip }),
22
+ }));
21
23
 
22
24
  // Mock takeAccessibleScreenshot to capture the options passed to it.
23
- const mockTakeAccessibleScreenshot = vi.fn()
24
- const mockForcePseudoState = vi.fn()
25
- vi.mock('./accessible-screenshot', () => ({
26
- takeAccessibleScreenshot: (...args: any[]) => mockTakeAccessibleScreenshot(...args),
27
- }))
28
- vi.mock('./pseudo-state', () => ({
25
+ const mockTakeAccessibleScreenshot = vi.fn();
26
+ const mockForcePseudoState = vi.fn();
27
+ vi.mock("./accessible-screenshot", () => ({
28
+ takeAccessibleScreenshot: (...args: any[]) =>
29
+ mockTakeAccessibleScreenshot(...args),
30
+ }));
31
+ vi.mock("./pseudo-state", () => ({
29
32
  forcePseudoState: (...args: any[]) => mockForcePseudoState(...args),
30
- }))
33
+ }));
31
34
 
32
35
  function makeTestCase(overrides?: Partial<VisualDiff>): VisualDiff {
33
36
  return {
34
- name: 'Test',
35
- path: '/test',
37
+ name: "Test",
38
+ path: "/test",
36
39
  ...overrides,
37
- }
40
+ };
38
41
  }
39
42
 
40
43
  function makeGroup(overrides?: Partial<VisualDiffGroup>): VisualDiffGroup {
41
44
  return {
42
- name: 'Group',
45
+ name: "Group",
43
46
  testCases: [],
44
47
  ...overrides,
45
- }
48
+ };
46
49
  }
47
50
 
48
- function makeConfig(overrides?: Partial<VisualDiffUrlConfig>): VisualDiffUrlConfig {
51
+ function makeConfig(
52
+ overrides?: Partial<VisualDiffUrlConfig>,
53
+ ): VisualDiffUrlConfig {
49
54
  return {
50
- name: 'Config',
55
+ name: "Config",
51
56
  groups: [],
52
57
  ...overrides,
53
- }
58
+ };
54
59
  }
55
60
 
56
61
  /**
@@ -62,391 +67,487 @@ async function runDefaultTestFunction(
62
67
  config?: VisualDiffUrlConfig,
63
68
  preset?: Parameters<typeof defaultTestFunction>[3],
64
69
  ) {
65
- const fn = defaultTestFunction(testCase, group, config, preset)
66
- const mockLocator = vi.fn((selector: string) => ({ _selector: selector }))
70
+ const fn = defaultTestFunction(testCase, group, config, preset);
71
+ const mockLocator = vi.fn((selector: string) => ({ _selector: selector }));
67
72
  const mockPage = {
68
73
  goto: vi.fn(),
69
74
  locator: mockLocator,
70
- }
75
+ };
71
76
  const mockContext = {
72
77
  on: vi.fn(),
73
- }
78
+ };
74
79
  const mockTestInfo = {
75
80
  annotations: [],
76
- }
77
- await fn({ page: mockPage, context: mockContext }, mockTestInfo)
78
- return { mockPage, mockTestInfo }
81
+ };
82
+ await fn({ page: mockPage, context: mockContext }, mockTestInfo);
83
+ return { mockPage, mockTestInfo };
79
84
  }
80
85
 
81
- describe('defaultTestFunction mask merging', () => {
86
+ describe("defaultTestFunction mask merging", () => {
82
87
  beforeEach(() => {
83
- mockTakeAccessibleScreenshot.mockReset()
84
- mockForcePseudoState.mockReset()
85
- })
88
+ mockTakeAccessibleScreenshot.mockReset();
89
+ mockForcePseudoState.mockReset();
90
+ });
86
91
 
87
- it('merges masks from all three levels', async () => {
88
- const config = makeConfig({ mask: ['.config-mask'] })
89
- const group = makeGroup({ mask: ['.group-mask'] })
90
- const testCase = makeTestCase({ mask: ['.test-mask'] })
92
+ it("merges masks from all three levels", async () => {
93
+ const config = makeConfig({ mask: [".config-mask"] });
94
+ const group = makeGroup({ mask: [".group-mask"] });
95
+ const testCase = makeTestCase({ mask: [".test-mask"] });
91
96
 
92
- await runDefaultTestFunction(testCase, group, config)
97
+ await runDefaultTestFunction(testCase, group, config);
93
98
 
94
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
95
- expect(options.mask).toHaveLength(3)
96
- expect(options.mask[0]).toEqual({ _selector: '.config-mask' })
97
- expect(options.mask[1]).toEqual({ _selector: '.group-mask' })
98
- expect(options.mask[2]).toEqual({ _selector: '.test-mask' })
99
- })
99
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
100
+ expect(options.mask).toHaveLength(3);
101
+ expect(options.mask[0]).toEqual({ _selector: ".config-mask" });
102
+ expect(options.mask[1]).toEqual({ _selector: ".group-mask" });
103
+ expect(options.mask[2]).toEqual({ _selector: ".test-mask" });
104
+ });
100
105
 
101
- it('works with masks from a single level', async () => {
102
- const group = makeGroup({ mask: ['.only-group'] })
103
- const testCase = makeTestCase()
106
+ it("works with masks from a single level", async () => {
107
+ const group = makeGroup({ mask: [".only-group"] });
108
+ const testCase = makeTestCase();
104
109
 
105
- await runDefaultTestFunction(testCase, group)
110
+ await runDefaultTestFunction(testCase, group);
106
111
 
107
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
108
- expect(options.mask).toHaveLength(1)
109
- expect(options.mask[0]).toEqual({ _selector: '.only-group' })
110
- })
112
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
113
+ expect(options.mask).toHaveLength(1);
114
+ expect(options.mask[0]).toEqual({ _selector: ".only-group" });
115
+ });
111
116
 
112
- it('handles empty/undefined masks at some levels', async () => {
113
- const config = makeConfig({ mask: ['.config-a', '.config-b'] })
114
- const group = makeGroup() // no mask
115
- const testCase = makeTestCase({ mask: ['.test-only'] })
117
+ it("handles empty/undefined masks at some levels", async () => {
118
+ const config = makeConfig({ mask: [".config-a", ".config-b"] });
119
+ const group = makeGroup(); // no mask
120
+ const testCase = makeTestCase({ mask: [".test-only"] });
116
121
 
117
- await runDefaultTestFunction(testCase, group, config)
122
+ await runDefaultTestFunction(testCase, group, config);
118
123
 
119
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
120
- expect(options.mask).toHaveLength(3)
121
- expect(options.mask[0]).toEqual({ _selector: '.config-a' })
122
- expect(options.mask[1]).toEqual({ _selector: '.config-b' })
123
- expect(options.mask[2]).toEqual({ _selector: '.test-only' })
124
- })
124
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
125
+ expect(options.mask).toHaveLength(3);
126
+ expect(options.mask[0]).toEqual({ _selector: ".config-a" });
127
+ expect(options.mask[1]).toEqual({ _selector: ".config-b" });
128
+ expect(options.mask[2]).toEqual({ _selector: ".test-only" });
129
+ });
125
130
 
126
- it('does not include mask property when no masks are defined', async () => {
127
- const testCase = makeTestCase()
128
- const group = makeGroup()
131
+ it("does not include mask property when no masks are defined", async () => {
132
+ const testCase = makeTestCase();
133
+ const group = makeGroup();
129
134
 
130
- await runDefaultTestFunction(testCase, group)
135
+ await runDefaultTestFunction(testCase, group);
131
136
 
132
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
133
- expect(options.mask).toBeUndefined()
134
- expect(options.fullPage).toBe(true)
135
- })
137
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
138
+ expect(options.mask).toBeUndefined();
139
+ expect(options.fullPage).toBe(true);
140
+ });
136
141
 
137
- it('resolves maskColor with most-specific-wins: testCase > group > config', async () => {
138
- const config = makeConfig({ maskColor: '#111' })
139
- const group = makeGroup({ maskColor: '#222' })
140
- const testCase = makeTestCase({ maskColor: '#333' })
142
+ it("resolves maskColor with most-specific-wins: testCase > group > config", async () => {
143
+ const config = makeConfig({ maskColor: "#111" });
144
+ const group = makeGroup({ maskColor: "#222" });
145
+ const testCase = makeTestCase({ maskColor: "#333" });
141
146
 
142
- await runDefaultTestFunction(testCase, group, config)
147
+ await runDefaultTestFunction(testCase, group, config);
143
148
 
144
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
145
- expect(options.maskColor).toBe('#333')
146
- })
149
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
150
+ expect(options.maskColor).toBe("#333");
151
+ });
147
152
 
148
- it('falls back maskColor to group when testCase is undefined', async () => {
149
- const config = makeConfig({ maskColor: '#111' })
150
- const group = makeGroup({ maskColor: '#222' })
151
- const testCase = makeTestCase()
153
+ it("falls back maskColor to group when testCase is undefined", async () => {
154
+ const config = makeConfig({ maskColor: "#111" });
155
+ const group = makeGroup({ maskColor: "#222" });
156
+ const testCase = makeTestCase();
152
157
 
153
- await runDefaultTestFunction(testCase, group, config)
158
+ await runDefaultTestFunction(testCase, group, config);
154
159
 
155
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
156
- expect(options.maskColor).toBe('#222')
157
- })
160
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
161
+ expect(options.maskColor).toBe("#222");
162
+ });
158
163
 
159
- it('falls back maskColor to config when testCase and group are undefined', async () => {
160
- const config = makeConfig({ maskColor: '#111' })
161
- const group = makeGroup()
162
- const testCase = makeTestCase()
164
+ it("falls back maskColor to config when testCase and group are undefined", async () => {
165
+ const config = makeConfig({ maskColor: "#111" });
166
+ const group = makeGroup();
167
+ const testCase = makeTestCase();
163
168
 
164
- await runDefaultTestFunction(testCase, group, config)
169
+ await runDefaultTestFunction(testCase, group, config);
165
170
 
166
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
167
- expect(options.maskColor).toBe('#111')
168
- })
171
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
172
+ expect(options.maskColor).toBe("#111");
173
+ });
169
174
 
170
- it('does not include maskColor when none are defined', async () => {
171
- const testCase = makeTestCase()
172
- const group = makeGroup()
175
+ it("does not include maskColor when none are defined", async () => {
176
+ const testCase = makeTestCase();
177
+ const group = makeGroup();
173
178
 
174
- await runDefaultTestFunction(testCase, group)
179
+ await runDefaultTestFunction(testCase, group);
175
180
 
176
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
177
- expect(options.maskColor).toBeUndefined()
178
- })
179
- })
181
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
182
+ expect(options.maskColor).toBeUndefined();
183
+ });
184
+ });
180
185
 
181
- describe('defaultTestFunction pseudo-state handling', () => {
186
+ describe("defaultTestFunction pseudo-state handling", () => {
182
187
  beforeEach(() => {
183
- vi.clearAllMocks()
184
- mockForcePseudoState.mockImplementation(async () => vi.fn())
185
- })
186
-
187
- it('merges pseudo-states from all three levels and keeps them through the scan', async () => {
188
- const clearConfigState = vi.fn()
189
- const clearGroupState = vi.fn()
190
- const clearTestState = vi.fn()
188
+ vi.clearAllMocks();
189
+ mockForcePseudoState.mockImplementation(async () => vi.fn());
190
+ });
191
+
192
+ it("merges pseudo-states from all three levels and keeps them through the scan", async () => {
193
+ const clearConfigState = vi.fn();
194
+ const clearGroupState = vi.fn();
195
+ const clearTestState = vi.fn();
191
196
  mockForcePseudoState
192
197
  .mockResolvedValueOnce(clearConfigState)
193
198
  .mockResolvedValueOnce(clearGroupState)
194
- .mockResolvedValueOnce(clearTestState)
199
+ .mockResolvedValueOnce(clearTestState);
195
200
  const config = makeConfig({
196
- pseudoStates: [{selector: '.nav', pseudoClasses: ['hover']}],
197
- })
201
+ pseudoStates: [{ selector: ".nav", pseudoClasses: ["hover"] }],
202
+ });
198
203
  const group = makeGroup({
199
- pseudoStates: [{selector: '.menu', pseudoClasses: ['focus-within']}],
200
- })
204
+ pseudoStates: [{ selector: ".menu", pseudoClasses: ["focus-within"] }],
205
+ });
201
206
  const testCase = makeTestCase({
202
- pseudoStates: [{selector: '.link', pseudoClasses: ['focus-visible']}],
203
- })
207
+ pseudoStates: [{ selector: ".link", pseudoClasses: ["focus-visible"] }],
208
+ });
204
209
 
205
- const {mockPage} = await runDefaultTestFunction(testCase, group, config)
210
+ const { mockPage } = await runDefaultTestFunction(testCase, group, config);
206
211
 
207
212
  expect(mockForcePseudoState.mock.calls).toEqual([
208
- [mockPage, '.nav', ['hover']],
209
- [mockPage, '.menu', ['focus-within']],
210
- [mockPage, '.link', ['focus-visible']],
211
- ])
212
- expect(mockTakeAccessibleScreenshot).toHaveBeenCalledOnce()
213
- expect(clearTestState.mock.invocationCallOrder[0])
214
- .toBeGreaterThan(mockTakeAccessibleScreenshot.mock.invocationCallOrder[0])
215
- expect(clearTestState.mock.invocationCallOrder[0])
216
- .toBeLessThan(clearGroupState.mock.invocationCallOrder[0])
217
- expect(clearGroupState.mock.invocationCallOrder[0])
218
- .toBeLessThan(clearConfigState.mock.invocationCallOrder[0])
219
- })
220
-
221
- it('clears forced states when the screenshot or accessibility scan fails', async () => {
222
- const clearState = vi.fn()
223
- mockForcePseudoState.mockResolvedValueOnce(clearState)
224
- mockTakeAccessibleScreenshot.mockRejectedValueOnce(new Error('scan failed'))
213
+ [mockPage, ".nav", ["hover"]],
214
+ [mockPage, ".menu", ["focus-within"]],
215
+ [mockPage, ".link", ["focus-visible"]],
216
+ ]);
217
+ expect(mockTakeAccessibleScreenshot).toHaveBeenCalledOnce();
218
+ expect(clearTestState.mock.invocationCallOrder[0]).toBeGreaterThan(
219
+ mockTakeAccessibleScreenshot.mock.invocationCallOrder[0],
220
+ );
221
+ expect(clearTestState.mock.invocationCallOrder[0]).toBeLessThan(
222
+ clearGroupState.mock.invocationCallOrder[0],
223
+ );
224
+ expect(clearGroupState.mock.invocationCallOrder[0]).toBeLessThan(
225
+ clearConfigState.mock.invocationCallOrder[0],
226
+ );
227
+ });
228
+
229
+ it("clears forced states when the screenshot or accessibility scan fails", async () => {
230
+ const clearState = vi.fn();
231
+ mockForcePseudoState.mockResolvedValueOnce(clearState);
232
+ mockTakeAccessibleScreenshot.mockRejectedValueOnce(
233
+ new Error("scan failed"),
234
+ );
225
235
  const testCase = makeTestCase({
226
- pseudoStates: [{selector: 'button', pseudoClasses: ['focus']}],
227
- })
236
+ pseudoStates: [{ selector: "button", pseudoClasses: ["focus"] }],
237
+ });
228
238
 
229
- await expect(runDefaultTestFunction(testCase, makeGroup())).rejects.toThrow('scan failed')
239
+ await expect(runDefaultTestFunction(testCase, makeGroup())).rejects.toThrow(
240
+ "scan failed",
241
+ );
230
242
 
231
- expect(clearState).toHaveBeenCalledOnce()
232
- })
243
+ expect(clearState).toHaveBeenCalledOnce();
244
+ });
233
245
 
234
- it('attempts every cleanup in reverse order when one cleanup fails', async () => {
235
- const events: string[] = []
236
- const clearConfigState = vi.fn(async () => { events.push('config') })
246
+ it("attempts every cleanup in reverse order when one cleanup fails", async () => {
247
+ const events: string[] = [];
248
+ const clearConfigState = vi.fn(async () => {
249
+ events.push("config");
250
+ });
237
251
  const clearGroupState = vi.fn(async () => {
238
- events.push('group')
239
- throw new Error('group cleanup failed')
240
- })
241
- const clearTestState = vi.fn(async () => { events.push('test') })
252
+ events.push("group");
253
+ throw new Error("group cleanup failed");
254
+ });
255
+ const clearTestState = vi.fn(async () => {
256
+ events.push("test");
257
+ });
242
258
  mockForcePseudoState
243
259
  .mockResolvedValueOnce(clearConfigState)
244
260
  .mockResolvedValueOnce(clearGroupState)
245
- .mockResolvedValueOnce(clearTestState)
246
-
247
- await expect(runDefaultTestFunction(
248
- makeTestCase({pseudoStates: [{selector: '.test', pseudoClasses: ['hover']}]}),
249
- makeGroup({pseudoStates: [{selector: '.group', pseudoClasses: ['focus']}]}),
250
- makeConfig({pseudoStates: [{selector: '.config', pseudoClasses: ['active']}]}),
251
- )).rejects.toThrow('group cleanup failed')
252
-
253
- expect(events).toEqual(['test', 'group', 'config'])
254
- })
255
- })
256
-
257
- describe('defaultTestFunction precedence and preset injection', () => {
261
+ .mockResolvedValueOnce(clearTestState);
262
+
263
+ await expect(
264
+ runDefaultTestFunction(
265
+ makeTestCase({
266
+ pseudoStates: [{ selector: ".test", pseudoClasses: ["hover"] }],
267
+ }),
268
+ makeGroup({
269
+ pseudoStates: [{ selector: ".group", pseudoClasses: ["focus"] }],
270
+ }),
271
+ makeConfig({
272
+ pseudoStates: [{ selector: ".config", pseudoClasses: ["active"] }],
273
+ }),
274
+ ),
275
+ ).rejects.toThrow("group cleanup failed");
276
+
277
+ expect(events).toEqual(["test", "group", "config"]);
278
+ });
279
+ });
280
+
281
+ describe("defaultTestFunction precedence and preset injection", () => {
258
282
  beforeEach(() => {
259
- vi.clearAllMocks()
260
- mockForcePseudoState.mockImplementation(async () => vi.fn())
261
- })
283
+ vi.clearAllMocks();
284
+ mockForcePseudoState.mockImplementation(async () => vi.fn());
285
+ });
262
286
 
263
- it('combines config and group path prefixes and uses the most specific representative URL', async () => {
264
- const config = makeConfig({pathPrefix: '/site', representativeUrl: 'https://example.com/config'} as any)
265
- const group = makeGroup({pathPrefix: '/section', representativeUrl: 'https://example.com/group'})
266
- const testCase = makeTestCase({path: '/page', representativeUrl: 'https://example.com/case'})
287
+ it("combines config and group path prefixes and uses the most specific representative URL", async () => {
288
+ const config = makeConfig({
289
+ pathPrefix: "/site",
290
+ representativeUrl: "https://example.com/config",
291
+ } as any);
292
+ const group = makeGroup({
293
+ pathPrefix: "/section",
294
+ representativeUrl: "https://example.com/group",
295
+ });
296
+ const testCase = makeTestCase({
297
+ path: "/page",
298
+ representativeUrl: "https://example.com/case",
299
+ });
267
300
 
268
- const {mockPage, mockTestInfo} = await runDefaultTestFunction(testCase, group, config)
301
+ const { mockPage, mockTestInfo } = await runDefaultTestFunction(
302
+ testCase,
303
+ group,
304
+ config,
305
+ );
269
306
 
270
- expect(mockPage.goto).toHaveBeenCalledWith('/site/section/page')
307
+ expect(mockPage.goto).toHaveBeenCalledWith("/site/section/page");
271
308
  expect(mockTestInfo.annotations).toContainEqual({
272
- type: 'Representative URL',
273
- description: 'https://example.com/case',
274
- })
275
- })
276
-
277
- it('uses the most specific accessibility baseline and merges it with preset options', async () => {
278
- const configBaseline = [{rule: 'config', targets: ['#config'], reason: 'Known', willBeFixedIn: 'PROJ-1'}]
279
- const groupBaseline = [{rule: 'group', targets: ['#group'], reason: 'Known', willBeFixedIn: 'PROJ-2'}]
280
- const caseBaseline = [{rule: 'case', targets: ['#case'], reason: 'Known', willBeFixedIn: 'PROJ-3'}]
309
+ type: "Representative URL",
310
+ description: "https://example.com/case",
311
+ });
312
+ });
313
+
314
+ it("uses the most specific accessibility baseline and merges it with preset options", async () => {
315
+ const configBaseline = [
316
+ {
317
+ rule: "config",
318
+ targets: ["#config"],
319
+ reason: "Known",
320
+ willBeFixedIn: "PROJ-1",
321
+ },
322
+ ];
323
+ const groupBaseline = [
324
+ {
325
+ rule: "group",
326
+ targets: ["#group"],
327
+ reason: "Known",
328
+ willBeFixedIn: "PROJ-2",
329
+ },
330
+ ];
331
+ const caseBaseline = [
332
+ {
333
+ rule: "case",
334
+ targets: ["#case"],
335
+ reason: "Known",
336
+ willBeFixedIn: "PROJ-3",
337
+ },
338
+ ];
281
339
  const preset = {
282
340
  screenshotOptions: vi.fn().mockResolvedValue({
283
341
  threshold: 0.4,
284
- accessibility: {wcagExclude: ['.adapter-owned']},
342
+ accessibility: { wcagExclude: [".adapter-owned"] },
285
343
  }),
286
- }
344
+ };
287
345
 
288
346
  await runDefaultTestFunction(
289
- makeTestCase({a11yBaseline: caseBaseline} as any),
290
- makeGroup({a11yBaseline: groupBaseline} as any),
291
- makeConfig({a11yBaseline: configBaseline}),
347
+ makeTestCase({ a11yBaseline: caseBaseline } as any),
348
+ makeGroup({ a11yBaseline: groupBaseline } as any),
349
+ makeConfig({ a11yBaseline: configBaseline }),
292
350
  preset,
293
- )
351
+ );
294
352
 
295
- const options = mockTakeAccessibleScreenshot.mock.calls[0][2]
296
- expect(options.threshold).toBe(0.4)
353
+ const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
354
+ expect(options.threshold).toBe(0.4);
297
355
  expect(options.accessibility).toEqual({
298
- wcagExclude: ['.adapter-owned'],
356
+ wcagExclude: [".adapter-owned"],
299
357
  baseline: caseBaseline,
300
- })
301
- })
302
-
303
- it('falls back to group-level representative URL and accessibility baseline', async () => {
304
- const configBaseline = [{rule: 'config', targets: ['#config'], reason: 'Known', willBeFixedIn: 'PROJ-1'}]
305
- const groupBaseline = [{rule: 'group', targets: ['#group'], reason: 'Known', willBeFixedIn: 'PROJ-2'}]
306
-
307
- const {mockTestInfo} = await runDefaultTestFunction(
358
+ });
359
+ });
360
+
361
+ it("falls back to group-level representative URL and accessibility baseline", async () => {
362
+ const configBaseline = [
363
+ {
364
+ rule: "config",
365
+ targets: ["#config"],
366
+ reason: "Known",
367
+ willBeFixedIn: "PROJ-1",
368
+ },
369
+ ];
370
+ const groupBaseline = [
371
+ {
372
+ rule: "group",
373
+ targets: ["#group"],
374
+ reason: "Known",
375
+ willBeFixedIn: "PROJ-2",
376
+ },
377
+ ];
378
+
379
+ const { mockTestInfo } = await runDefaultTestFunction(
308
380
  makeTestCase(),
309
381
  makeGroup({
310
- representativeUrl: 'https://example.com/group',
382
+ representativeUrl: "https://example.com/group",
311
383
  a11yBaseline: groupBaseline,
312
384
  }),
313
385
  makeConfig({
314
- representativeUrl: 'https://example.com/config',
386
+ representativeUrl: "https://example.com/config",
315
387
  a11yBaseline: configBaseline,
316
388
  }),
317
- )
389
+ );
318
390
 
319
391
  expect(mockTestInfo.annotations).toContainEqual({
320
- type: 'Representative URL',
321
- description: 'https://example.com/group',
322
- })
323
- expect(mockTakeAccessibleScreenshot.mock.calls[0][2].accessibility.baseline)
324
- .toBe(groupBaseline)
325
- })
326
-
327
- it('installs a case mock before navigation', async () => {
328
- const mock = vi.fn().mockResolvedValue(undefined)
329
- class CaseMock { mock = mock }
330
-
331
- const {mockPage} = await runDefaultTestFunction(
332
- makeTestCase({mockClass: CaseMock}),
333
- makeGroup(),
334
- )
335
-
336
- expect(mock).toHaveBeenCalledWith(mockPage)
337
- expect(mock.mock.invocationCallOrder[0]).toBeLessThan(mockPage.goto.mock.invocationCallOrder[0])
338
- })
392
+ type: "Representative URL",
393
+ description: "https://example.com/group",
394
+ });
395
+ expect(
396
+ mockTakeAccessibleScreenshot.mock.calls[0][2].accessibility.baseline,
397
+ ).toBe(groupBaseline);
398
+ });
399
+
400
+ it("installs a case mock before navigation", async () => {
401
+ const mock = vi.fn().mockResolvedValue(undefined);
402
+ class CaseMock {
403
+ mock = mock;
404
+ }
339
405
 
340
- it('falls back to a group mock when a case does not define one', async () => {
341
- const mock = vi.fn().mockResolvedValue(undefined)
342
- class GroupMock { mock = mock }
406
+ const { mockPage } = await runDefaultTestFunction(
407
+ makeTestCase({ mockClass: CaseMock }),
408
+ makeGroup(),
409
+ );
410
+
411
+ expect(mock).toHaveBeenCalledWith(mockPage);
412
+ expect(mock.mock.invocationCallOrder[0]).toBeLessThan(
413
+ mockPage.goto.mock.invocationCallOrder[0],
414
+ );
415
+ });
416
+
417
+ it("falls back to a group mock when a case does not define one", async () => {
418
+ const mock = vi.fn().mockResolvedValue(undefined);
419
+ class GroupMock {
420
+ mock = mock;
421
+ }
343
422
 
344
- const {mockPage} = await runDefaultTestFunction(
423
+ const { mockPage } = await runDefaultTestFunction(
345
424
  makeTestCase(),
346
- makeGroup({mockClass: GroupMock}),
347
- )
425
+ makeGroup({ mockClass: GroupMock }),
426
+ );
348
427
 
349
- expect(mock).toHaveBeenCalledWith(mockPage)
350
- })
428
+ expect(mock).toHaveBeenCalledWith(mockPage);
429
+ });
351
430
 
352
- it('forwards defineVisualDiffConfig preset options into generated tests', async () => {
353
- const preset = {screenshotOptions: {threshold: 0.25}}
354
- const visualTests = defineVisualDiffConfig(makeConfig({
355
- groups: [makeGroup({testCases: [makeTestCase()]})],
356
- }), preset)
357
- visualTests.describe()
431
+ it("forwards defineVisualDiffConfig preset options into generated tests", async () => {
432
+ const preset = { screenshotOptions: { threshold: 0.25 } };
433
+ const visualTests = defineVisualDiffConfig(
434
+ makeConfig({
435
+ groups: [makeGroup({ testCases: [makeTestCase()] })],
436
+ }),
437
+ preset,
438
+ );
439
+ visualTests.describe();
358
440
 
359
- const generatedTest = mockTest.mock.calls[0][1]
360
- const page = {goto: vi.fn(), locator: vi.fn()}
361
- const context = {on: vi.fn()}
362
- const testInfo = {annotations: []}
363
- await generatedTest({page, context}, testInfo)
441
+ const generatedTest = mockTest.mock.calls[0][1];
442
+ const page = { goto: vi.fn(), locator: vi.fn() };
443
+ const context = { on: vi.fn() };
444
+ const testInfo = { annotations: [] };
445
+ await generatedTest({ page, context }, testInfo);
364
446
 
365
447
  expect(mockTakeAccessibleScreenshot.mock.calls[0][2]).toMatchObject({
366
448
  fullPage: true,
367
449
  threshold: 0.25,
368
- })
369
- })
370
- })
450
+ });
451
+ });
452
+ });
371
453
 
372
- describe('VisualDiffTestCases.describe skip semantics', () => {
373
- beforeEach(() => vi.clearAllMocks())
454
+ describe("VisualDiffTestCases.describe skip semantics", () => {
455
+ beforeEach(() => vi.clearAllMocks());
374
456
 
375
- it('registers skipped groups and cases with their reason and tracking reference', () => {
376
- const config = makeConfig({groups: [
377
- makeGroup({
378
- name: 'Skipped group',
379
- skip: {reason: 'Unavailable', willBeFixedIn: 'PROJ-10'},
380
- }),
381
- makeGroup({
382
- name: 'Active group',
383
- testCases: [makeTestCase({
384
- name: 'Skipped case',
385
- skip: {reason: 'Flaky', willBeFixedIn: 'PROJ-11'},
386
- })],
387
- }),
388
- ]})
389
-
390
- const visualTests = new VisualDiffTestCases(config)
391
- visualTests.describe()
457
+ it("registers skipped groups and cases with their reason and tracking reference", () => {
458
+ const config = makeConfig({
459
+ groups: [
460
+ makeGroup({
461
+ name: "Skipped group",
462
+ skip: { reason: "Unavailable", willBeFixedIn: "PROJ-10" },
463
+ }),
464
+ makeGroup({
465
+ name: "Active group",
466
+ testCases: [
467
+ makeTestCase({
468
+ name: "Skipped case",
469
+ skip: { reason: "Flaky", willBeFixedIn: "PROJ-11" },
470
+ }),
471
+ ],
472
+ }),
473
+ ],
474
+ });
475
+
476
+ const visualTests = new VisualDiffTestCases(config);
477
+ visualTests.describe();
392
478
 
393
479
  expect(mockSkip).toHaveBeenCalledWith(
394
- 'Skipped group: Unavailable <PROJ-10>',
480
+ "Skipped group: Unavailable <PROJ-10>",
395
481
  expect.any(Function),
396
- )
482
+ );
397
483
  expect(mockSkip).toHaveBeenCalledWith(
398
- 'Skipped case: Flaky <PROJ-11>',
484
+ "Skipped case: Flaky <PROJ-11>",
485
+ expect.any(Function),
486
+ );
487
+ });
488
+
489
+ it("defines a test when a conditional skip callback returns false", () => {
490
+ const config = makeConfig({
491
+ groups: [
492
+ makeGroup({
493
+ name: "Conditional group",
494
+ skip: {
495
+ reason: "Conditional",
496
+ willBeFixedIn: "PROJ-12",
497
+ callback: () => false,
498
+ },
499
+ testCases: [makeTestCase()],
500
+ }),
501
+ ],
502
+ });
503
+
504
+ const visualTests = new VisualDiffTestCases(config);
505
+ visualTests.describe();
506
+
507
+ expect(mockSkip).not.toHaveBeenCalled();
508
+ expect(mockDescribe).toHaveBeenCalledWith(
509
+ "Conditional group",
399
510
  expect.any(Function),
400
- )
401
- })
402
-
403
- it('defines a test when a conditional skip callback returns false', () => {
404
- const config = makeConfig({groups: [makeGroup({
405
- name: 'Conditional group',
406
- skip: {reason: 'Conditional', willBeFixedIn: 'PROJ-12', callback: () => false},
407
- testCases: [makeTestCase()],
408
- })]})
409
-
410
- const visualTests = new VisualDiffTestCases(config)
411
- visualTests.describe()
412
-
413
- expect(mockSkip).not.toHaveBeenCalled()
414
- expect(mockDescribe).toHaveBeenCalledWith('Conditional group', expect.any(Function))
415
- expect(mockTest).toHaveBeenCalledWith('Test: /test', expect.any(Function))
416
- })
417
- })
418
-
419
- describe('defaultTestFunction interaction-state handling', () => {
511
+ );
512
+ expect(mockTest).toHaveBeenCalledWith("Test: /test", expect.any(Function));
513
+ });
514
+ });
515
+
516
+ describe("defaultTestFunction interaction-state handling", () => {
420
517
  beforeEach(() => {
421
- vi.clearAllMocks()
422
- })
518
+ vi.clearAllMocks();
519
+ });
423
520
 
424
- it('merges cross-browser interaction states from all three levels', async () => {
521
+ it("merges cross-browser interaction states from all three levels", async () => {
425
522
  const config = makeConfig({
426
- interactionStates: [{selector: '.nav', states: ['hover']}],
427
- })
523
+ interactionStates: [{ selector: ".nav", states: ["hover"] }],
524
+ });
428
525
  const group = makeGroup({
429
- interactionStates: [{selector: '.menu-item', states: ['focus']}],
430
- })
526
+ interactionStates: [{ selector: ".menu-item", states: ["focus"] }],
527
+ });
431
528
  const testCase = makeTestCase({
432
- interactionStates: [{selector: '.trigger', states: ['hover', 'focus']}],
433
- })
434
-
435
- const {mockPage} = await runDefaultTestFunction(testCase, group, config)
436
-
437
- expect(mockTakeAccessibleScreenshot.mock.calls[0][2].interactionStates).toEqual([
438
- {locator: {_selector: '.nav'}, states: ['hover']},
439
- {locator: {_selector: '.menu-item'}, states: ['focus']},
440
- {locator: {_selector: '.trigger'}, states: ['hover', 'focus']},
441
- ])
442
- expect(mockPage.locator).toHaveBeenCalledWith('.nav')
443
- expect(mockPage.locator).toHaveBeenCalledWith('.menu-item')
444
- expect(mockPage.locator).toHaveBeenCalledWith('.trigger')
445
- })
446
-
447
- it('omits interactionStates when none are configured', async () => {
448
- await runDefaultTestFunction(makeTestCase(), makeGroup())
449
-
450
- expect(mockTakeAccessibleScreenshot.mock.calls[0][2].interactionStates).toBeUndefined()
451
- })
452
- })
529
+ interactionStates: [{ selector: ".trigger", states: ["hover", "focus"] }],
530
+ });
531
+
532
+ const { mockPage } = await runDefaultTestFunction(testCase, group, config);
533
+
534
+ expect(
535
+ mockTakeAccessibleScreenshot.mock.calls[0][2].interactionStates,
536
+ ).toEqual([
537
+ { locator: { _selector: ".nav" }, states: ["hover"] },
538
+ { locator: { _selector: ".menu-item" }, states: ["focus"] },
539
+ { locator: { _selector: ".trigger" }, states: ["hover", "focus"] },
540
+ ]);
541
+ expect(mockPage.locator).toHaveBeenCalledWith(".nav");
542
+ expect(mockPage.locator).toHaveBeenCalledWith(".menu-item");
543
+ expect(mockPage.locator).toHaveBeenCalledWith(".trigger");
544
+ });
545
+
546
+ it("omits interactionStates when none are configured", async () => {
547
+ await runDefaultTestFunction(makeTestCase(), makeGroup());
548
+
549
+ expect(
550
+ mockTakeAccessibleScreenshot.mock.calls[0][2].interactionStates,
551
+ ).toBeUndefined();
552
+ });
553
+ });