@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
package/src/visualdiff.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from
|
|
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
|
|
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) =>
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
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(
|
|
26
|
-
takeAccessibleScreenshot: (...args: any[]) =>
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
35
|
-
path:
|
|
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:
|
|
45
|
+
name: "Group",
|
|
43
46
|
testCases: [],
|
|
44
47
|
...overrides,
|
|
45
|
-
}
|
|
48
|
+
};
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
function makeConfig(
|
|
51
|
+
function makeConfig(
|
|
52
|
+
overrides?: Partial<VisualDiffUrlConfig>,
|
|
53
|
+
): VisualDiffUrlConfig {
|
|
49
54
|
return {
|
|
50
|
-
name:
|
|
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(
|
|
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(
|
|
88
|
-
const config = makeConfig({ mask: [
|
|
89
|
-
const group = makeGroup({ mask: [
|
|
90
|
-
const testCase = makeTestCase({ 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:
|
|
97
|
-
expect(options.mask[1]).toEqual({ _selector:
|
|
98
|
-
expect(options.mask[2]).toEqual({ _selector:
|
|
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(
|
|
102
|
-
const group = makeGroup({ mask: [
|
|
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:
|
|
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(
|
|
113
|
-
const config = makeConfig({ mask: [
|
|
114
|
-
const group = makeGroup() // no mask
|
|
115
|
-
const testCase = makeTestCase({ mask: [
|
|
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:
|
|
122
|
-
expect(options.mask[1]).toEqual({ _selector:
|
|
123
|
-
expect(options.mask[2]).toEqual({ _selector:
|
|
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(
|
|
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(
|
|
138
|
-
const config = makeConfig({ maskColor:
|
|
139
|
-
const group = makeGroup({ maskColor:
|
|
140
|
-
const testCase = makeTestCase({ maskColor:
|
|
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(
|
|
146
|
-
})
|
|
149
|
+
const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
|
|
150
|
+
expect(options.maskColor).toBe("#333");
|
|
151
|
+
});
|
|
147
152
|
|
|
148
|
-
it(
|
|
149
|
-
const config = makeConfig({ maskColor:
|
|
150
|
-
const group = makeGroup({ maskColor:
|
|
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(
|
|
157
|
-
})
|
|
160
|
+
const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
|
|
161
|
+
expect(options.maskColor).toBe("#222");
|
|
162
|
+
});
|
|
158
163
|
|
|
159
|
-
it(
|
|
160
|
-
const config = makeConfig({ maskColor:
|
|
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(
|
|
168
|
-
})
|
|
171
|
+
const options = mockTakeAccessibleScreenshot.mock.calls[0][2];
|
|
172
|
+
expect(options.maskColor).toBe("#111");
|
|
173
|
+
});
|
|
169
174
|
|
|
170
|
-
it(
|
|
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(
|
|
186
|
+
describe("defaultTestFunction pseudo-state handling", () => {
|
|
182
187
|
beforeEach(() => {
|
|
183
|
-
vi.clearAllMocks()
|
|
184
|
-
mockForcePseudoState.mockImplementation(async () => vi.fn())
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it(
|
|
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:
|
|
197
|
-
})
|
|
201
|
+
pseudoStates: [{ selector: ".nav", pseudoClasses: ["hover"] }],
|
|
202
|
+
});
|
|
198
203
|
const group = makeGroup({
|
|
199
|
-
pseudoStates: [{selector:
|
|
200
|
-
})
|
|
204
|
+
pseudoStates: [{ selector: ".menu", pseudoClasses: ["focus-within"] }],
|
|
205
|
+
});
|
|
201
206
|
const testCase = makeTestCase({
|
|
202
|
-
pseudoStates: [{selector:
|
|
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,
|
|
209
|
-
[mockPage,
|
|
210
|
-
[mockPage,
|
|
211
|
-
])
|
|
212
|
-
expect(mockTakeAccessibleScreenshot).toHaveBeenCalledOnce()
|
|
213
|
-
expect(clearTestState.mock.invocationCallOrder[0])
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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:
|
|
227
|
-
})
|
|
236
|
+
pseudoStates: [{ selector: "button", pseudoClasses: ["focus"] }],
|
|
237
|
+
});
|
|
228
238
|
|
|
229
|
-
await expect(runDefaultTestFunction(testCase, makeGroup())).rejects.toThrow(
|
|
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(
|
|
235
|
-
const events: string[] = []
|
|
236
|
-
const clearConfigState = vi.fn(async () => {
|
|
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(
|
|
239
|
-
throw new Error(
|
|
240
|
-
})
|
|
241
|
-
const clearTestState = vi.fn(async () => {
|
|
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(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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(
|
|
264
|
-
const config = makeConfig({
|
|
265
|
-
|
|
266
|
-
|
|
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(
|
|
301
|
+
const { mockPage, mockTestInfo } = await runDefaultTestFunction(
|
|
302
|
+
testCase,
|
|
303
|
+
group,
|
|
304
|
+
config,
|
|
305
|
+
);
|
|
269
306
|
|
|
270
|
-
expect(mockPage.goto).toHaveBeenCalledWith(
|
|
307
|
+
expect(mockPage.goto).toHaveBeenCalledWith("/site/section/page");
|
|
271
308
|
expect(mockTestInfo.annotations).toContainEqual({
|
|
272
|
-
type:
|
|
273
|
-
description:
|
|
274
|
-
})
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
it(
|
|
278
|
-
const configBaseline = [
|
|
279
|
-
|
|
280
|
-
|
|
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: [
|
|
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: [
|
|
356
|
+
wcagExclude: [".adapter-owned"],
|
|
299
357
|
baseline: caseBaseline,
|
|
300
|
-
})
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it(
|
|
304
|
-
const configBaseline = [
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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:
|
|
382
|
+
representativeUrl: "https://example.com/group",
|
|
311
383
|
a11yBaseline: groupBaseline,
|
|
312
384
|
}),
|
|
313
385
|
makeConfig({
|
|
314
|
-
representativeUrl:
|
|
386
|
+
representativeUrl: "https://example.com/config",
|
|
315
387
|
a11yBaseline: configBaseline,
|
|
316
388
|
}),
|
|
317
|
-
)
|
|
389
|
+
);
|
|
318
390
|
|
|
319
391
|
expect(mockTestInfo.annotations).toContainEqual({
|
|
320
|
-
type:
|
|
321
|
-
description:
|
|
322
|
-
})
|
|
323
|
-
expect(
|
|
324
|
-
.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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(
|
|
353
|
-
const preset = {screenshotOptions: {threshold: 0.25}}
|
|
354
|
-
const visualTests = defineVisualDiffConfig(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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(
|
|
373
|
-
beforeEach(() => vi.clearAllMocks())
|
|
454
|
+
describe("VisualDiffTestCases.describe skip semantics", () => {
|
|
455
|
+
beforeEach(() => vi.clearAllMocks());
|
|
374
456
|
|
|
375
|
-
it(
|
|
376
|
-
const config = makeConfig({
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
480
|
+
"Skipped group: Unavailable <PROJ-10>",
|
|
395
481
|
expect.any(Function),
|
|
396
|
-
)
|
|
482
|
+
);
|
|
397
483
|
expect(mockSkip).toHaveBeenCalledWith(
|
|
398
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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(
|
|
521
|
+
it("merges cross-browser interaction states from all three levels", async () => {
|
|
425
522
|
const config = makeConfig({
|
|
426
|
-
interactionStates: [{selector:
|
|
427
|
-
})
|
|
523
|
+
interactionStates: [{ selector: ".nav", states: ["hover"] }],
|
|
524
|
+
});
|
|
428
525
|
const group = makeGroup({
|
|
429
|
-
interactionStates: [{selector:
|
|
430
|
-
})
|
|
526
|
+
interactionStates: [{ selector: ".menu-item", states: ["focus"] }],
|
|
527
|
+
});
|
|
431
528
|
const testCase = makeTestCase({
|
|
432
|
-
interactionStates: [{selector:
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
const {mockPage} = await runDefaultTestFunction(testCase, group, config)
|
|
436
|
-
|
|
437
|
-
expect(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
{locator: {_selector:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
expect(mockPage.locator).toHaveBeenCalledWith(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
+
});
|