@brightspace-ui/testing 1.38.2 → 1.39.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/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.2",
5
+ "version": "1.39.0",
6
6
  "repository": "https://github.com/BrightspaceUI/testing.git",
7
7
  "scripts": {
8
8
  "lint": "npm run lint:eslint && npm run lint:style",
@@ -89,12 +89,11 @@ async function ScreenshotAndCompare(opts = {}) {
89
89
  ({ rect, fullPage } = findLargestRect(elemsToInclude, margin));
90
90
  }
91
91
  const slowDuration = this.test.slow();
92
+ const altTests = [];
93
+ if (opts.allColorModes) altTests.push('dark');
92
94
 
93
- let result = await executeServerCommand('brightspace-visual-diff-compare', { name, fullPage, rect, slowDuration });
94
- if (result.resizeRequired) {
95
- this.test.timeout(0);
96
- result = await executeServerCommand('brightspace-visual-diff-compare-resize', { name });
97
- }
95
+ this.test.timeout(0);
96
+ const result = await executeServerCommand('brightspace-visual-diff-compare', { altTests, name, fullPage, rect, slowDuration });
98
97
 
99
98
  if (window.d2lTest) document.documentElement.classList.remove('screenshot');
100
99
 
@@ -8,9 +8,9 @@ export class TestInfoManager {
8
8
  return testInfoMap.get(this.key);
9
9
  }
10
10
 
11
- set(testInfo) {
11
+ set(testInfo, alt = 'default') {
12
12
  if (testInfoMap.has(this.key)) {
13
- const info = this.get();
13
+ const info = this.get()?.[alt] || {};
14
14
  testInfo.slowDuration = info.slowDuration;
15
15
 
16
16
  if (info.golden || testInfo.golden) {
@@ -21,7 +21,8 @@ export class TestInfoManager {
21
21
  }
22
22
  testInfo.diff = testInfo.diff || info.diff;
23
23
  }
24
- testInfoMap.set(this.key, testInfo);
24
+ const tests = testInfoMap.get(this.key) || {};
25
+ testInfoMap.set(this.key, { ...tests, [alt]: testInfo });
25
26
  }
26
27
  }
27
28
 
@@ -9,6 +9,17 @@ import { TestInfoManager } from './visual-diff-info.js';
9
9
  const isCI = !!env['CI'];
10
10
  const DEFAULT_TOLERANCE = 0; // TODO: Support tolerance override?
11
11
 
12
+ const ALT_TESTS = {
13
+ dark: {
14
+ set() {
15
+ document.documentElement.setAttribute('data-color-mode', 'dark');
16
+ },
17
+ reset() {
18
+ document.documentElement.removeAttribute('data-color-mode');
19
+ }
20
+ }
21
+ };
22
+
12
23
  async function checkFileExists(fileName) {
13
24
  try {
14
25
  await access(fileName, constants.F_OK);
@@ -67,6 +78,45 @@ function getResizedPng(xName, yName, original, newSize) {
67
78
  return resized;
68
79
  }
69
80
 
81
+ let bestPosition = null;
82
+ async function getBestResizePng(screenshotImage, goldenImage) {
83
+ const newWidth = Math.max(screenshotImage.width, goldenImage.width);
84
+ const newHeight = Math.max(screenshotImage.height, goldenImage.height);
85
+ const newSize = { width: newWidth, height: newHeight };
86
+
87
+ if (bestPosition !== null) {
88
+ const [y, x] = bestPosition.split('-');
89
+ const screenshot = getResizedPng(x, y, screenshotImage, newSize);
90
+ const golden = getResizedPng(x, y, goldenImage, newSize);
91
+ const { diff, pixelsDiff } = getPixelsDiff(screenshot, golden);
92
+ return { screenshot, golden, diff, pixelsDiff };
93
+ }
94
+
95
+ const newScreenshots = await createComparisonPNGs(screenshotImage, newSize);
96
+ const newGoldens = await createComparisonPNGs(goldenImage, newSize);
97
+
98
+ let bestIndex = -1;
99
+ let bestDiffImage = null;
100
+ let bestPixelsDiff = Number.MAX_SAFE_INTEGER;
101
+ for (let i = 0; i < newScreenshots.length; i++) {
102
+ const { diff, pixelsDiff } = getPixelsDiff(newScreenshots[i].png, newGoldens[i].png);
103
+
104
+ if (pixelsDiff < bestPixelsDiff) {
105
+ bestIndex = i;
106
+ bestDiffImage = diff;
107
+ bestPixelsDiff = pixelsDiff;
108
+ }
109
+ }
110
+
111
+ bestPosition = newScreenshots[bestIndex].position;
112
+ return {
113
+ screenshot: newScreenshots[bestIndex].png,
114
+ golden: newGoldens[bestIndex].png,
115
+ diff: bestDiffImage,
116
+ pixelsDiff: bestPixelsDiff
117
+ };
118
+ }
119
+
70
120
  async function createComparisonPNGs(original, newSize) {
71
121
  const resizedPNGs = [];
72
122
  [ 'top', 'center', 'bottom' ].forEach(y => {
@@ -145,156 +195,156 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) {
145
195
  const { dir, newName } = extractTestPartsFromName(payload.name);
146
196
  const testPath = dirname(session.testFile).replace(rootDir, '');
147
197
  const newPath = join(rootDir, PATHS.VDIFF_ROOT, testPath, dir);
148
- const goldenFileName = `${join(newPath, PATHS.GOLDEN, browser, newName)}.png`;
149
- const passFileName = `${join(newPath, PATHS.PASS, browser, newName)}.png`;
198
+ const goldenFile = join(newPath, PATHS.GOLDEN, browser, newName);
199
+ const passFile = join(newPath, PATHS.PASS, browser, newName);
150
200
  const screenshotFile = join(newPath, PATHS.FAIL, browser, newName);
151
- const screenshotFileName = `${screenshotFile}.png`;
152
-
153
- if (command === 'brightspace-visual-diff-compare') {
154
- if (!isCI) { // CI will be a fresh .vdiff folder each time and only one run
155
- if (session.testRun !== currentRun) {
156
- currentRun = session.testRun;
157
- clearedDirs.clear();
158
- }
201
+ function getFileName(base, alt = 'default') {
202
+ return `${base}${alt === 'default' ? '' : `.${alt}`}.png`;
203
+ }
204
+ const tests = ['default', ...(payload.altTests || [])];
205
+ const rootLength = join(rootDir, PATHS.VDIFF_ROOT).length + 1;
206
+
207
+ if (command !== 'brightspace-visual-diff-compare') return;
208
+ if (!isCI) { // CI will be a fresh .vdiff folder each time and only one run
209
+ if (session.testRun !== currentRun) {
210
+ currentRun = session.testRun;
211
+ clearedDirs.clear();
212
+ }
159
213
 
160
- if (runSubset || currentRun > 0) {
161
- if (!clearedDirs.has(newPath)) {
162
- clearedDirs.set(newPath, clearDir(updateGoldens, newPath));
163
- }
164
- await clearedDirs.get(newPath);
214
+ if (runSubset || currentRun > 0) {
215
+ if (!clearedDirs.has(newPath)) {
216
+ clearedDirs.set(newPath, clearDir(updateGoldens, newPath));
165
217
  }
218
+ await clearedDirs.get(newPath);
166
219
  }
220
+ }
167
221
 
168
- const screenshotOpts = {
169
- animations: 'disabled'
170
- };
222
+ const screenshotOpts = {
223
+ animations: 'disabled'
224
+ };
171
225
 
172
- if (payload.fullPage) screenshotOpts.fullPage = true;
173
- if (payload.rect) screenshotOpts.clip = payload.rect;
226
+ if (payload.fullPage) screenshotOpts.fullPage = true;
227
+ if (payload.rect) screenshotOpts.clip = payload.rect;
174
228
 
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
- }
229
+ const page = session.browser.getPage(session.id);
230
+ async function takeScreenshot(alt = 'default') {
231
+ const path = getFileName(updateGoldens ? goldenFile : screenshotFile, alt);
232
+ if (ALT_TESTS[alt]?.set) page.evaluate(ALT_TESTS[alt].set);
233
+ const screenshot = await page.screenshot({ path, ...screenshotOpts });
234
+ if (ALT_TESTS[alt]?.reset) page.evaluate(ALT_TESTS[alt].reset);
235
+ return screenshot;
236
+ }
180
237
 
181
- const screenshotFileBuffer = await takeScreenshot();
238
+ const screenshotFileBuffers = {};
239
+ for (const test of tests) screenshotFileBuffers[test] = await takeScreenshot(test);
182
240
 
183
- if (updateGoldens) {
184
- return { pass: true };
185
- }
241
+ if (updateGoldens) {
242
+ return { pass: true };
243
+ }
186
244
 
187
- const rootLength = join(rootDir, PATHS.VDIFF_ROOT).length + 1;
245
+ async function runCompareTest(alt = 'default') {
246
+ const screenshotFileName = getFileName(screenshotFile, alt);
247
+ const goldenFileName = getFileName(goldenFile, alt);
248
+ const passFileName = getFileName(passFile, alt);
188
249
 
189
- async function runCompareTest() {
190
- const screenshotImage = PNG.sync.read(screenshotFileBuffer);
191
- infoManager.set({
192
- slowDuration: payload.slowDuration,
193
- new: {
194
- height: screenshotImage.height,
195
- path: screenshotFileName.substring(rootLength),
196
- width: screenshotImage.width
197
- }
198
- });
199
-
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.' };
250
+ const screenshotImage = PNG.sync.read(screenshotFileBuffers[alt]);
251
+ infoManager.set({
252
+ slowDuration: payload.slowDuration,
253
+ new: {
254
+ height: screenshotImage.height,
255
+ path: screenshotFileName.substring(rootLength),
256
+ width: screenshotImage.width
203
257
  }
258
+ }, alt);
204
259
 
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
- });
214
-
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
- }
250
- } else {
251
- return { resizeRequired: true };
252
- }
253
- }
254
- return await runCompareTest();
255
- } else if (command === 'brightspace-visual-diff-compare-resize') {
256
- const screenshotImage = PNG.sync.read(await readFile(screenshotFileName));
257
- const goldenImage = PNG.sync.read(await readFile(goldenFileName));
258
-
259
- const newWidth = Math.max(screenshotImage.width, goldenImage.width);
260
- const newHeight = Math.max(screenshotImage.height, goldenImage.height);
261
- const newSize = { width: newWidth, height: newHeight };
262
-
263
- const newScreenshots = await createComparisonPNGs(screenshotImage, newSize);
264
- const newGoldens = await createComparisonPNGs(goldenImage, newSize);
265
-
266
- let bestIndex = -1;
267
- let bestDiffImage = null;
268
- let bestPixelsDiff = Number.MAX_SAFE_INTEGER;
269
- for (let i = 0; i < newScreenshots.length; i++) {
270
- const { diff, pixelsDiff } = getPixelsDiff(newScreenshots[i].png, newGoldens[i].png);
271
-
272
- if (pixelsDiff < bestPixelsDiff) {
273
- bestIndex = i;
274
- bestDiffImage = diff;
275
- bestPixelsDiff = pixelsDiff;
276
- }
260
+ const goldenExists = await checkFileExists(goldenFileName);
261
+ if (!goldenExists) {
262
+ return { pass: false, message: 'No golden exists. Use the "--golden" CLI flag to re-run and re-generate goldens.' };
277
263
  }
278
264
 
279
- await writeFile(`${screenshotFile}-resized-screenshot.png`, PNG.sync.write(newScreenshots[bestIndex].png));
280
- await writeFile(`${screenshotFile}-resized-golden.png`, PNG.sync.write(newGoldens[bestIndex].png));
281
- await writeFile(`${screenshotFile}-diff.png`, PNG.sync.write(bestDiffImage));
282
-
283
- const rootLength = join(rootDir, PATHS.VDIFF_ROOT).length + 1;
265
+ const goldenFileBuffer = await readFile(goldenFileName);
266
+ const goldenImage = PNG.sync.read(goldenFileBuffer);
284
267
  infoManager.set({
285
- diff: `${screenshotFile.substring(rootLength)}-diff.png`,
286
268
  golden: {
287
- path: `${screenshotFile.substring(rootLength)}-resized-golden.png`
288
- },
289
- new: {
290
- path: `${screenshotFile.substring(rootLength)}-resized-screenshot.png`
291
- },
292
- pixelsDiff: bestPixelsDiff
293
- });
269
+ height: goldenImage.height,
270
+ path: goldenFileName.substring(rootLength),
271
+ width: goldenImage.width
272
+ }
273
+ }, alt);
274
+
275
+ if (screenshotImage.width === goldenImage.width && screenshotImage.height === goldenImage.height) {
276
+ const goldenSize = (await stat(goldenFileName)).size;
277
+ const screenshotSize = (await stat(screenshotFileName)).size;
278
+ if (goldenSize === screenshotSize && screenshotFileBuffers[alt].equals(goldenFileBuffer)) {
279
+ const success = await tryMoveFile(screenshotFileName, passFileName);
280
+ if (!success) return { pass: false, message: 'Problem moving file to "pass" directory.' };
281
+ infoManager.set({
282
+ new: {
283
+ path: passFileName.substring(rootLength)
284
+ }
285
+ }, alt);
286
+ return { pass: true };
287
+ }
288
+
289
+ const { diff, pixelsDiff } = getPixelsDiff(screenshotImage, goldenImage);
290
+
291
+ if (pixelsDiff !== 0) {
292
+ infoManager.set({
293
+ diff: getFileName(`${screenshotFile}-diff`, alt).substring(rootLength),
294
+ pixelsDiff
295
+ }, alt);
296
+ await writeFile(getFileName(`${screenshotFile}-diff`, alt), PNG.sync.write(diff));
297
+ return { pass: false, message: `Image does not match golden. ${pixelsDiff} pixels are different.` };
298
+ } else {
299
+ infoManager.set({
300
+ golden: {
301
+ byteSize: goldenSize
302
+ },
303
+ new: {
304
+ byteSize: screenshotSize
305
+ },
306
+ pixelsDiff
307
+ }, alt);
308
+ return { pass: false, message: 'Image diff is clean but the images do not have the same bytes.' };
309
+ }
310
+ } else {
311
+ const { screenshot, golden, diff, pixelsDiff } = await getBestResizePng(screenshotImage, goldenImage);
312
+
313
+ const screenshotResizedPath = getFileName(`${screenshotFile}-resized-screenshot`, alt);
314
+ const goldenResizedPath = getFileName(`${screenshotFile}-resized-golden`, alt);
315
+ const diffPath = getFileName(`${screenshotFile}-diff`, alt);
316
+ await writeFile(screenshotResizedPath, PNG.sync.write(screenshot));
317
+ await writeFile(goldenResizedPath, PNG.sync.write(golden));
318
+ await writeFile(diffPath, PNG.sync.write(diff));
319
+ infoManager.set({
320
+ diff: diffPath.substring(rootLength),
321
+ golden: {
322
+ path: goldenResizedPath.substring(rootLength)
323
+ },
324
+ new: {
325
+ path: screenshotResizedPath.substring(rootLength)
326
+ },
327
+ pixelsDiff
328
+ }, alt);
294
329
 
295
- return { pass: false, message: `Images are not the same size. When resized, ${bestPixelsDiff} pixels are different.` };
330
+ return { pass: false, message: `Images are not the same size. When resized, ${pixelsDiff} pixels are different.` };
331
+ }
296
332
  }
297
333
 
334
+ bestPosition = null; // reset best position
335
+ let pass = true;
336
+ let errors = '';
337
+ const testResults = {};
338
+ for (const test of tests) {
339
+ testResults[test] = await runCompareTest(test);
340
+ if (!testResults[test].pass) {
341
+ pass = false;
342
+ errors += `\n(${test}) ${testResults[test].message}`;
343
+ }
344
+ }
345
+ if (pass) return { pass: true };
346
+ else if (tests.length === 1) return testResults[tests[0]];
347
+ else return { pass: false, message: `One or more tests failed:${errors}` };
298
348
  }
299
349
  };
300
350
  }
