flexysnap 0.2.6 → 0.3.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexysnap",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Flexible visual snapshot testing for Playwright, built for e-commerce.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -60,11 +60,32 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
60
60
  box: { fill: 'rgba(220, 160, 40, 0.4)', stroke: '#E6A500' },
61
61
  image: { fill: 'rgba(70, 180, 120, 0.4)', stroke: '#2EBC6F' },
62
62
  text: { fill: 'rgba(50, 130, 190, 0.4)', stroke: '#0066CC' },
63
- error: { fill: 'rgba(211, 47, 47, 0.4)', stroke: '#D32F2F' }
63
+ error: { fill: 'rgba(211, 47, 47, 0.4)', stroke: '#D32F2F' },
64
+ missing: { fill: 'rgba(233, 30, 140, 0.4)', stroke: '#E91E8C' },
65
+ extra: { fill: 'rgba(156, 39, 176, 0.4)', stroke: '#9C27B0' },
66
+ shift: { fill: 'rgba(121, 85, 72, 0.4)', stroke: '#795548' }
64
67
  };
65
68
 
66
- function hasErrors(obj) {
67
- return obj.differences && Array.isArray(obj.differences) && obj.differences.length > 0;
69
+ function determineColorKey(obj, defaultKey) {
70
+ if (!obj.differences || !Array.isArray(obj.differences) || obj.differences.length === 0) {
71
+ return defaultKey;
72
+ }
73
+
74
+ const differenceTypes = obj.differences.map(difference => difference.type);
75
+
76
+ if (differenceTypes.includes('missing_text') || differenceTypes.includes('missing_element')) {
77
+ return 'missing';
78
+ }
79
+
80
+ if (differenceTypes.includes('extra_text') || differenceTypes.includes('extra_element')) {
81
+ return 'extra';
82
+ }
83
+
84
+ if (differenceTypes.includes('layout_shift') || differenceTypes.includes('size_mismatch')) {
85
+ return 'shift';
86
+ }
87
+
88
+ return 'error';
68
89
  }
69
90
 
70
91
  function drawBoundingBox(rect, colorKey, dashed = false) {
@@ -89,20 +110,20 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
89
110
  }
90
111
 
91
112
  for (const elementGroup of wireframe.elementGroups) {
92
- const dashed = elementGroup.strictPosition === false;
113
+ const dashed = elementGroup.options?.strictPosition === false;
93
114
 
94
115
  for (const element of elementGroup.elements) {
95
116
  const rect = element.boundingRect;
96
117
 
97
118
  if (element.type === 'box') {
98
- const colorKey = hasErrors(element) ? 'error' : 'box';
119
+ const colorKey = determineColorKey(element, 'box');
99
120
  drawBoundingBox(rect, colorKey, dashed);
100
121
  } else if (element.type === 'image') {
101
- const colorKey = hasErrors(element) ? 'error' : 'image';
122
+ const colorKey = determineColorKey(element, 'image');
102
123
  drawBoundingBox(rect, colorKey, dashed);
103
124
  } else if (element.type === 'text' && element.texts && element.texts.length > 0) {
104
125
  for (const text of element.texts) {
105
- const colorKey = hasErrors(text) ? 'error' : 'text';
126
+ const colorKey = determineColorKey(text, 'text');
106
127
  drawBoundingBox(text.boundingRect, colorKey, dashed);
107
128
  }
108
129
  }
package/src/testUtils.js CHANGED
@@ -2,6 +2,10 @@ import { test } from '@playwright/test';
2
2
 
3
3
  let closePopups = async function(page) {}
4
4
 
5
+ async function callClosePopups(page) {
6
+ await closePopups(page);
7
+ }
8
+
5
9
  async function gotoWithRetry(page, url) {
6
10
  try {
7
11
  logTimestamp('Loading ' + url);
@@ -209,6 +213,7 @@ export {
209
213
  rehover,
210
214
  scrollToTopOfElement,
211
215
  setClosePopups,
216
+ callClosePopups,
212
217
  logTimestamp,
213
218
  setBaseUrl
214
219
  };
@@ -64,9 +64,10 @@ function createPairings(currentElements, baselineElements) {
64
64
  };
65
65
  }
66
66
 
67
- function compareBoundingBoxes(baselineRect, currentRect, strictPosition = true, strictSize = true) {
67
+ function compareBoundingBoxes(baselineRect, currentRect, options) {
68
68
  const differences = [];
69
-
69
+ const strictPosition = options?.strictPosition !== false;
70
+ const strictSize = options?.strictSize !== false;
70
71
  if (strictPosition) {
71
72
  const boundingBoxDifference = calculateBoundingBoxDistance(baselineRect, currentRect)
72
73
  if (boundingBoxDifference > 10) {
@@ -110,15 +111,15 @@ function replaceDigitsWithPlaceholder(text) {
110
111
  return text.replace(/\d+/g, '[digits]');
111
112
  }
112
113
 
113
- function compareTexts(baselineTexts, currentTexts, strictPosition = true, strictSize = true, maskDigits = false) {
114
- const { matchedPairs, unmatchedCurrent, unmatchedBaseline } = createPairings(currentTexts, baselineTexts);
115
-
114
+ function compareTexts(baselineTexts, currentTexts, options) {
115
+ const {matchedPairs, unmatchedCurrent, unmatchedBaseline} = createPairings(currentTexts, baselineTexts);
116
+
116
117
  for (const pairing of matchedPairs) {
117
118
  const baselineText = baselineTexts[pairing.baselineIndex];
118
119
  const currentText = currentTexts[pairing.currentIndex];
119
120
  currentText.differences = [];
120
121
 
121
-
122
+ const maskDigits = options?.maskDigits === true;
122
123
  let textsEqual = false
123
124
  if (maskDigits) {
124
125
  textsEqual = replaceDigitsWithPlaceholder(baselineText.text) !== replaceDigitsWithPlaceholder(currentText.text);
@@ -136,27 +137,28 @@ function compareTexts(baselineTexts, currentTexts, strictPosition = true, strict
136
137
  const boundingBoxDifferences = compareBoundingBoxes(
137
138
  baselineText.boundingRect,
138
139
  currentText.boundingRect,
139
- strictPosition,
140
- strictSize
140
+ options
141
141
  );
142
142
  currentText.differences = currentText.differences.concat(boundingBoxDifferences);
143
143
  }
144
144
  }
145
145
 
146
- for (const currentIndex of unmatchedCurrent) {
147
- const currentText = currentTexts[currentIndex];
148
- currentText.differences = [{
149
- type: 'extra_text',
150
- }];
151
- }
146
+ if (options?.extraAllowed !== true)
147
+ for (const currentIndex of unmatchedCurrent) {
148
+ const currentText = currentTexts[currentIndex];
149
+ currentText.differences = [{
150
+ type: 'extra_text',
151
+ }];
152
+ }
152
153
 
153
- for (const baselineIndex of unmatchedBaseline) {
154
- const baselineText = baselineTexts[baselineIndex];
155
- currentTexts.push(baselineText);
156
- baselineText.differences = [{
157
- type: 'missing_text',
158
- }];
159
- }
154
+ if (options.missingAllowed !== true)
155
+ for (const baselineIndex of unmatchedBaseline) {
156
+ const baselineText = baselineTexts[baselineIndex];
157
+ currentTexts.push(baselineText);
158
+ baselineText.differences = [{
159
+ type: 'missing_text',
160
+ }];
161
+ }
160
162
 
161
163
  for (const currentText of currentTexts) {
162
164
  if (currentText.differences?.length === 0) {
@@ -165,7 +167,7 @@ function compareTexts(baselineTexts, currentTexts, strictPosition = true, strict
165
167
  }
166
168
  }
167
169
 
168
- function compareElements(baselineElement, currentElement, strictPosition = true, strictSize = true, maskDigits = false) {
170
+ function compareElements(baselineElement, currentElement, options) {
169
171
  if (baselineElement.histogram && currentElement.histogram) {
170
172
  const dh = histogramDiff(baselineElement.histogram, currentElement.histogram);
171
173
  if (dh > 15) {
@@ -176,13 +178,12 @@ function compareElements(baselineElement, currentElement, strictPosition = true,
176
178
  }
177
179
 
178
180
  if (baselineElement.texts.length > 0 || currentElement.texts.length > 0) {
179
- compareTexts(baselineElement.texts, currentElement.texts, strictPosition, strictSize, maskDigits);
181
+ compareTexts(baselineElement.texts, currentElement.texts, options);
180
182
  } else {
181
183
  currentElement.differences = compareBoundingBoxes(
182
184
  baselineElement.boundingRect,
183
185
  currentElement.boundingRect,
184
- strictPosition,
185
- strictSize
186
+ options
186
187
  );
187
188
  }
188
189
  }
@@ -200,11 +201,7 @@ function compareWireframes(baselineWireframe, currentWireframe) {
200
201
 
201
202
  expect(baselineElementGroup.selector).toEqual(currentElementGroup.selector);
202
203
 
203
- const strictPosition = currentElementGroup.strictPosition !== false;
204
- const strictSize = currentElementGroup.strictSize !== false;
205
- const maskDigits = currentElementGroup.maskDigits === true;
206
-
207
- const { matchedPairs, unmatchedCurrent, unmatchedBaseline } = createPairings(
204
+ const {matchedPairs, unmatchedCurrent, unmatchedBaseline} = createPairings(
208
205
  currentElementGroup.elements,
209
206
  baselineElementGroup.elements
210
207
  );
@@ -212,29 +209,31 @@ function compareWireframes(baselineWireframe, currentWireframe) {
212
209
  for (const pairing of matchedPairs) {
213
210
  const baselineElement = baselineElementGroup.elements[pairing.baselineIndex];
214
211
  const currentElement = currentElementGroup.elements[pairing.currentIndex];
215
- compareElements(baselineElement, currentElement, strictPosition, strictSize, maskDigits);
212
+ compareElements(baselineElement, currentElement, currentElementGroup.options);
216
213
  if (currentElement.differences?.length === 0) {
217
214
  currentElement.differences = undefined
218
215
  }
219
216
  }
220
-
221
- for (const currentIndex of unmatchedCurrent) {
222
- const currentElement = currentElementGroup.elements[currentIndex];
223
- currentElement.differences = [{
224
- type: 'extra_element',
225
- }];
226
- }
227
-
228
- for (const baselineIndex of unmatchedBaseline) {
229
- const baselineElement = baselineElementGroup.elements[baselineIndex];
230
- currentElementGroup.elements.push(baselineElement);
231
- baselineElement.differences = [{
232
- type: 'missing_element',
233
- }];
234
- }
217
+
218
+ if (currentElementGroup.options?.extraAllowed !== true)
219
+ for (const currentIndex of unmatchedCurrent) {
220
+ const currentElement = currentElementGroup.elements[currentIndex];
221
+ currentElement.differences = [{
222
+ type: 'extra_element',
223
+ }];
224
+ }
225
+
226
+ if (currentElementGroup.options?.missingAllowed !== true)
227
+ for (const baselineIndex of unmatchedBaseline) {
228
+ const baselineElement = baselineElementGroup.elements[baselineIndex];
229
+ currentElementGroup.elements.push(baselineElement);
230
+ baselineElement.differences = [{
231
+ type: 'missing_element',
232
+ }];
233
+ }
235
234
  }
236
235
 
237
236
  return currentWireframe;
238
237
  }
239
238
 
240
- export { compareWireframes };
239
+ export { compareWireframes };
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { logTimestamp } from './testUtils.js';
3
+ import { logTimestamp, callClosePopups } from './testUtils.js';
4
4
  import { loadImage, createCanvas } from 'canvas';
5
5
  import { areWireframesStable } from './wireframeStability.js';
6
6
  import { resolveWireframeOutputDir } from './wireframeOutput.js';
@@ -105,9 +105,13 @@ function scaleWireframeData(wireframeData, scaleFactor) {
105
105
  function createEmptyElementGroupResult(elementGroup) {
106
106
  return {
107
107
  ...elementGroup,
108
- strictPosition: elementGroup.strictPosition !== false,
109
- strictSize: elementGroup.strictSize !== false,
110
- maskDigits: elementGroup.maskDigits !== false,
108
+ options: {
109
+ strictPosition: elementGroup.options?.strictPosition !== false,
110
+ strictSize: elementGroup.options?.strictSize !== false,
111
+ maskDigits: elementGroup.options?.maskDigits !== false,
112
+ extraAllowed: elementGroup.options?.extraAllowed === true,
113
+ missingAllowed: elementGroup.options?.missingAllowed === true
114
+ },
111
115
  elements: []
112
116
  };
113
117
  }
@@ -197,9 +201,12 @@ async function extractElementGroupData(page, elementGroup, groupIndex, elementId
197
201
  return results;
198
202
  }
199
203
 
200
- elementGroup.strictPosition = elementGroup.strictPosition !== false;
201
- elementGroup.strictSize = elementGroup.strictSize !== false;
202
- elementGroup.maskDigits = elementGroup.maskDigits !== false;
204
+ elementGroup.options = elementGroup.options || {};
205
+ elementGroup.options.strictPosition = elementGroup.options.strictPosition !== false;
206
+ elementGroup.options.strictSize = elementGroup.options.strictSize !== false;
207
+ elementGroup.options.maskDigits = elementGroup.options.maskDigits !== false;
208
+ elementGroup.options.extraAllowed = elementGroup.options.extraAllowed === true;
209
+ elementGroup.options.missingAllowed = elementGroup.options.missingAllowed === true;
203
210
  elementGroup.elements = [];
204
211
 
205
212
  let index = 0;
@@ -271,11 +278,11 @@ async function extractElementGroupData(page, elementGroup, groupIndex, elementId
271
278
  }
272
279
  }
273
280
 
274
- async function extractWireframe(page, elementGroups, closePopups) {
281
+ async function extractWireframe(page, elementGroups) {
275
282
  const wireframeData = [];
276
283
 
277
284
  for (let groupIndex = 0; groupIndex < elementGroups.length; groupIndex++) {
278
- closePopups();
285
+ await callClosePopups(page);
279
286
  const elementGroupData = await extractElementGroupData(page, elementGroups[groupIndex], groupIndex, FLEXYSNAP_ELEMENT_ID_ATTRIBUTE);
280
287
  wireframeData.push(elementGroupData);
281
288
  }
@@ -335,14 +342,14 @@ function cloneElementGroups(elementGroups) {
335
342
  return elementGroups.map(elementGroup => ({...elementGroup, elements: undefined}));
336
343
  }
337
344
 
338
- async function extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount, closePopups) {
345
+ async function extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount) {
339
346
  let previousWireframeData = null;
340
347
  let currentWireframeData = null;
341
348
  let currentScrollPosition = null;
342
349
 
343
350
  for (let attempt = 0; attempt < maxRetryCount; attempt++) {
344
351
  const extractionStartTime = Date.now();
345
- const extractionResult = await extractWireframe(page, cloneElementGroups(elementGroups), closePopups);
352
+ const extractionResult = await extractWireframe(page, cloneElementGroups(elementGroups));
346
353
  currentWireframeData = extractionResult.wireframeData;
347
354
  currentScrollPosition = extractionResult.scrollPosition;
348
355
  const extractionDuration = Date.now() - extractionStartTime;
@@ -368,10 +375,10 @@ async function extractStableWireframe(page, elementGroups, retryDelay, maxRetryC
368
375
  }
369
376
 
370
377
  async function expectWireframe(page, elementGroups, outputDir, outputFile, outputName, options = {}) {
371
- const { retryDelay = 1000, maxRetryCount = 10, metadata = {}, closePopups = function() {} } = options;
378
+ const { retryDelay = 1000, maxRetryCount = 10, metadata = {} } = options;
372
379
 
373
380
  logTimestamp(`Starting wireframe capture for: ${outputFile}`);
374
- const { wireframeData, scrollPosition } = await extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount, closePopups);
381
+ const { wireframeData, scrollPosition } = await extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount);
375
382
 
376
383
  const screenshotScrollPosition = await page.evaluate(() => ({
377
384
  x: window.scrollX,