@brightspace-ui/testing 1.38.0 → 1.38.2
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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@brightspace-ui/testing",
|
|
3
3
|
"description": "Utilities for testing front-end components and applications",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.38.
|
|
5
|
+
"version": "1.38.2",
|
|
6
6
|
"repository": "https://github.com/BrightspaceUI/testing.git",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"lint": "npm run lint:eslint && npm run lint:style",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const testInfoMap = new Map();
|
|
2
|
+
|
|
3
|
+
export class TestInfoManager {
|
|
4
|
+
constructor(session, fullTitle) {
|
|
5
|
+
this.key = `${session.browser.name.toLowerCase()}|${session.testFile}|${fullTitle}`;
|
|
6
|
+
}
|
|
7
|
+
get() {
|
|
8
|
+
return testInfoMap.get(this.key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
set(testInfo) {
|
|
12
|
+
if (testInfoMap.has(this.key)) {
|
|
13
|
+
const info = this.get();
|
|
14
|
+
testInfo.slowDuration = info.slowDuration;
|
|
15
|
+
|
|
16
|
+
if (info.golden || testInfo.golden) {
|
|
17
|
+
testInfo.golden = { ...info.golden, ...testInfo.golden };
|
|
18
|
+
}
|
|
19
|
+
if (info.new || testInfo.new) {
|
|
20
|
+
testInfo.new = { ...info.new, ...testInfo.new };
|
|
21
|
+
}
|
|
22
|
+
testInfo.diff = testInfo.diff || info.diff;
|
|
23
|
+
}
|
|
24
|
+
testInfoMap.set(this.key, testInfo);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getTestInfo(session, fullTitle) {
|
|
29
|
+
return new TestInfoManager(session, fullTitle).get();
|
|
30
|
+
}
|
|
@@ -4,6 +4,7 @@ import { env } from 'node:process';
|
|
|
4
4
|
import { PATHS } from './paths.js';
|
|
5
5
|
import pixelmatch from 'pixelmatch';
|
|
6
6
|
import { PNG } from 'pngjs';
|
|
7
|
+
import { TestInfoManager } from './visual-diff-info.js';
|
|
7
8
|
|
|
8
9
|
const isCI = !!env['CI'];
|
|
9
10
|
const DEFAULT_TOLERANCE = 0; // TODO: Support tolerance override?
|
|
@@ -50,24 +51,31 @@ async function clearDiffPaths(dir) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function getResizedPng(xName, yName, original, newSize) {
|
|
55
|
+
const xOffsets = {
|
|
56
|
+
left: 0,
|
|
57
|
+
center: Math.floor((newSize.width - original.width) / 2),
|
|
58
|
+
right: newSize.width - original.width
|
|
59
|
+
};
|
|
60
|
+
const yOffsets = {
|
|
61
|
+
top: 0,
|
|
62
|
+
center: Math.floor((newSize.height - original.height) / 2),
|
|
63
|
+
bottom: newSize.height - original.height
|
|
64
|
+
};
|
|
65
|
+
const resized = new PNG(newSize);
|
|
66
|
+
PNG.bitblt(original, resized, 0, 0, original.width, original.height, xOffsets[xName], yOffsets[yName]);
|
|
67
|
+
return resized;
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
async function createComparisonPNGs(original, newSize) {
|
|
54
71
|
const resizedPNGs = [];
|
|
55
|
-
[
|
|
56
|
-
|
|
57
|
-
{ name: 'center', coord: Math.floor((newSize.height - original.height) / 2) },
|
|
58
|
-
{ name: 'bottom', coord: newSize.height - original.height }
|
|
59
|
-
].forEach(y => {
|
|
60
|
-
[
|
|
61
|
-
{ name: 'left', coord: 0 },
|
|
62
|
-
{ name: 'center', coord: Math.floor((newSize.width - original.width) / 2) },
|
|
63
|
-
{ name: 'right', coord: newSize.width - original.width }
|
|
64
|
-
].forEach(x => { // TODO: position added for reports, remove/adjust as needed
|
|
72
|
+
[ 'top', 'center', 'bottom' ].forEach(y => {
|
|
73
|
+
['left', 'center', 'right'].forEach(x => {
|
|
65
74
|
if (original.width === newSize.width && original.height === newSize.height) {
|
|
66
|
-
resizedPNGs.push({ png: original, position: `${y
|
|
75
|
+
resizedPNGs.push({ png: original, position: `${y}-${x}` });
|
|
67
76
|
} else {
|
|
68
|
-
const resized =
|
|
69
|
-
|
|
70
|
-
resizedPNGs.push({ png: resized, position: `${y.name}-${x.name}` });
|
|
77
|
+
const resized = getResizedPng(x, y, original, newSize);
|
|
78
|
+
resizedPNGs.push({ png: resized, position: `${y}-${x}` });
|
|
71
79
|
}
|
|
72
80
|
});
|
|
73
81
|
});
|
|
@@ -75,6 +83,14 @@ async function createComparisonPNGs(original, newSize) {
|
|
|
75
83
|
return resizedPNGs;
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
function getPixelsDiff(screenshotImage, goldenImage) {
|
|
87
|
+
const diff = new PNG({ width: screenshotImage.width, height: screenshotImage.height });
|
|
88
|
+
const pixelsDiff = pixelmatch(
|
|
89
|
+
screenshotImage.data, goldenImage.data, diff.data, screenshotImage.width, screenshotImage.height, { diffMask: true, threshold: DEFAULT_TOLERANCE }
|
|
90
|
+
);
|
|
91
|
+
return { diff, pixelsDiff };
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
async function tryMoveFile(srcFileName, destFileName) {
|
|
79
95
|
await mkdir(dirname(destFileName), { recursive: true });
|
|
80
96
|
try {
|
|
@@ -105,29 +121,6 @@ function extractTestPartsFromName(name) {
|
|
|
105
121
|
};
|
|
106
122
|
}
|
|
107
123
|
|
|
108
|
-
const testInfoMap = new Map();
|
|
109
|
-
export function getTestInfo(session, fullTitle) {
|
|
110
|
-
return testInfoMap.get(getTestInfoKey(session, fullTitle));
|
|
111
|
-
}
|
|
112
|
-
function getTestInfoKey(session, fullTitle) {
|
|
113
|
-
return `${session.browser.name.toLowerCase()}|${session.testFile}|${fullTitle}`;
|
|
114
|
-
}
|
|
115
|
-
function setTestInfo(session, fullTitle, testInfo) {
|
|
116
|
-
const key = getTestInfoKey(session, fullTitle);
|
|
117
|
-
if (testInfoMap.has(key)) {
|
|
118
|
-
const info = testInfoMap.get(key);
|
|
119
|
-
testInfo.slowDuration = info.slowDuration;
|
|
120
|
-
if (info.golden || testInfo.golden) {
|
|
121
|
-
testInfo.golden = { ...info.golden, ...testInfo.golden };
|
|
122
|
-
}
|
|
123
|
-
if (info.new || testInfo.new) {
|
|
124
|
-
testInfo.new = { ...info.new, ...testInfo.new };
|
|
125
|
-
}
|
|
126
|
-
testInfo.diff = testInfo.diff || info.diff;
|
|
127
|
-
}
|
|
128
|
-
testInfoMap.set(key, testInfo);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
124
|
export function visualDiff({ updateGoldens = false, runSubset = false } = {}) {
|
|
132
125
|
let currentRun = 0,
|
|
133
126
|
rootDir;
|
|
@@ -142,6 +135,7 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) {
|
|
|
142
135
|
await clearAllDirs(updateGoldens, join(rootDir, PATHS.VDIFF_ROOT));
|
|
143
136
|
},
|
|
144
137
|
async executeCommand({ command, payload, session }) {
|
|
138
|
+
const infoManager = new TestInfoManager(session, payload.name);
|
|
145
139
|
|
|
146
140
|
if (session.browser.type !== 'playwright') {
|
|
147
141
|
throw new Error('Visual-diff is only supported for browser type Playwright.');
|
|
@@ -172,15 +166,19 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) {
|
|
|
172
166
|
}
|
|
173
167
|
|
|
174
168
|
const screenshotOpts = {
|
|
175
|
-
animations: 'disabled'
|
|
176
|
-
path: updateGoldens ? goldenFileName : screenshotFileName
|
|
169
|
+
animations: 'disabled'
|
|
177
170
|
};
|
|
178
171
|
|
|
179
172
|
if (payload.fullPage) screenshotOpts.fullPage = true;
|
|
180
173
|
if (payload.rect) screenshotOpts.clip = payload.rect;
|
|
181
174
|
|
|
182
|
-
|
|
183
|
-
|
|
175
|
+
async function takeScreenshot() {
|
|
176
|
+
const path = updateGoldens ? goldenFileName : screenshotFileName;
|
|
177
|
+
const page = session.browser.getPage(session.id);
|
|
178
|
+
return await page.screenshot({ path, ...screenshotOpts });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const screenshotFileBuffer = await takeScreenshot();
|
|
184
182
|
|
|
185
183
|
if (updateGoldens) {
|
|
186
184
|
return { pass: true };
|
|
@@ -188,77 +186,72 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) {
|
|
|
188
186
|
|
|
189
187
|
const rootLength = join(rootDir, PATHS.VDIFF_ROOT).length + 1;
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
new: {
|
|
196
|
-
height: screenshotImage.height,
|
|
197
|
-
path: passFileName.substring(rootLength),
|
|
198
|
-
width: screenshotImage.width
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const goldenExists = await checkFileExists(goldenFileName);
|
|
203
|
-
if (!goldenExists) {
|
|
204
|
-
setTestInfo(session, payload.name, {
|
|
189
|
+
async function runCompareTest() {
|
|
190
|
+
const screenshotImage = PNG.sync.read(screenshotFileBuffer);
|
|
191
|
+
infoManager.set({
|
|
192
|
+
slowDuration: payload.slowDuration,
|
|
205
193
|
new: {
|
|
206
|
-
|
|
194
|
+
height: screenshotImage.height,
|
|
195
|
+
path: screenshotFileName.substring(rootLength),
|
|
196
|
+
width: screenshotImage.width
|
|
207
197
|
}
|
|
208
198
|
});
|
|
209
|
-
return { pass: false, message: 'No golden exists. Use the "--golden" CLI flag to re-run and re-generate goldens.' };
|
|
210
|
-
}
|
|
211
199
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
golden: {
|
|
216
|
-
height: goldenImage.height,
|
|
217
|
-
path: goldenFileName.substring(rootLength),
|
|
218
|
-
width: goldenImage.width
|
|
200
|
+
const goldenExists = await checkFileExists(goldenFileName);
|
|
201
|
+
if (!goldenExists) {
|
|
202
|
+
return { pass: false, message: 'No golden exists. Use the "--golden" CLI flag to re-run and re-generate goldens.' };
|
|
219
203
|
}
|
|
220
|
-
});
|
|
221
204
|
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
205
|
+
const goldenFileBuffer = await readFile(goldenFileName);
|
|
206
|
+
const goldenImage = PNG.sync.read(goldenFileBuffer);
|
|
207
|
+
infoManager.set({
|
|
208
|
+
golden: {
|
|
209
|
+
height: goldenImage.height,
|
|
210
|
+
path: goldenFileName.substring(rootLength),
|
|
211
|
+
width: goldenImage.width
|
|
212
|
+
}
|
|
213
|
+
});
|
|
230
214
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
215
|
+
if (screenshotImage.width === goldenImage.width && screenshotImage.height === goldenImage.height) {
|
|
216
|
+
const goldenSize = (await stat(goldenFileName)).size;
|
|
217
|
+
const screenshotSize = (await stat(screenshotFileName)).size;
|
|
218
|
+
if (goldenSize === screenshotSize && screenshotFileBuffer.equals(goldenFileBuffer)) {
|
|
219
|
+
const success = await tryMoveFile(screenshotFileName, passFileName);
|
|
220
|
+
if (!success) return { pass: false, message: 'Problem moving file to "pass" directory.' };
|
|
221
|
+
infoManager.set({
|
|
222
|
+
new: {
|
|
223
|
+
path: passFileName.substring(rootLength)
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return { pass: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { diff, pixelsDiff } = getPixelsDiff(screenshotImage, goldenImage);
|
|
230
|
+
|
|
231
|
+
if (pixelsDiff !== 0) {
|
|
232
|
+
infoManager.set({
|
|
233
|
+
diff: `${screenshotFile.substring(rootLength)}-diff.png`,
|
|
234
|
+
pixelsDiff
|
|
235
|
+
});
|
|
236
|
+
await writeFile(`${screenshotFile}-diff.png`, PNG.sync.write(diff));
|
|
237
|
+
return { pass: false, message: `Image does not match golden. ${pixelsDiff} pixels are different.` };
|
|
238
|
+
} else {
|
|
239
|
+
infoManager.set({
|
|
240
|
+
golden: {
|
|
241
|
+
byteSize: goldenSize
|
|
242
|
+
},
|
|
243
|
+
new: {
|
|
244
|
+
byteSize: screenshotSize
|
|
245
|
+
},
|
|
246
|
+
pixelsDiff
|
|
247
|
+
});
|
|
248
|
+
return { pass: false, message: 'Image diff is clean but the images do not have the same bytes.' };
|
|
249
|
+
}
|
|
246
250
|
} else {
|
|
247
|
-
|
|
248
|
-
golden: {
|
|
249
|
-
byteSize: goldenSize
|
|
250
|
-
},
|
|
251
|
-
new: {
|
|
252
|
-
path: `${screenshotFile.substring(rootLength)}.png`,
|
|
253
|
-
byteSize: screenshotSize
|
|
254
|
-
},
|
|
255
|
-
pixelsDiff
|
|
256
|
-
});
|
|
257
|
-
return { pass: false, message: 'Image diff is clean but the images do not have the same bytes.' };
|
|
251
|
+
return { resizeRequired: true };
|
|
258
252
|
}
|
|
259
|
-
} else {
|
|
260
|
-
return { resizeRequired: true };
|
|
261
253
|
}
|
|
254
|
+
return await runCompareTest();
|
|
262
255
|
} else if (command === 'brightspace-visual-diff-compare-resize') {
|
|
263
256
|
const screenshotImage = PNG.sync.read(await readFile(screenshotFileName));
|
|
264
257
|
const goldenImage = PNG.sync.read(await readFile(goldenFileName));
|
|
@@ -272,17 +265,14 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) {
|
|
|
272
265
|
|
|
273
266
|
let bestIndex = -1;
|
|
274
267
|
let bestDiffImage = null;
|
|
275
|
-
let
|
|
268
|
+
let bestPixelsDiff = Number.MAX_SAFE_INTEGER;
|
|
276
269
|
for (let i = 0; i < newScreenshots.length; i++) {
|
|
277
|
-
const
|
|
278
|
-
const currentPixelsDiff = pixelmatch(
|
|
279
|
-
newScreenshots[i].png.data, newGoldens[i].png.data, currentDiff.data, currentDiff.width, currentDiff.height, { diffMask: true, threshold: DEFAULT_TOLERANCE }
|
|
280
|
-
);
|
|
270
|
+
const { diff, pixelsDiff } = getPixelsDiff(newScreenshots[i].png, newGoldens[i].png);
|
|
281
271
|
|
|
282
|
-
if (
|
|
272
|
+
if (pixelsDiff < bestPixelsDiff) {
|
|
283
273
|
bestIndex = i;
|
|
284
|
-
bestDiffImage =
|
|
285
|
-
|
|
274
|
+
bestDiffImage = diff;
|
|
275
|
+
bestPixelsDiff = pixelsDiff;
|
|
286
276
|
}
|
|
287
277
|
}
|
|
288
278
|
|
|
@@ -291,7 +281,7 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) {
|
|
|
291
281
|
await writeFile(`${screenshotFile}-diff.png`, PNG.sync.write(bestDiffImage));
|
|
292
282
|
|
|
293
283
|
const rootLength = join(rootDir, PATHS.VDIFF_ROOT).length + 1;
|
|
294
|
-
|
|
284
|
+
infoManager.set({
|
|
295
285
|
diff: `${screenshotFile.substring(rootLength)}-diff.png`,
|
|
296
286
|
golden: {
|
|
297
287
|
path: `${screenshotFile.substring(rootLength)}-resized-golden.png`
|
|
@@ -299,10 +289,10 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) {
|
|
|
299
289
|
new: {
|
|
300
290
|
path: `${screenshotFile.substring(rootLength)}-resized-screenshot.png`
|
|
301
291
|
},
|
|
302
|
-
pixelsDiff
|
|
292
|
+
pixelsDiff: bestPixelsDiff
|
|
303
293
|
});
|
|
304
294
|
|
|
305
|
-
return { pass: false, message: `Images are not the same size. When resized, ${
|
|
295
|
+
return { pass: false, message: `Images are not the same size. When resized, ${bestPixelsDiff} pixels are different.` };
|
|
306
296
|
}
|
|
307
297
|
|
|
308
298
|
}
|
|
@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { env } from 'node:process';
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { getTestInfo } from './visual-diff-
|
|
7
|
+
import { getTestInfo } from './visual-diff-info.js';
|
|
8
8
|
import { PATHS } from './paths.js';
|
|
9
9
|
|
|
10
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|