@@ -85,39 +85,43 @@ function flattenResults(session, browserData, fileData) {
85
85
 
86
86
  function collectTests(prefix, tests) {
87
87
  tests.forEach(t => {
88
- const testName = `${prefix}${t.name}`;
88
+ let testName = `${prefix}${t.name}`;
89
89
  const testKey = testName.replaceAll(' > ', ' ');
90
- const info = getTestInfo(session, testKey);
90
+ const allTestInfo = getTestInfo(session, testKey);
91
91
 
92
92
  // tests missing info but with no error were skipped via grep, so exclude them
93
- if (!info && !t.error) return;
94
-
95
- if (!fileData.tests.has(testName)) {
96
- fileData.tests.set(testName, {
97
- name: testName,
98
- numByteDiff: 0,
99
- numFailed: 0,
100
- results: []
101
- });
102
- }
103
- const testData = fileData.tests.get(testName);
104
- const bytediff = !t.passed && info?.diff === undefined && info?.pixelsDiff === 0;
105
- if (!t.passed) {
106
- if (bytediff) {
107
- browserData.numByteDiff++;
108
- testData.numByteDiff++;
93
+ if (!allTestInfo && !t.error) return;
94
+
95
+ for (const test in allTestInfo) {
96
+ const info = allTestInfo[test];
97
+ if (test !== 'default') testName = `${prefix}${t.name} (${test})`;
98
+ if (!fileData.tests.has(testName)) {
99
+ fileData.tests.set(testName, {
100
+ name: testName,
101
+ numByteDiff: 0,
102
+ numFailed: 0,
103
+ results: []
104
+ });
105
+ }
106
+ const testData = fileData.tests.get(testName);
107
+ const bytediff = !t.passed && info?.diff === undefined && info?.pixelsDiff === 0;
108
+ if (!t.passed) {
109
+ if (bytediff) {
110
+ browserData.numByteDiff++;
111
+ testData.numByteDiff++;
112
+ }
113
+ browserData.numFailed++;
114
+ testData.numFailed++;
109
115
  }
110
- browserData.numFailed++;
111
- testData.numFailed++;
116
+ testData.results.push({
117
+ name: browserData.name,
118
+ duration: t.duration,
119
+ error: t.error?.message,
120
+ passed: t.passed,
121
+ bytediff,
122
+ info: info
123
+ });
112
124
  }
113
- testData.results.push({
114
- name: browserData.name,
115
- duration: t.duration,
116
- error: t.error?.message,
117
- passed: t.passed,
118
- bytediff,
119
- info: info
120
- });
121
125
  });
122
126
  }
123
127