@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
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# `@lullabot/playwright-testing`
|
|
2
|
+
|
|
3
|
+
Framework-neutral utilities for stable Playwright screenshots, accessibility
|
|
4
|
+
baselines, URL-driven visual comparisons, and GitHub reporting.
|
|
5
|
+
|
|
6
|
+
This package is developed in the
|
|
7
|
+
[`playwright-drupal` monorepo](https://github.com/Lullabot/playwright-drupal),
|
|
8
|
+
but it has no Drupal runtime assumptions. Drupal projects can instead install
|
|
9
|
+
[`@lullabot/playwright-drupal`](https://www.npmjs.com/package/@lullabot/playwright-drupal),
|
|
10
|
+
which applies Drupal-specific defaults and preserves the original API.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
npm install --save-dev @lullabot/playwright-testing @playwright/test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@axe-core/playwright` and `@playwright/test` are runtime dependencies of this
|
|
19
|
+
package. Your project should still declare its own compatible
|
|
20
|
+
`@playwright/test` dependency so the test runner and imported types use the same
|
|
21
|
+
version.
|
|
22
|
+
|
|
23
|
+
## Package entry points
|
|
24
|
+
|
|
25
|
+
- `@lullabot/playwright-testing` exports screenshot stabilization,
|
|
26
|
+
accessibility checks and baselines, visual-diff definitions, interaction and
|
|
27
|
+
pseudo-state helpers, and reusable mocks.
|
|
28
|
+
- `@lullabot/playwright-testing/github` exports the optional GitHub report,
|
|
29
|
+
attachment-upload, and path-remapping APIs.
|
|
30
|
+
- `playwright-testing-a11y-summary` and
|
|
31
|
+
`playwright-testing-failure-summary` expose the reporting commands.
|
|
32
|
+
|
|
33
|
+
The package does not replace Playwright's `test` fixture. Import `test`,
|
|
34
|
+
`expect`, and `testInfo` from `@playwright/test`, then pass the page and test
|
|
35
|
+
metadata to the helpers:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { expect, test } from '@playwright/test';
|
|
39
|
+
import { takeAccessibleScreenshot } from '@lullabot/playwright-testing';
|
|
40
|
+
|
|
41
|
+
test('home page', async ({ page }, testInfo) => {
|
|
42
|
+
await page.goto('/');
|
|
43
|
+
await takeAccessibleScreenshot(page, testInfo, { fullPage: true });
|
|
44
|
+
await expect(page).toHaveTitle(/Example/);
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Guides
|
|
49
|
+
|
|
50
|
+
- [Accessibility testing](https://github.com/Lullabot/playwright-drupal/blob/main/packages/playwright-testing/docs/accessibility.md)
|
|
51
|
+
- [Stable screenshots and visual comparisons](https://github.com/Lullabot/playwright-drupal/blob/main/packages/playwright-testing/docs/screenshots-and-visual-comparisons.md)
|
|
52
|
+
- [GitHub reporting](https://github.com/Lullabot/playwright-drupal/blob/main/packages/playwright-testing/docs/github-reporting.md)
|
|
53
|
+
|
|
54
|
+
## Drupal compatibility
|
|
55
|
+
|
|
56
|
+
Existing `@lullabot/playwright-drupal` imports remain supported. That package
|
|
57
|
+
depends on this one, re-exports the generic APIs, and wraps screenshot and
|
|
58
|
+
visual-diff calls with its Drupal preset. New framework-neutral code can import
|
|
59
|
+
this package directly; Drupal tests that depend on database isolation, Drush,
|
|
60
|
+
login helpers, or the `a11y` fixture should continue importing the Drupal
|
|
61
|
+
package.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { TestInfo } from '@playwright/test';
|
|
2
|
+
import type { AccessibilityBaselineEntry } from './accessibility-baseline.js';
|
|
3
|
+
export type ScanKind = 'wcag' | 'best-practice';
|
|
4
|
+
export interface OnDiskBaselineFile {
|
|
5
|
+
note: string;
|
|
6
|
+
violations: AccessibilityBaselineEntry[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Increment and return the 1-indexed invocation number for this scan kind
|
|
10
|
+
* within the current test. Every call to `checkAccessibility()` runs up to
|
|
11
|
+
* two scans (WCAG + best-practice); each scan gets its own counter so that
|
|
12
|
+
* a test calling `checkAccessibility()` twice produces distinct baseline
|
|
13
|
+
* file names (`…-1.a11y-baseline.json`, `…-2.a11y-baseline.json`) even when
|
|
14
|
+
* both scans share a single `checkAccessibility()` call.
|
|
15
|
+
*
|
|
16
|
+
* Counter state is keyed by the TestInfo object (held in a WeakMap so it
|
|
17
|
+
* is discarded with the test). Use `resetAccessibilityScanCounts()` to
|
|
18
|
+
* clear state explicitly — for example, between tests in a unit-test
|
|
19
|
+
* suite that reuses a mocked TestInfo.
|
|
20
|
+
*/
|
|
21
|
+
export declare function nextAccessibilityScanCount(testInfo: Pick<TestInfo, 'testId'> | object, scan: ScanKind): number;
|
|
22
|
+
export declare function resetAccessibilityScanCounts(testInfo: object): void;
|
|
23
|
+
/**
|
|
24
|
+
* Build the on-disk baseline file path for a single scan call.
|
|
25
|
+
*
|
|
26
|
+
* Playwright exposes `testInfo.snapshotPath(...name)` as the public way to
|
|
27
|
+
* resolve a file under the test's snapshots directory (honoring any
|
|
28
|
+
* user-configured `snapshotPathTemplate`). We pass a filename that mixes
|
|
29
|
+
* the slugified test title + call counter + a scan-specific suffix, so two
|
|
30
|
+
* different tests in the same spec file don't collide and each scan has
|
|
31
|
+
* its own file.
|
|
32
|
+
*
|
|
33
|
+
* There is no reusable Playwright API that bakes the test title into an
|
|
34
|
+
* explicit-name snapshot path — that logic is private to `toMatchSnapshot()`
|
|
35
|
+
* for its auto-counter naming — so we build the stem ourselves.
|
|
36
|
+
*/
|
|
37
|
+
export declare function baselineFilePath(testInfo: Pick<TestInfo, 'snapshotPath' | 'titlePath' | 'title'>, scan: ScanKind, callCount: number): string;
|
|
38
|
+
/**
|
|
39
|
+
* Return true if a Playwright snapshot file already exists on disk for
|
|
40
|
+
* this test — i.e. the test has been previously committed under snapshot
|
|
41
|
+
* mode. Used to preserve snapshot-mode behaviour for existing tests while
|
|
42
|
+
* defaulting new (snapshotless) tests into on-disk baseline mode.
|
|
43
|
+
*/
|
|
44
|
+
export declare function snapshotExists(testInfo: Pick<TestInfo, 'snapshotPath' | 'titlePath' | 'title'>): Promise<boolean>;
|
|
45
|
+
export declare function readBaselineFile(filePath: string): Promise<OnDiskBaselineFile | null>;
|
|
46
|
+
export declare function writeBaselineFile(filePath: string, data: OnDiskBaselineFile): Promise<void>;
|
|
47
|
+
export declare function buildSeed(violations: AccessibilityBaselineEntry[]): OnDiskBaselineFile;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.nextAccessibilityScanCount = nextAccessibilityScanCount;
|
|
7
|
+
exports.resetAccessibilityScanCounts = resetAccessibilityScanCounts;
|
|
8
|
+
exports.baselineFilePath = baselineFilePath;
|
|
9
|
+
exports.snapshotExists = snapshotExists;
|
|
10
|
+
exports.readBaselineFile = readBaselineFile;
|
|
11
|
+
exports.writeBaselineFile = writeBaselineFile;
|
|
12
|
+
exports.buildSeed = buildSeed;
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const scanInvocationCounters = new WeakMap();
|
|
16
|
+
/**
|
|
17
|
+
* Increment and return the 1-indexed invocation number for this scan kind
|
|
18
|
+
* within the current test. Every call to `checkAccessibility()` runs up to
|
|
19
|
+
* two scans (WCAG + best-practice); each scan gets its own counter so that
|
|
20
|
+
* a test calling `checkAccessibility()` twice produces distinct baseline
|
|
21
|
+
* file names (`…-1.a11y-baseline.json`, `…-2.a11y-baseline.json`) even when
|
|
22
|
+
* both scans share a single `checkAccessibility()` call.
|
|
23
|
+
*
|
|
24
|
+
* Counter state is keyed by the TestInfo object (held in a WeakMap so it
|
|
25
|
+
* is discarded with the test). Use `resetAccessibilityScanCounts()` to
|
|
26
|
+
* clear state explicitly — for example, between tests in a unit-test
|
|
27
|
+
* suite that reuses a mocked TestInfo.
|
|
28
|
+
*/
|
|
29
|
+
function nextAccessibilityScanCount(testInfo, scan) {
|
|
30
|
+
const key = testInfo;
|
|
31
|
+
let counts = scanInvocationCounters.get(key);
|
|
32
|
+
if (!counts) {
|
|
33
|
+
counts = new Map();
|
|
34
|
+
scanInvocationCounters.set(key, counts);
|
|
35
|
+
}
|
|
36
|
+
const n = (counts.get(scan) ?? 0) + 1;
|
|
37
|
+
counts.set(scan, n);
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
function resetAccessibilityScanCounts(testInfo) {
|
|
41
|
+
scanInvocationCounters.delete(testInfo);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Slugify a test's fully qualified title the same way we use it as the
|
|
45
|
+
* stem of both on-disk baseline filenames and (for existence checks) the
|
|
46
|
+
* prefix of Playwright's auto-generated snapshot filenames.
|
|
47
|
+
*
|
|
48
|
+
* Implemented as a single-pass character scan to avoid regex-based
|
|
49
|
+
* polynomial backtracking on library-supplied input (CodeQL
|
|
50
|
+
* js/polynomial-redos).
|
|
51
|
+
*/
|
|
52
|
+
function slugifyTitle(testInfo) {
|
|
53
|
+
const segments = testInfo.titlePath?.slice(1) ?? [];
|
|
54
|
+
const raw = segments.length > 0 ? segments.join(' ') : testInfo.title;
|
|
55
|
+
let out = '';
|
|
56
|
+
let lastWasHyphen = true; // suppresses leading hyphen
|
|
57
|
+
for (let i = 0; i < raw.length; i++) {
|
|
58
|
+
const code = raw.charCodeAt(i);
|
|
59
|
+
const isLowerAlpha = code >= 97 && code <= 122; // a-z
|
|
60
|
+
const isUpperAlpha = code >= 65 && code <= 90; // A-Z
|
|
61
|
+
const isDigit = code >= 48 && code <= 57; // 0-9
|
|
62
|
+
if (isLowerAlpha || isDigit) {
|
|
63
|
+
out += raw[i];
|
|
64
|
+
lastWasHyphen = false;
|
|
65
|
+
}
|
|
66
|
+
else if (isUpperAlpha) {
|
|
67
|
+
out += String.fromCharCode(code + 32);
|
|
68
|
+
lastWasHyphen = false;
|
|
69
|
+
}
|
|
70
|
+
else if (!lastWasHyphen) {
|
|
71
|
+
out += '-';
|
|
72
|
+
lastWasHyphen = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Trim trailing hyphen.
|
|
76
|
+
if (out.endsWith('-'))
|
|
77
|
+
out = out.slice(0, -1);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Reproduce the stem Playwright uses for auto-named snapshot files: the
|
|
82
|
+
* title path (minus the spec file) joined with spaces, with every run of
|
|
83
|
+
* control/punctuation characters collapsed to a single hyphen. Unlike
|
|
84
|
+
* `slugifyTitle()` this preserves case and does not trim hyphens, so it
|
|
85
|
+
* matches committed snapshot filenames exactly. Mirrors Playwright's
|
|
86
|
+
* `sanitizeForFilePath()`. Very long titles, which Playwright truncates
|
|
87
|
+
* and hashes, are not handled.
|
|
88
|
+
*/
|
|
89
|
+
function playwrightSnapshotStem(testInfo) {
|
|
90
|
+
const segments = testInfo.titlePath?.slice(1) ?? [];
|
|
91
|
+
const raw = segments.length > 0 ? segments.join(' ') : testInfo.title;
|
|
92
|
+
let out = '';
|
|
93
|
+
let lastWasReplaced = false;
|
|
94
|
+
for (let i = 0; i < raw.length; i++) {
|
|
95
|
+
const code = raw.charCodeAt(i);
|
|
96
|
+
const replaced = code <= 0x2c ||
|
|
97
|
+
(code >= 0x2e && code <= 0x2f) ||
|
|
98
|
+
(code >= 0x3a && code <= 0x40) ||
|
|
99
|
+
(code >= 0x5b && code <= 0x60) ||
|
|
100
|
+
(code >= 0x7b && code <= 0x7f);
|
|
101
|
+
if (replaced) {
|
|
102
|
+
if (!lastWasReplaced)
|
|
103
|
+
out += '-';
|
|
104
|
+
lastWasReplaced = true;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
out += raw[i];
|
|
108
|
+
lastWasReplaced = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build the on-disk baseline file path for a single scan call.
|
|
115
|
+
*
|
|
116
|
+
* Playwright exposes `testInfo.snapshotPath(...name)` as the public way to
|
|
117
|
+
* resolve a file under the test's snapshots directory (honoring any
|
|
118
|
+
* user-configured `snapshotPathTemplate`). We pass a filename that mixes
|
|
119
|
+
* the slugified test title + call counter + a scan-specific suffix, so two
|
|
120
|
+
* different tests in the same spec file don't collide and each scan has
|
|
121
|
+
* its own file.
|
|
122
|
+
*
|
|
123
|
+
* There is no reusable Playwright API that bakes the test title into an
|
|
124
|
+
* explicit-name snapshot path — that logic is private to `toMatchSnapshot()`
|
|
125
|
+
* for its auto-counter naming — so we build the stem ourselves.
|
|
126
|
+
*/
|
|
127
|
+
function baselineFilePath(testInfo, scan, callCount) {
|
|
128
|
+
const slug = slugifyTitle(testInfo);
|
|
129
|
+
const suffix = scan === 'wcag' ? 'a11y-baseline' : 'a11y-baseline-best-practice';
|
|
130
|
+
return testInfo.snapshotPath(`${slug}-${callCount}.${suffix}.json`);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Return true if a Playwright snapshot file already exists on disk for
|
|
134
|
+
* this test — i.e. the test has been previously committed under snapshot
|
|
135
|
+
* mode. Used to preserve snapshot-mode behaviour for existing tests while
|
|
136
|
+
* defaulting new (snapshotless) tests into on-disk baseline mode.
|
|
137
|
+
*/
|
|
138
|
+
async function snapshotExists(testInfo) {
|
|
139
|
+
const dir = path_1.default.dirname(testInfo.snapshotPath('a11y-baseline-probe'));
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
entries = await fs_1.promises.readdir(dir);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err?.code === 'ENOENT')
|
|
146
|
+
return false;
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
// Playwright names snapshots `<stem>-<counter>[-<project>-<platform>].txt`.
|
|
150
|
+
// Require the counter so a title that merely extends this one (e.g.
|
|
151
|
+
// "Video" vs "Video Promo") is not mistaken for this test's snapshot.
|
|
152
|
+
const prefix = `${playwrightSnapshotStem(testInfo)}-`;
|
|
153
|
+
return entries.some(name => {
|
|
154
|
+
if (!name.startsWith(prefix) || !name.endsWith('.txt'))
|
|
155
|
+
return false;
|
|
156
|
+
let i = prefix.length;
|
|
157
|
+
while (i < name.length && name.charCodeAt(i) >= 48 && name.charCodeAt(i) <= 57)
|
|
158
|
+
i++;
|
|
159
|
+
return i > prefix.length && (name[i] === '-' || name[i] === '.');
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function readBaselineFile(filePath) {
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = await fs_1.promises.readFile(filePath, 'utf8');
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (err?.code === 'ENOENT')
|
|
169
|
+
return null;
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(raw);
|
|
174
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.violations)) {
|
|
175
|
+
throw new Error('expected an object with a "violations" array');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
note: typeof parsed.note === 'string' ? parsed.note : '',
|
|
179
|
+
violations: parsed.violations,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
throw new Error(`Malformed baseline file at ${filePath}: ${err.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function writeBaselineFile(filePath, data) {
|
|
187
|
+
await fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
188
|
+
try {
|
|
189
|
+
await fs_1.promises.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', { flag: 'wx' });
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (err?.code === 'EEXIST')
|
|
193
|
+
return;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function buildSeed(violations) {
|
|
198
|
+
if (violations.length === 0) {
|
|
199
|
+
return { note: 'No accessibility violations found', violations: [] };
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
note: 'TODO: fill in reason and willBeFixedIn for each entry before committing.',
|
|
203
|
+
violations,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AccessibilityBaselineEntry {
|
|
2
|
+
/** axe rule ID (e.g. 'color-contrast') */
|
|
3
|
+
rule: string;
|
|
4
|
+
/** CSS selectors for the elements with this violation */
|
|
5
|
+
targets: string[];
|
|
6
|
+
/** Why this violation is accepted */
|
|
7
|
+
reason: string;
|
|
8
|
+
/** Link to tracking ticket */
|
|
9
|
+
willBeFixedIn: string;
|
|
10
|
+
}
|
|
11
|
+
export type AccessibilityBaseline = AccessibilityBaselineEntry[];
|
|
12
|
+
export declare function defineAccessibilityBaseline(entries: AccessibilityBaseline): AccessibilityBaseline;
|
|
13
|
+
/**
|
|
14
|
+
* Validate waiver metadata and reject ambiguous duplicate entries.
|
|
15
|
+
*
|
|
16
|
+
* Seed files deliberately contain TODO placeholders, so callers validate only
|
|
17
|
+
* committed/in-code baselines, not a seed during the local creation run.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateAccessibilityBaseline(entries: AccessibilityBaseline): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineAccessibilityBaseline = defineAccessibilityBaseline;
|
|
4
|
+
exports.validateAccessibilityBaseline = validateAccessibilityBaseline;
|
|
5
|
+
function defineAccessibilityBaseline(entries) {
|
|
6
|
+
validateAccessibilityBaseline(entries);
|
|
7
|
+
return entries;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Validate waiver metadata and reject ambiguous duplicate entries.
|
|
11
|
+
*
|
|
12
|
+
* Seed files deliberately contain TODO placeholders, so callers validate only
|
|
13
|
+
* committed/in-code baselines, not a seed during the local creation run.
|
|
14
|
+
*/
|
|
15
|
+
function validateAccessibilityBaseline(entries) {
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
entries.forEach((entry, index) => {
|
|
18
|
+
if (!entry.rule.trim()) {
|
|
19
|
+
throw new Error(`Accessibility baseline entry ${index + 1} requires a rule.`);
|
|
20
|
+
}
|
|
21
|
+
if (entry.targets.length === 0 || entry.targets.some(target => !target.trim())) {
|
|
22
|
+
throw new Error(`Accessibility baseline entry ${index + 1} requires at least one non-empty target.`);
|
|
23
|
+
}
|
|
24
|
+
if (!entry.reason.trim() || entry.reason.trim().toUpperCase() === 'TODO') {
|
|
25
|
+
throw new Error(`Accessibility baseline entry ${index + 1} requires a reason.`);
|
|
26
|
+
}
|
|
27
|
+
if (!entry.willBeFixedIn.trim() || entry.willBeFixedIn.trim().toUpperCase() === 'TODO') {
|
|
28
|
+
throw new Error(`Accessibility baseline entry ${index + 1} requires a willBeFixedIn tracking reference.`);
|
|
29
|
+
}
|
|
30
|
+
for (const target of new Set(entry.targets)) {
|
|
31
|
+
const key = JSON.stringify([entry.rule, target]);
|
|
32
|
+
if (seen.has(key)) {
|
|
33
|
+
throw new Error(`Accessibility baseline contains a duplicate waiver for ${entry.rule} on ${target}.`);
|
|
34
|
+
}
|
|
35
|
+
seen.add(key);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { type Locator, type Page, type TestInfo } from "@playwright/test";
|
|
2
|
+
import { type WaitForImagesOptions } from "./images.js";
|
|
3
|
+
import { type WaitForVideosOptions } from "./videos.js";
|
|
4
|
+
import { type ScreenshotInteractionState } from './interaction-states.js';
|
|
5
|
+
import { type AccessibilityBaseline } from './accessibility-baseline.js';
|
|
6
|
+
export type { InteractionState, ScreenshotInteractionState } from './interaction-states.js';
|
|
7
|
+
export interface ScreenshotStabilizationOptions {
|
|
8
|
+
images?: WaitForImagesOptions;
|
|
9
|
+
videos?: WaitForVideosOptions;
|
|
10
|
+
}
|
|
11
|
+
export interface ScreenshotOptions {
|
|
12
|
+
/**
|
|
13
|
+
* When set to `"disabled"`, stops CSS animations, CSS transitions and Web Animations. Animations get different
|
|
14
|
+
* treatment depending on their duration:
|
|
15
|
+
* - finite animations are fast-forwarded to completion, so they'll fire `transitionend` event.
|
|
16
|
+
* - infinite animations are canceled to initial state, and then played over after the screenshot.
|
|
17
|
+
*
|
|
18
|
+
* Defaults to `"disabled"` that disables animations.
|
|
19
|
+
*/
|
|
20
|
+
animations?: "disabled" | "allow";
|
|
21
|
+
/**
|
|
22
|
+
* When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, text caret behavior will not be
|
|
23
|
+
* changed. Defaults to `"hide"`.
|
|
24
|
+
*/
|
|
25
|
+
caret?: "hide" | "initial";
|
|
26
|
+
/**
|
|
27
|
+
* When `true` (the default), blurs the active element before capturing so a
|
|
28
|
+
* stray focus ring left over from earlier test interactions does not appear in
|
|
29
|
+
* only some runs (an invisible-to-a-human diff that still trips the pixel
|
|
30
|
+
* budget). Set to `false` when the screenshot intentionally captures a focused
|
|
31
|
+
* state.
|
|
32
|
+
*/
|
|
33
|
+
blur?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* When `true` (the default), moves the pointer onto a temporary transparent
|
|
36
|
+
* shield before capturing so stale pointer activity does not leave an
|
|
37
|
+
* unrelated element in its hover state. Set to `false` when the screenshot
|
|
38
|
+
* intentionally captures a hovered state.
|
|
39
|
+
*/
|
|
40
|
+
clearHover?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Real hover and focus states to apply after the page has settled. These are
|
|
43
|
+
* supported in Chromium, Firefox, and WebKit and remain active through both
|
|
44
|
+
* the screenshot and accessibility scan. At most one locator may receive
|
|
45
|
+
* each state, matching what a real pointer and keyboard can do.
|
|
46
|
+
*/
|
|
47
|
+
interactionStates?: ScreenshotInteractionState[];
|
|
48
|
+
/** Configuration for image and video settling. */
|
|
49
|
+
stabilization?: ScreenshotStabilizationOptions;
|
|
50
|
+
/**
|
|
51
|
+
* An object specifying the page area to capture, in CSS pixels.
|
|
52
|
+
*/
|
|
53
|
+
clip?: {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to
|
|
61
|
+
* `false`.
|
|
62
|
+
*/
|
|
63
|
+
fullPage?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Derive a page screenshot clip from this locator after readiness waits and
|
|
66
|
+
* scrolling. Round x, y, width and height to integer CSS pixels without
|
|
67
|
+
* modifying rendering. Mutually exclusive with clip and a locator target.
|
|
68
|
+
* Use fullPage for targets larger than the viewport.
|
|
69
|
+
*/
|
|
70
|
+
clipLocator?: Locator;
|
|
71
|
+
/**
|
|
72
|
+
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink
|
|
73
|
+
* box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box.
|
|
74
|
+
*/
|
|
75
|
+
mask?: Array<Locator>;
|
|
76
|
+
/**
|
|
77
|
+
* Specify the color of the overlay box for masked elements, in
|
|
78
|
+
* [CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`.
|
|
79
|
+
*/
|
|
80
|
+
maskColor?: string;
|
|
81
|
+
/**
|
|
82
|
+
* An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`. Default is
|
|
83
|
+
* configurable with `TestConfig.expect`. Unset by default.
|
|
84
|
+
*/
|
|
85
|
+
maxDiffPixelRatio?: number;
|
|
86
|
+
/**
|
|
87
|
+
* An acceptable amount of pixels that could be different. Default is configurable with `TestConfig.expect`. Unset by
|
|
88
|
+
* default.
|
|
89
|
+
*/
|
|
90
|
+
maxDiffPixels?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
|
|
93
|
+
* Defaults to `false`.
|
|
94
|
+
*/
|
|
95
|
+
omitBackground?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* When set to `"css"`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this
|
|
98
|
+
* will keep screenshots small. Using `"device"` option will produce a single pixel per each device pixel, so
|
|
99
|
+
* screenshots of high-dpi devices will be twice as large or even larger.
|
|
100
|
+
*
|
|
101
|
+
* Defaults to `"css"`.
|
|
102
|
+
*/
|
|
103
|
+
scale?: "css" | "device";
|
|
104
|
+
/**
|
|
105
|
+
* A stylesheet, or list of stylesheets, to apply while taking the screenshot.
|
|
106
|
+
* The styles pierce Shadow DOM and apply to inner frames.
|
|
107
|
+
*/
|
|
108
|
+
stylePath?: string | string[];
|
|
109
|
+
/**
|
|
110
|
+
* An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the
|
|
111
|
+
* same pixel in compared images, between zero (strict) and one (lax), default is configurable with
|
|
112
|
+
* `TestConfig.expect`. Defaults to `0.2`.
|
|
113
|
+
*/
|
|
114
|
+
threshold?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
|
117
|
+
*/
|
|
118
|
+
timeout?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Accessibility options passed through to checkAccessibility().
|
|
121
|
+
*/
|
|
122
|
+
accessibility?: AccessibilityOptions;
|
|
123
|
+
}
|
|
124
|
+
export interface AccessibilityOptions {
|
|
125
|
+
/** axe tags for WCAG scan. Default: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] */
|
|
126
|
+
wcagTags?: string[];
|
|
127
|
+
/** Additional CSS selectors to exclude from both scans. */
|
|
128
|
+
exclude?: string[];
|
|
129
|
+
/** CSS selectors to exclude only from the best-practice scan. */
|
|
130
|
+
bestPracticeExclude?: string[];
|
|
131
|
+
/** CSS selectors to exclude only from the WCAG scan. */
|
|
132
|
+
wcagExclude?: string[];
|
|
133
|
+
/**
|
|
134
|
+
* Best-practice scan mode.
|
|
135
|
+
* - 'soft': uses expect.soft() (default, current behaviour)
|
|
136
|
+
* - 'hard': uses expect() — test fails immediately on violations
|
|
137
|
+
* - 'off': skips best-practice scan entirely
|
|
138
|
+
*/
|
|
139
|
+
bestPracticeMode?: 'soft' | 'hard' | 'off';
|
|
140
|
+
/** Additional axe rules to enable/disable. */
|
|
141
|
+
rules?: Record<string, {
|
|
142
|
+
enabled: boolean;
|
|
143
|
+
}>;
|
|
144
|
+
/** Baseline of known violations. When provided, violations matching the baseline are suppressed and toMatchSnapshot() is skipped. */
|
|
145
|
+
baseline?: AccessibilityBaseline;
|
|
146
|
+
/**
|
|
147
|
+
* When true, captures a full-page screenshot with violating elements
|
|
148
|
+
* highlighted (red outline) and attaches it to the test report.
|
|
149
|
+
* Default: true.
|
|
150
|
+
*/
|
|
151
|
+
screenshotViolations?: boolean;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Run accessibility checks on the current page using axe-core.
|
|
155
|
+
*
|
|
156
|
+
* Runs a best-practice scan (unless bestPracticeMode is 'off') and a WCAG scan,
|
|
157
|
+
* attaching JSON results and asserting on violations via snapshots.
|
|
158
|
+
*
|
|
159
|
+
* @param page The Page fixture from the test.
|
|
160
|
+
* @param testInfo The testInfo object from the test.
|
|
161
|
+
* @param options Accessibility options to customise the scan.
|
|
162
|
+
*/
|
|
163
|
+
export declare function checkAccessibility(page: Page, testInfo: TestInfo, options?: AccessibilityOptions): Promise<void>;
|
|
164
|
+
/**
|
|
165
|
+
* Take a visual comparison, and also ensure there's no accessibility issues.
|
|
166
|
+
*
|
|
167
|
+
* @param page The Page fixture from the test.
|
|
168
|
+
* @param testInfo The testInfo object from the test.
|
|
169
|
+
* @param options Screenshot options from toHaveScreenshot().
|
|
170
|
+
* @param scrollLocator A locator to ensure is visible before taking the screenshot.
|
|
171
|
+
* @param locator A specific locator to take the screenshot of. aXe still checks the whole page.
|
|
172
|
+
*/
|
|
173
|
+
export declare function takeAccessibleScreenshot(page: Page, testInfo: TestInfo, options?: ScreenshotOptions, scrollLocator?: Locator, locator?: Locator | Page): Promise<void>;
|
|
174
|
+
/**
|
|
175
|
+
* Normalize a single CSS selector target for stable comparison.
|
|
176
|
+
*
|
|
177
|
+
* Replaces unique numeric HTML IDs and aria-labelledby suffixes with
|
|
178
|
+
* a stable placeholder so that snapshots and baseline matching are
|
|
179
|
+
* deterministic across runs.
|
|
180
|
+
*/
|
|
181
|
+
export declare function normalizeTarget(target: string | string[]): string | string[];
|