@lullabot/playwright-testing 0.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/README.md +61 -0
- package/bin/github-a11y-summary +5 -0
- package/bin/github-failure-summary +8 -0
- package/lib/accessibility-baseline-file.d.ts +47 -0
- package/lib/accessibility-baseline-file.js +205 -0
- package/lib/accessibility-baseline.d.ts +19 -0
- package/lib/accessibility-baseline.js +38 -0
- package/lib/accessible-screenshot.d.ts +181 -0
- package/lib/accessible-screenshot.js +519 -0
- package/lib/focus.d.ts +14 -0
- package/lib/focus.js +28 -0
- package/lib/fonts.d.ts +18 -0
- package/lib/fonts.js +24 -0
- package/lib/frames.d.ts +7 -0
- package/lib/frames.js +27 -0
- package/lib/github/a11y-summary.d.ts +55 -0
- package/lib/github/a11y-summary.js +383 -0
- package/lib/github/attachments.d.ts +98 -0
- package/lib/github/attachments.js +297 -0
- package/lib/github/failure-summary.d.ts +144 -0
- package/lib/github/failure-summary.js +567 -0
- package/lib/github/index.d.ts +6 -0
- package/lib/github/index.js +35 -0
- package/lib/github/report-paths.d.ts +38 -0
- package/lib/github/report-paths.js +200 -0
- package/lib/hover.d.ts +13 -0
- package/lib/hover.js +61 -0
- package/lib/images.d.ts +118 -0
- package/lib/images.js +260 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +29 -0
- package/lib/interaction-states.d.ts +22 -0
- package/lib/interaction-states.js +75 -0
- package/lib/mock/index.d.ts +1 -0
- package/lib/mock/index.js +5 -0
- package/lib/mock/youtube.d.ts +5 -0
- package/lib/mock/youtube.js +38 -0
- package/lib/pseudo-state.d.ts +17 -0
- package/lib/pseudo-state.js +50 -0
- package/lib/videos.d.ts +134 -0
- package/lib/videos.js +349 -0
- package/lib/visualdiff.d.ts +154 -0
- package/lib/visualdiff.js +197 -0
- package/package.json +47 -0
- package/src/accessibility-baseline-file.test.ts +181 -0
- package/src/accessibility-baseline-file.ts +208 -0
- package/src/accessibility-baseline.test.ts +601 -0
- package/src/accessibility-baseline.ts +50 -0
- package/src/accessible-screenshot.test.ts +597 -0
- package/src/accessible-screenshot.ts +809 -0
- package/src/focus.test.ts +34 -0
- package/src/focus.ts +27 -0
- package/src/fonts.test.ts +17 -0
- package/src/fonts.ts +23 -0
- package/src/frames.test.ts +75 -0
- package/src/frames.ts +26 -0
- package/src/github/a11y-summary.test.ts +439 -0
- package/src/github/a11y-summary.ts +421 -0
- package/src/github/attachments.test.ts +248 -0
- package/src/github/attachments.ts +328 -0
- package/src/github/failure-summary.test.ts +636 -0
- package/src/github/failure-summary.ts +720 -0
- package/src/github/index.test.ts +24 -0
- package/src/github/index.ts +35 -0
- package/src/github/report-paths.test.ts +222 -0
- package/src/github/report-paths.ts +208 -0
- package/src/hover.test.ts +76 -0
- package/src/hover.ts +64 -0
- package/src/images.test.ts +355 -0
- package/src/images.ts +299 -0
- package/src/index.ts +13 -0
- package/src/interaction-states.test.ts +48 -0
- package/src/interaction-states.ts +94 -0
- package/src/mock/index.ts +1 -0
- package/src/mock/youtube.test.ts +38 -0
- package/src/mock/youtube.ts +39 -0
- package/src/pseudo-state.test.ts +83 -0
- package/src/pseudo-state.ts +69 -0
- package/src/videos.test.ts +637 -0
- package/src/videos.ts +389 -0
- package/src/visualdiff.test.ts +452 -0
- package/src/visualdiff.ts +381 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import {
|
|
2
|
+
test,
|
|
3
|
+
type BrowserContext,
|
|
4
|
+
type Page,
|
|
5
|
+
type TestInfo,
|
|
6
|
+
type WebError,
|
|
7
|
+
} from '@playwright/test';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
takeAccessibleScreenshot,
|
|
11
|
+
type ScreenshotOptions,
|
|
12
|
+
} from "./accessible-screenshot.js";
|
|
13
|
+
import type {AccessibilityBaseline} from './accessibility-baseline.js'
|
|
14
|
+
import {forcePseudoState, type ForcedPseudoState} from './pseudo-state.js'
|
|
15
|
+
import type {InteractionState} from './interaction-states.js'
|
|
16
|
+
|
|
17
|
+
export interface VisualDiffExecutionContext {
|
|
18
|
+
page: Page
|
|
19
|
+
testInfo: TestInfo
|
|
20
|
+
testCase: VisualDiff
|
|
21
|
+
group: VisualDiffGroup
|
|
22
|
+
config?: VisualDiffUrlConfig
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VisualDiffPreset {
|
|
26
|
+
/**
|
|
27
|
+
* Neutral adapter seam for screenshot defaults such as exclusions, browser
|
|
28
|
+
* thresholds, or post-scroll settling hooks.
|
|
29
|
+
*/
|
|
30
|
+
screenshotOptions?: ScreenshotOptions | (
|
|
31
|
+
(context: VisualDiffExecutionContext) => ScreenshotOptions | Promise<ScreenshotOptions>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function defineVisualDiffConfig(cases: VisualDiffUrlConfig, preset?: VisualDiffPreset) {
|
|
36
|
+
return new VisualDiffTestCases(cases, preset);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function defaultTestFunction(
|
|
40
|
+
testCase: VisualDiff,
|
|
41
|
+
group: VisualDiffGroup,
|
|
42
|
+
config?: VisualDiffUrlConfig,
|
|
43
|
+
preset?: VisualDiffPreset,
|
|
44
|
+
) {
|
|
45
|
+
return async (
|
|
46
|
+
{page, context}: {page: Page, context: BrowserContext},
|
|
47
|
+
testInfo: TestInfo,
|
|
48
|
+
) => {
|
|
49
|
+
const MockClass = testCase.mockClass ?? group.mockClass ?? config?.mockClass
|
|
50
|
+
if (MockClass) {
|
|
51
|
+
const mock = new MockClass();
|
|
52
|
+
await mock.mock(page);
|
|
53
|
+
}
|
|
54
|
+
// Log any errors to the Playwright console too.
|
|
55
|
+
|
|
56
|
+
context.on('weberror', (webError: WebError) => console.log(webError.error()));
|
|
57
|
+
testInfo.annotations.push({
|
|
58
|
+
type: 'Description',
|
|
59
|
+
description: testCase.description,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const representativeUrl = testCase.representativeUrl
|
|
63
|
+
?? group.representativeUrl
|
|
64
|
+
?? config?.representativeUrl;
|
|
65
|
+
|
|
66
|
+
if (representativeUrl) {
|
|
67
|
+
testInfo.annotations.push({
|
|
68
|
+
type: 'Representative URL',
|
|
69
|
+
description: representativeUrl,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const path = `${config?.pathPrefix ?? ''}${group.pathPrefix ?? ''}${testCase.path}`;
|
|
74
|
+
|
|
75
|
+
await page.goto(path);
|
|
76
|
+
|
|
77
|
+
const configuredScreenshotOptions = typeof preset?.screenshotOptions === 'function'
|
|
78
|
+
? await preset.screenshotOptions({page, testInfo, testCase, group, config})
|
|
79
|
+
: preset?.screenshotOptions ?? {}
|
|
80
|
+
|
|
81
|
+
// Merge masks from all three levels: config, group, and testCase.
|
|
82
|
+
const maskSelectors: string[] = [
|
|
83
|
+
...(config?.mask ?? []),
|
|
84
|
+
...(group.mask ?? []),
|
|
85
|
+
...(testCase.mask ?? []),
|
|
86
|
+
];
|
|
87
|
+
const maskLocators = [
|
|
88
|
+
...(configuredScreenshotOptions.mask ?? []),
|
|
89
|
+
...maskSelectors.map(selector => page.locator(selector)),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// Most-specific-wins for maskColor: testCase > group > config.
|
|
93
|
+
const maskColor = testCase.maskColor
|
|
94
|
+
?? group.maskColor
|
|
95
|
+
?? config?.maskColor
|
|
96
|
+
?? configuredScreenshotOptions.maskColor;
|
|
97
|
+
|
|
98
|
+
const screenshotOptions: ScreenshotOptions = {
|
|
99
|
+
...configuredScreenshotOptions,
|
|
100
|
+
fullPage: true,
|
|
101
|
+
};
|
|
102
|
+
if (maskLocators.length > 0) {
|
|
103
|
+
screenshotOptions.mask = maskLocators;
|
|
104
|
+
}
|
|
105
|
+
if (maskColor) {
|
|
106
|
+
screenshotOptions.maskColor = maskColor;
|
|
107
|
+
}
|
|
108
|
+
const a11yBaseline = testCase.a11yBaseline
|
|
109
|
+
?? group.a11yBaseline
|
|
110
|
+
?? config?.a11yBaseline
|
|
111
|
+
?? configuredScreenshotOptions.accessibility?.baseline
|
|
112
|
+
if (a11yBaseline || configuredScreenshotOptions.accessibility) {
|
|
113
|
+
screenshotOptions.accessibility = {
|
|
114
|
+
...configuredScreenshotOptions.accessibility,
|
|
115
|
+
...(a11yBaseline ? {baseline: a11yBaseline} : {}),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const interactionStates: VisualDiffInteractionState[] = [
|
|
120
|
+
...(config?.interactionStates ?? []),
|
|
121
|
+
...(group.interactionStates ?? []),
|
|
122
|
+
...(testCase.interactionStates ?? []),
|
|
123
|
+
];
|
|
124
|
+
if (interactionStates.length > 0 || configuredScreenshotOptions.interactionStates) {
|
|
125
|
+
screenshotOptions.interactionStates = [
|
|
126
|
+
...(configuredScreenshotOptions.interactionStates ?? []),
|
|
127
|
+
...interactionStates.map(({selector, states}) => ({
|
|
128
|
+
locator: page.locator(selector),
|
|
129
|
+
states,
|
|
130
|
+
})),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Force declared pseudo-states after navigation and keep them active
|
|
135
|
+
// through both the screenshot and the accessibility scan. These synthetic
|
|
136
|
+
// states are unaffected by takeAccessibleScreenshot() clearing incidental
|
|
137
|
+
// pointer hover and DOM focus.
|
|
138
|
+
const pseudoStates: ForcedPseudoState[] = [
|
|
139
|
+
...(config?.pseudoStates ?? []),
|
|
140
|
+
...(group.pseudoStates ?? []),
|
|
141
|
+
...(testCase.pseudoStates ?? []),
|
|
142
|
+
];
|
|
143
|
+
const clearPseudoStates: Array<() => Promise<void>> = [];
|
|
144
|
+
let operationFailed = false
|
|
145
|
+
let operationError: unknown
|
|
146
|
+
try {
|
|
147
|
+
for (const pseudoState of pseudoStates) {
|
|
148
|
+
clearPseudoStates.push(await forcePseudoState(
|
|
149
|
+
page,
|
|
150
|
+
pseudoState.selector,
|
|
151
|
+
pseudoState.pseudoClasses,
|
|
152
|
+
));
|
|
153
|
+
}
|
|
154
|
+
await takeAccessibleScreenshot(page, testInfo, screenshotOptions);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
operationFailed = true
|
|
157
|
+
operationError = error
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const cleanupErrors: unknown[] = []
|
|
161
|
+
for (const clearPseudoState of clearPseudoStates.reverse()) {
|
|
162
|
+
try {
|
|
163
|
+
await clearPseudoState();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
cleanupErrors.push(error)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (operationFailed) {
|
|
170
|
+
if (cleanupErrors.length > 0) {
|
|
171
|
+
throw new AggregateError([operationError, ...cleanupErrors], 'Visual diff capture and pseudo-state cleanup failed.')
|
|
172
|
+
}
|
|
173
|
+
throw operationError
|
|
174
|
+
}
|
|
175
|
+
if (cleanupErrors.length === 1) {
|
|
176
|
+
throw cleanupErrors[0]
|
|
177
|
+
}
|
|
178
|
+
if (cleanupErrors.length > 1) {
|
|
179
|
+
throw new AggregateError(cleanupErrors, 'Multiple pseudo-state cleanup operations failed.')
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Execute a set of visual diffs against groups of test cases.
|
|
186
|
+
*/
|
|
187
|
+
export class VisualDiffTestCases {
|
|
188
|
+
/**
|
|
189
|
+
* The configuration object containing all visual diff test cases.
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
private config: VisualDiffUrlConfig;
|
|
193
|
+
private preset?: VisualDiffPreset;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Construct a new set of VisualDiffTestCases
|
|
197
|
+
*
|
|
198
|
+
* @param config The config that has been imported via "import ..."
|
|
199
|
+
*/
|
|
200
|
+
constructor(config: VisualDiffUrlConfig, preset?: VisualDiffPreset) {
|
|
201
|
+
this.config = config;
|
|
202
|
+
this.preset = preset;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Describe, execute, and skip test cases
|
|
207
|
+
*
|
|
208
|
+
* @param overriddenTestFunction An optional custom test function. Note: when
|
|
209
|
+
* using a custom test function, automatic mask, interaction-state, and
|
|
210
|
+
* pseudo-state handling is bypassed. You must apply them yourself.
|
|
211
|
+
*/
|
|
212
|
+
public describe(overriddenTestFunction?: (testCase: VisualDiff, group: VisualDiffGroup) => Function | void) {
|
|
213
|
+
// Handle skipping of test cases, either based on a simple boolean or a callback.
|
|
214
|
+
function shouldSkip(testCase: BaseVisualDiff): boolean {
|
|
215
|
+
return testCase.skip !== undefined
|
|
216
|
+
&& (testCase.skip.callback === undefined || testCase.skip.callback(testCase))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function registerSkip(testCase: BaseVisualDiff): void {
|
|
220
|
+
const skip = testCase.skip!
|
|
221
|
+
test.skip(`${testCase.name}: ${skip.reason} <${skip.willBeFixedIn}>`, async () => {});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.config.groups.forEach((group: VisualDiffGroup) => {
|
|
225
|
+
// Allow skipping of entire groups of tests.
|
|
226
|
+
if (shouldSkip(group)) {
|
|
227
|
+
registerSkip(group);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Actually describe the group.
|
|
232
|
+
test.describe(group.name, () => {
|
|
233
|
+
group.testCases.forEach((testCase => {
|
|
234
|
+
// Allow skipping of individual test cases.
|
|
235
|
+
if (shouldSkip(testCase)) {
|
|
236
|
+
registerSkip(testCase);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Define a default function for test cases.
|
|
241
|
+
let testFunction: any;
|
|
242
|
+
if (typeof overriddenTestFunction != 'function') {
|
|
243
|
+
testFunction = defaultTestFunction(testCase, group, this.config, this.preset)
|
|
244
|
+
} else {
|
|
245
|
+
testFunction = overriddenTestFunction(testCase, group);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
test(`${testCase.name}: ${testCase.path}`, testFunction);
|
|
249
|
+
}));
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The top level configuration object.
|
|
259
|
+
*/
|
|
260
|
+
export type VisualDiffUrlConfig = {
|
|
261
|
+
// The name of the visual diff configuration, such as "Example Site Visual Diffs".
|
|
262
|
+
name: string,
|
|
263
|
+
// A further description of the configuration.
|
|
264
|
+
description?: string,
|
|
265
|
+
// An array of groups of visual diffs. Good groups include by content type, site
|
|
266
|
+
// section, or common feature.
|
|
267
|
+
groups: VisualDiffGroup[],
|
|
268
|
+
/** Prefix prepended before group and case paths. */
|
|
269
|
+
pathPrefix?: string,
|
|
270
|
+
/** Fallback representative URL for every case. */
|
|
271
|
+
representativeUrl?: string,
|
|
272
|
+
/** Fallback network/page mock for every case. */
|
|
273
|
+
mockClass?: MockableConstructor,
|
|
274
|
+
/**
|
|
275
|
+
* CSS selectors for elements to mask globally across all test cases.
|
|
276
|
+
* Useful for dynamic content like copyright years that change over time.
|
|
277
|
+
* These are merged with any group-level and test-case-level masks.
|
|
278
|
+
*/
|
|
279
|
+
mask?: string[],
|
|
280
|
+
/**
|
|
281
|
+
* The color of the overlay box for masked elements, in CSS color format.
|
|
282
|
+
* Can be overridden at the group or test-case level.
|
|
283
|
+
*/
|
|
284
|
+
maskColor?: string,
|
|
285
|
+
/**
|
|
286
|
+
* Accessibility baseline for managing known violations.
|
|
287
|
+
* When provided, violations matching the baseline are suppressed and
|
|
288
|
+
* toMatchSnapshot() is skipped in favour of baseline-driven assertions.
|
|
289
|
+
*/
|
|
290
|
+
a11yBaseline?: AccessibilityBaseline,
|
|
291
|
+
/**
|
|
292
|
+
* Cross-browser hover and focus states to apply for every test case. These
|
|
293
|
+
* are merged with group-level and test-case-level interaction states.
|
|
294
|
+
*/
|
|
295
|
+
interactionStates?: VisualDiffInteractionState[],
|
|
296
|
+
/**
|
|
297
|
+
* Chromium-only pseudo-states to force for every test case. These are merged
|
|
298
|
+
* with group-level and test-case-level pseudo-states.
|
|
299
|
+
*/
|
|
300
|
+
pseudoStates?: ForcedPseudoState[],
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A group of Visual Diff test cases.
|
|
305
|
+
*/
|
|
306
|
+
export type VisualDiffGroup = BaseVisualDiff & {
|
|
307
|
+
pathPrefix?: string,
|
|
308
|
+
// An array of test cases.
|
|
309
|
+
testCases: VisualDiff[],
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface MockableConstructor {
|
|
313
|
+
new (): Mockable;
|
|
314
|
+
}
|
|
315
|
+
export interface Mockable {
|
|
316
|
+
mock(page: Page): Promise<void>
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** A selector and real cross-browser interaction states to apply to it. */
|
|
320
|
+
export interface VisualDiffInteractionState {
|
|
321
|
+
selector: string
|
|
322
|
+
states: InteractionState[]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* An individual test case.
|
|
327
|
+
*/
|
|
328
|
+
export type VisualDiff = BaseVisualDiff & {
|
|
329
|
+
// A relative path for the test case.
|
|
330
|
+
path: string,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export type BaseVisualDiff = {
|
|
334
|
+
// The name of the test case, such as "Alert (White Background)".
|
|
335
|
+
name: string,
|
|
336
|
+
// An optional description of the test case.
|
|
337
|
+
description?: string,
|
|
338
|
+
// An optional representative URL for this test.
|
|
339
|
+
representativeUrl?: string,
|
|
340
|
+
// Allow skipping of this test.
|
|
341
|
+
skip?: SkipTest,
|
|
342
|
+
mockClass?: MockableConstructor,
|
|
343
|
+
/** Accessibility baseline overriding less-specific configuration. */
|
|
344
|
+
a11yBaseline?: AccessibilityBaseline,
|
|
345
|
+
/**
|
|
346
|
+
* CSS selectors for elements to mask when taking screenshots.
|
|
347
|
+
* These are merged with any config-level and (for test cases) group-level masks.
|
|
348
|
+
*/
|
|
349
|
+
mask?: string[],
|
|
350
|
+
/**
|
|
351
|
+
* The color of the overlay box for masked elements, in CSS color format.
|
|
352
|
+
* Overrides the mask color set at less-specific levels (config or group).
|
|
353
|
+
*/
|
|
354
|
+
maskColor?: string,
|
|
355
|
+
/**
|
|
356
|
+
* Cross-browser hover and focus states to apply while capturing the
|
|
357
|
+
* screenshot and running its accessibility scan.
|
|
358
|
+
*/
|
|
359
|
+
interactionStates?: VisualDiffInteractionState[],
|
|
360
|
+
/**
|
|
361
|
+
* Chromium-only pseudo-states to force while capturing the screenshot and
|
|
362
|
+
* running its accessibility scan.
|
|
363
|
+
*/
|
|
364
|
+
pseudoStates?: ForcedPseudoState[],
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* A declaration that a test should be skipped.
|
|
369
|
+
*
|
|
370
|
+
* Nothing prevents calling test.skip() in a custom test function, but this
|
|
371
|
+
* type ensures that every skip has both a reason and a link to a ticket.
|
|
372
|
+
*/
|
|
373
|
+
export type SkipTest = {
|
|
374
|
+
// The reason why this test should be skipped, such as "The News listing has undefined ordering".
|
|
375
|
+
reason: string,
|
|
376
|
+
// A link to the ticket or issue that will allow this test to be re-enabled.
|
|
377
|
+
willBeFixedIn: string,
|
|
378
|
+
// An optional callback to control if this test is skipped. For example, a skip
|
|
379
|
+
// callback could check the VisualDiff.path property.
|
|
380
|
+
callback?: (testCase: BaseVisualDiff) => boolean
|
|
381
|
+
}
|