@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.
- package/bin/github-a11y-summary +2 -2
- package/bin/github-failure-summary +4 -4
- package/lib/accessibility-baseline-file.d.ts +6 -6
- package/lib/accessibility-baseline-file.js +29 -23
- package/lib/accessibility-baseline.js +5 -3
- package/lib/accessible-screenshot.d.ts +4 -4
- package/lib/accessible-screenshot.js +110 -88
- package/lib/focus.js +4 -1
- package/lib/frames.js +4 -4
- package/lib/github/a11y-summary.js +74 -79
- package/lib/github/attachments.d.ts +1 -1
- package/lib/github/attachments.js +36 -34
- package/lib/github/failure-summary.d.ts +4 -4
- package/lib/github/failure-summary.js +140 -108
- package/lib/github/index.d.ts +6 -6
- package/lib/github/report-paths.js +17 -9
- package/lib/hover.js +17 -17
- package/lib/images.js +15 -12
- package/lib/index.d.ts +13 -13
- package/lib/interaction-states.d.ts +2 -2
- package/lib/interaction-states.js +7 -7
- package/lib/mock/index.d.ts +1 -1
- package/lib/mock/youtube.d.ts +1 -1
- package/lib/mock/youtube.js +1 -1
- package/lib/pseudo-state.d.ts +2 -2
- package/lib/pseudo-state.js +15 -7
- package/lib/videos.js +21 -10
- package/lib/visualdiff.d.ts +4 -4
- package/lib/visualdiff.js +35 -27
- package/package.json +3 -3
- package/src/accessibility-baseline-file.test.ts +190 -154
- package/src/accessibility-baseline-file.ts +107 -83
- package/src/accessibility-baseline.test.ts +869 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +358 -251
- package/src/focus.test.ts +23 -23
- package/src/focus.ts +7 -2
- package/src/fonts.test.ts +13 -13
- package/src/fonts.ts +1 -1
- package/src/frames.test.ts +10 -8
- package/src/frames.ts +5 -5
- package/src/github/a11y-summary.test.ts +436 -332
- package/src/github/a11y-summary.ts +189 -179
- package/src/github/attachments.test.ts +228 -167
- package/src/github/attachments.ts +144 -121
- package/src/github/failure-summary.test.ts +660 -445
- package/src/github/failure-summary.ts +376 -274
- package/src/github/index.test.ts +14 -14
- package/src/github/index.ts +6 -6
- package/src/github/report-paths.test.ts +211 -156
- package/src/github/report-paths.ts +75 -61
- package/src/hover.test.ts +50 -46
- package/src/hover.ts +18 -18
- package/src/images.test.ts +251 -206
- package/src/images.ts +52 -43
- package/src/index.ts +13 -13
- package/src/interaction-states.test.ts +72 -42
- package/src/interaction-states.ts +43 -29
- package/src/mock/index.ts +1 -1
- package/src/mock/youtube.test.ts +26 -26
- package/src/mock/youtube.ts +4 -6
- package/src/pseudo-state.test.ts +58 -51
- package/src/pseudo-state.ts +32 -27
- package/src/videos.test.ts +559 -399
- package/src/videos.ts +68 -44
- package/src/visualdiff.test.ts +416 -315
- package/src/visualdiff.ts +140 -99
|
@@ -1,52 +1,65 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from
|
|
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 } =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
18
|
+
return {
|
|
19
|
+
mockWithTags,
|
|
20
|
+
mockExclude,
|
|
21
|
+
mockOptions,
|
|
22
|
+
mockAnalyze,
|
|
23
|
+
MockAxeBuilder,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
20
26
|
|
|
21
|
-
vi.mock(
|
|
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 {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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(
|
|
72
|
-
vi.mock(
|
|
73
|
-
vi.mock(
|
|
74
|
-
vi.mock(
|
|
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(
|
|
79
|
-
vi.mock(
|
|
80
|
-
|
|
81
|
-
import {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
98
|
-
titlePath: [
|
|
99
|
-
annotations: [] as Array<{ type: 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[]) =>
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
125
|
+
describe("checkAccessibility", () => {
|
|
108
126
|
beforeEach(() => {
|
|
109
|
-
vi.clearAllMocks()
|
|
110
|
-
mockAnalyze.mockResolvedValue(makeAxeResults())
|
|
111
|
-
mockClearHover.mockResolvedValue(mockRemoveHoverShield)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it(
|
|
115
|
-
const testInfo = makeTestInfo()
|
|
116
|
-
await checkAccessibility({} as any, testInfo as any)
|
|
117
|
-
|
|
118
|
-
expect(mockWithTags).toHaveBeenCalledWith([
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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: [
|
|
125
|
-
})
|
|
147
|
+
wcagTags: ["wcag22aa"],
|
|
148
|
+
});
|
|
126
149
|
|
|
127
|
-
expect(mockWithTags).toHaveBeenCalledWith([
|
|
128
|
-
})
|
|
150
|
+
expect(mockWithTags).toHaveBeenCalledWith(["wcag22aa"]);
|
|
151
|
+
});
|
|
129
152
|
|
|
130
|
-
it(
|
|
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: [
|
|
134
|
-
})
|
|
156
|
+
exclude: [".my-selector", "#another"],
|
|
157
|
+
});
|
|
135
158
|
|
|
136
|
-
expect(mockExclude).toHaveBeenCalledWith(
|
|
137
|
-
expect(mockExclude).toHaveBeenCalledWith(
|
|
138
|
-
})
|
|
159
|
+
expect(mockExclude).toHaveBeenCalledWith(".my-selector");
|
|
160
|
+
expect(mockExclude).toHaveBeenCalledWith("#another");
|
|
161
|
+
});
|
|
139
162
|
|
|
140
|
-
it(
|
|
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:
|
|
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([
|
|
149
|
-
expect(mockWithTags).toHaveBeenCalledWith([
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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: [
|
|
183
|
-
wcagExclude: [
|
|
184
|
-
})
|
|
210
|
+
bestPracticeExclude: [".best-practice-only"],
|
|
211
|
+
wcagExclude: [".wcag-only"],
|
|
212
|
+
});
|
|
185
213
|
|
|
186
|
-
expect(mockExclude).toHaveBeenCalledWith(
|
|
187
|
-
expect(mockExclude).toHaveBeenCalledWith(
|
|
188
|
-
})
|
|
214
|
+
expect(mockExclude).toHaveBeenCalledWith(".best-practice-only");
|
|
215
|
+
expect(mockExclude).toHaveBeenCalledWith(".wcag-only");
|
|
216
|
+
});
|
|
189
217
|
|
|
190
|
-
it(
|
|
191
|
-
const testInfo = makeTestInfo()
|
|
192
|
-
const rules = {
|
|
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(
|
|
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(
|
|
205
|
-
|
|
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(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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:
|
|
216
|
-
passes: [{ id:
|
|
217
|
-
})
|
|
247
|
+
violations: [{ id: "rule1", nodes: [] }],
|
|
248
|
+
passes: [{ id: "rule2" }, { id: "rule3" }],
|
|
249
|
+
});
|
|
218
250
|
const wcagResults = makeAxeResults({
|
|
219
251
|
violations: [],
|
|
220
|
-
passes: [{ id:
|
|
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(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
expect(accessibilityAnnotations
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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:
|
|
240
|
-
})
|
|
277
|
+
bestPracticeMode: "off",
|
|
278
|
+
});
|
|
241
279
|
|
|
242
|
-
const accessibilityAnnotations = testInfo.annotations.filter(
|
|
243
|
-
|
|
244
|
-
|
|
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(
|
|
287
|
+
it("attaches a violation screenshot when screenshotViolations is true and violations exist", async () => {
|
|
248
288
|
const wcagResults = makeAxeResults({
|
|
249
|
-
violations: [
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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)
|
|
302
|
+
.mockResolvedValueOnce(wcagResults); // WCAG
|
|
261
303
|
|
|
262
304
|
const mockPage = {
|
|
263
305
|
evaluate: vi.fn().mockResolvedValue(undefined),
|
|
264
|
-
screenshot: vi.fn().mockResolvedValue(Buffer.from(
|
|
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(
|
|
278
|
-
body: Buffer.from(
|
|
279
|
-
contentType:
|
|
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(
|
|
325
|
+
it("removes violation highlights when attaching the screenshot fails", async () => {
|
|
284
326
|
const wcagResults = makeAxeResults({
|
|
285
|
-
violations: [
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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(
|
|
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 ===
|
|
303
|
-
? Promise.reject(new Error(
|
|
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(
|
|
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(
|
|
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(
|
|
372
|
+
it("takes a screenshot by default when violations exist", async () => {
|
|
327
373
|
const wcagResults = makeAxeResults({
|
|
328
|
-
violations: [
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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)
|
|
387
|
+
.mockResolvedValueOnce(wcagResults); // WCAG
|
|
340
388
|
|
|
341
389
|
const mockPage = {
|
|
342
390
|
evaluate: vi.fn().mockResolvedValue(undefined),
|
|
343
|
-
screenshot: vi.fn().mockResolvedValue(Buffer.from(
|
|
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(
|
|
400
|
+
it("does not take a screenshot when screenshotViolations is explicitly false", async () => {
|
|
353
401
|
const wcagResults = makeAxeResults({
|
|
354
|
-
violations: [
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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)
|
|
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(
|
|
431
|
+
describe("takeAccessibleScreenshot hover handling", () => {
|
|
382
432
|
beforeEach(() => {
|
|
383
|
-
vi.clearAllMocks()
|
|
384
|
-
mockAnalyze.mockResolvedValue(makeAxeResults())
|
|
385
|
-
mockClearHover.mockResolvedValue(mockRemoveHoverShield)
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
it(
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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(
|
|
416
|
-
const testInfo = makeTestInfo()
|
|
417
|
-
testInfo.project.name =
|
|
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(
|
|
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(
|
|
439
|
-
const page = {} as any
|
|
440
|
-
const testInfo = makeTestInfo()
|
|
441
|
-
mockToHaveScreenshot.mockRejectedValueOnce(new Error(
|
|
442
|
-
|
|
443
|
-
await expect(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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: [
|
|
463
|
-
{locator: focusLocator as any, states: [
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
expect(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
expect(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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(
|
|
493
|
-
|
|
494
|
-
await expect(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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(
|
|
575
|
+
hover: vi.fn().mockRejectedValue(new Error("hover failed")),
|
|
511
576
|
focus: vi.fn(),
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
await expect(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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 = {
|
|
548
|
-
|
|
549
|
-
|
|
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, {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
expect(
|
|
560
|
-
|
|
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(
|
|
565
|
-
const {page, locator} = fixture()
|
|
566
|
-
await expect(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
+
});
|