@lullabot/playwright-testing 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +88 -76
- 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 +139 -107
- 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 +633 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +334 -245
- 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 +659 -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,16 +1,16 @@
|
|
|
1
|
-
import { promises as fs } from
|
|
2
|
-
import path from
|
|
3
|
-
import type { TestInfo } from
|
|
4
|
-
import type { AccessibilityBaselineEntry } from
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { TestInfo } from "@playwright/test";
|
|
4
|
+
import type { AccessibilityBaselineEntry } from "./accessibility-baseline.js";
|
|
5
5
|
|
|
6
|
-
export type ScanKind =
|
|
6
|
+
export type ScanKind = "wcag" | "best-practice";
|
|
7
7
|
|
|
8
8
|
export interface OnDiskBaselineFile {
|
|
9
|
-
note: string
|
|
10
|
-
violations: AccessibilityBaselineEntry[]
|
|
9
|
+
note: string;
|
|
10
|
+
violations: AccessibilityBaselineEntry[];
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const scanInvocationCounters = new WeakMap<object, Map<ScanKind, number>>()
|
|
13
|
+
const scanInvocationCounters = new WeakMap<object, Map<ScanKind, number>>();
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Increment and return the 1-indexed invocation number for this scan kind
|
|
@@ -26,22 +26,22 @@ const scanInvocationCounters = new WeakMap<object, Map<ScanKind, number>>()
|
|
|
26
26
|
* suite that reuses a mocked TestInfo.
|
|
27
27
|
*/
|
|
28
28
|
export function nextAccessibilityScanCount(
|
|
29
|
-
testInfo: Pick<TestInfo,
|
|
29
|
+
testInfo: Pick<TestInfo, "testId"> | object,
|
|
30
30
|
scan: ScanKind,
|
|
31
31
|
): number {
|
|
32
|
-
const key = testInfo as object
|
|
33
|
-
let counts = scanInvocationCounters.get(key)
|
|
32
|
+
const key = testInfo as object;
|
|
33
|
+
let counts = scanInvocationCounters.get(key);
|
|
34
34
|
if (!counts) {
|
|
35
|
-
counts = new Map()
|
|
36
|
-
scanInvocationCounters.set(key, counts)
|
|
35
|
+
counts = new Map();
|
|
36
|
+
scanInvocationCounters.set(key, counts);
|
|
37
37
|
}
|
|
38
|
-
const n = (counts.get(scan) ?? 0) + 1
|
|
39
|
-
counts.set(scan, n)
|
|
40
|
-
return n
|
|
38
|
+
const n = (counts.get(scan) ?? 0) + 1;
|
|
39
|
+
counts.set(scan, n);
|
|
40
|
+
return n;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function resetAccessibilityScanCounts(testInfo: object): void {
|
|
44
|
-
scanInvocationCounters.delete(testInfo)
|
|
44
|
+
scanInvocationCounters.delete(testInfo);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -53,30 +53,30 @@ export function resetAccessibilityScanCounts(testInfo: object): void {
|
|
|
53
53
|
* polynomial backtracking on library-supplied input (CodeQL
|
|
54
54
|
* js/polynomial-redos).
|
|
55
55
|
*/
|
|
56
|
-
function slugifyTitle(testInfo: Pick<TestInfo,
|
|
57
|
-
const segments = testInfo.titlePath?.slice(1) ?? []
|
|
58
|
-
const raw = segments.length > 0 ? segments.join(
|
|
59
|
-
let out =
|
|
60
|
-
let lastWasHyphen = true // suppresses leading hyphen
|
|
56
|
+
function slugifyTitle(testInfo: Pick<TestInfo, "titlePath" | "title">): string {
|
|
57
|
+
const segments = testInfo.titlePath?.slice(1) ?? [];
|
|
58
|
+
const raw = segments.length > 0 ? segments.join(" ") : testInfo.title;
|
|
59
|
+
let out = "";
|
|
60
|
+
let lastWasHyphen = true; // suppresses leading hyphen
|
|
61
61
|
for (let i = 0; i < raw.length; i++) {
|
|
62
|
-
const code = raw.charCodeAt(i)
|
|
63
|
-
const isLowerAlpha = code >= 97 && code <= 122 // a-z
|
|
64
|
-
const isUpperAlpha = code >= 65 && code <= 90 // A-Z
|
|
65
|
-
const isDigit = code >= 48 && code <= 57 // 0-9
|
|
62
|
+
const code = raw.charCodeAt(i);
|
|
63
|
+
const isLowerAlpha = code >= 97 && code <= 122; // a-z
|
|
64
|
+
const isUpperAlpha = code >= 65 && code <= 90; // A-Z
|
|
65
|
+
const isDigit = code >= 48 && code <= 57; // 0-9
|
|
66
66
|
if (isLowerAlpha || isDigit) {
|
|
67
|
-
out += raw[i]
|
|
68
|
-
lastWasHyphen = false
|
|
67
|
+
out += raw[i];
|
|
68
|
+
lastWasHyphen = false;
|
|
69
69
|
} else if (isUpperAlpha) {
|
|
70
|
-
out += String.fromCharCode(code + 32)
|
|
71
|
-
lastWasHyphen = false
|
|
70
|
+
out += String.fromCharCode(code + 32);
|
|
71
|
+
lastWasHyphen = false;
|
|
72
72
|
} else if (!lastWasHyphen) {
|
|
73
|
-
out +=
|
|
74
|
-
lastWasHyphen = true
|
|
73
|
+
out += "-";
|
|
74
|
+
lastWasHyphen = true;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
// Trim trailing hyphen.
|
|
78
|
-
if (out.endsWith(
|
|
79
|
-
return out
|
|
78
|
+
if (out.endsWith("-")) out = out.slice(0, -1);
|
|
79
|
+
return out;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
@@ -88,28 +88,30 @@ function slugifyTitle(testInfo: Pick<TestInfo, 'titlePath' | 'title'>): string {
|
|
|
88
88
|
* `sanitizeForFilePath()`. Very long titles, which Playwright truncates
|
|
89
89
|
* and hashes, are not handled.
|
|
90
90
|
*/
|
|
91
|
-
function playwrightSnapshotStem(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
function playwrightSnapshotStem(
|
|
92
|
+
testInfo: Pick<TestInfo, "titlePath" | "title">,
|
|
93
|
+
): string {
|
|
94
|
+
const segments = testInfo.titlePath?.slice(1) ?? [];
|
|
95
|
+
const raw = segments.length > 0 ? segments.join(" ") : testInfo.title;
|
|
96
|
+
let out = "";
|
|
97
|
+
let lastWasReplaced = false;
|
|
96
98
|
for (let i = 0; i < raw.length; i++) {
|
|
97
|
-
const code = raw.charCodeAt(i)
|
|
99
|
+
const code = raw.charCodeAt(i);
|
|
98
100
|
const replaced =
|
|
99
101
|
code <= 0x2c ||
|
|
100
102
|
(code >= 0x2e && code <= 0x2f) ||
|
|
101
103
|
(code >= 0x3a && code <= 0x40) ||
|
|
102
104
|
(code >= 0x5b && code <= 0x60) ||
|
|
103
|
-
(code >= 0x7b && code <= 0x7f)
|
|
105
|
+
(code >= 0x7b && code <= 0x7f);
|
|
104
106
|
if (replaced) {
|
|
105
|
-
if (!lastWasReplaced) out +=
|
|
106
|
-
lastWasReplaced = true
|
|
107
|
+
if (!lastWasReplaced) out += "-";
|
|
108
|
+
lastWasReplaced = true;
|
|
107
109
|
} else {
|
|
108
|
-
out += raw[i]
|
|
109
|
-
lastWasReplaced = false
|
|
110
|
+
out += raw[i];
|
|
111
|
+
lastWasReplaced = false;
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
|
-
return out
|
|
114
|
+
return out;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/**
|
|
@@ -127,13 +129,14 @@ function playwrightSnapshotStem(testInfo: Pick<TestInfo, 'titlePath' | 'title'>)
|
|
|
127
129
|
* for its auto-counter naming — so we build the stem ourselves.
|
|
128
130
|
*/
|
|
129
131
|
export function baselineFilePath(
|
|
130
|
-
testInfo: Pick<TestInfo,
|
|
132
|
+
testInfo: Pick<TestInfo, "snapshotPath" | "titlePath" | "title">,
|
|
131
133
|
scan: ScanKind,
|
|
132
134
|
callCount: number,
|
|
133
135
|
): string {
|
|
134
|
-
const slug = slugifyTitle(testInfo)
|
|
135
|
-
const suffix =
|
|
136
|
-
|
|
136
|
+
const slug = slugifyTitle(testInfo);
|
|
137
|
+
const suffix =
|
|
138
|
+
scan === "wcag" ? "a11y-baseline" : "a11y-baseline-best-practice";
|
|
139
|
+
return testInfo.snapshotPath(`${slug}-${callCount}.${suffix}.json`);
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
/**
|
|
@@ -143,66 +146,87 @@ export function baselineFilePath(
|
|
|
143
146
|
* defaulting new (snapshotless) tests into on-disk baseline mode.
|
|
144
147
|
*/
|
|
145
148
|
export async function snapshotExists(
|
|
146
|
-
testInfo: Pick<TestInfo,
|
|
149
|
+
testInfo: Pick<TestInfo, "snapshotPath" | "titlePath" | "title">,
|
|
147
150
|
): Promise<boolean> {
|
|
148
|
-
const dir = path.dirname(testInfo.snapshotPath(
|
|
149
|
-
let entries: string[]
|
|
151
|
+
const dir = path.dirname(testInfo.snapshotPath("a11y-baseline-probe"));
|
|
152
|
+
let entries: string[];
|
|
150
153
|
try {
|
|
151
|
-
entries = await fs.readdir(dir)
|
|
154
|
+
entries = await fs.readdir(dir);
|
|
152
155
|
} catch (err: any) {
|
|
153
|
-
if (err?.code ===
|
|
154
|
-
throw err
|
|
156
|
+
if (err?.code === "ENOENT") return false;
|
|
157
|
+
throw err;
|
|
155
158
|
}
|
|
156
159
|
// Playwright names snapshots `<stem>-<counter>[-<project>-<platform>].txt`.
|
|
157
160
|
// Require the counter so a title that merely extends this one (e.g.
|
|
158
161
|
// "Video" vs "Video Promo") is not mistaken for this test's snapshot.
|
|
159
|
-
const prefix = `${playwrightSnapshotStem(testInfo)}
|
|
160
|
-
return entries.some(name => {
|
|
161
|
-
if (!name.startsWith(prefix) || !name.endsWith(
|
|
162
|
-
let i = prefix.length
|
|
163
|
-
while (
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
const prefix = `${playwrightSnapshotStem(testInfo)}-`;
|
|
163
|
+
return entries.some((name) => {
|
|
164
|
+
if (!name.startsWith(prefix) || !name.endsWith(".txt")) return false;
|
|
165
|
+
let i = prefix.length;
|
|
166
|
+
while (
|
|
167
|
+
i < name.length &&
|
|
168
|
+
name.charCodeAt(i) >= 48 &&
|
|
169
|
+
name.charCodeAt(i) <= 57
|
|
170
|
+
)
|
|
171
|
+
i++;
|
|
172
|
+
return i > prefix.length && (name[i] === "-" || name[i] === ".");
|
|
173
|
+
});
|
|
166
174
|
}
|
|
167
175
|
|
|
168
|
-
export async function readBaselineFile(
|
|
169
|
-
|
|
176
|
+
export async function readBaselineFile(
|
|
177
|
+
filePath: string,
|
|
178
|
+
): Promise<OnDiskBaselineFile | null> {
|
|
179
|
+
let raw: string;
|
|
170
180
|
try {
|
|
171
|
-
raw = await fs.readFile(filePath,
|
|
181
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
172
182
|
} catch (err: any) {
|
|
173
|
-
if (err?.code ===
|
|
174
|
-
throw err
|
|
183
|
+
if (err?.code === "ENOENT") return null;
|
|
184
|
+
throw err;
|
|
175
185
|
}
|
|
176
186
|
try {
|
|
177
|
-
const parsed = JSON.parse(raw)
|
|
178
|
-
if (
|
|
179
|
-
|
|
187
|
+
const parsed = JSON.parse(raw);
|
|
188
|
+
if (
|
|
189
|
+
!parsed ||
|
|
190
|
+
typeof parsed !== "object" ||
|
|
191
|
+
!Array.isArray(parsed.violations)
|
|
192
|
+
) {
|
|
193
|
+
throw new Error('expected an object with a "violations" array');
|
|
180
194
|
}
|
|
181
195
|
return {
|
|
182
|
-
note: typeof parsed.note ===
|
|
196
|
+
note: typeof parsed.note === "string" ? parsed.note : "",
|
|
183
197
|
violations: parsed.violations as AccessibilityBaselineEntry[],
|
|
184
|
-
}
|
|
198
|
+
};
|
|
185
199
|
} catch (err) {
|
|
186
|
-
throw new Error(
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Malformed baseline file at ${filePath}: ${(err as Error).message}`,
|
|
202
|
+
{ cause: err },
|
|
203
|
+
);
|
|
187
204
|
}
|
|
188
205
|
}
|
|
189
206
|
|
|
190
|
-
export async function writeBaselineFile(
|
|
191
|
-
|
|
207
|
+
export async function writeBaselineFile(
|
|
208
|
+
filePath: string,
|
|
209
|
+
data: OnDiskBaselineFile,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
192
212
|
try {
|
|
193
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2) +
|
|
213
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", {
|
|
214
|
+
flag: "wx",
|
|
215
|
+
});
|
|
194
216
|
} catch (err: any) {
|
|
195
|
-
if (err?.code ===
|
|
196
|
-
throw err
|
|
217
|
+
if (err?.code === "EEXIST") return;
|
|
218
|
+
throw err;
|
|
197
219
|
}
|
|
198
220
|
}
|
|
199
221
|
|
|
200
|
-
export function buildSeed(
|
|
222
|
+
export function buildSeed(
|
|
223
|
+
violations: AccessibilityBaselineEntry[],
|
|
224
|
+
): OnDiskBaselineFile {
|
|
201
225
|
if (violations.length === 0) {
|
|
202
|
-
return { note:
|
|
226
|
+
return { note: "No accessibility violations found", violations: [] };
|
|
203
227
|
}
|
|
204
228
|
return {
|
|
205
|
-
note:
|
|
229
|
+
note: "TODO: fill in reason and willBeFixedIn for each entry before committing.",
|
|
206
230
|
violations,
|
|
207
|
-
}
|
|
231
|
+
};
|
|
208
232
|
}
|