flexysnap 0.2.7 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexysnap",
3
- "version": "0.2.7",
3
+ "version": "0.3.1",
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: '#D32F2F' },
65
+ extra: { fill: 'rgba(156, 39, 176, 0.4)', stroke: '#D32F2F' },
66
+ shift: { fill: 'rgba(121, 85, 72, 0.4)', stroke: '#D32F2F' }
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
  }
@@ -11,16 +11,16 @@ function calculateBoundingBoxDistance(rect1, rect2) {
11
11
  Math.abs(rect1.bottom - rect2.bottom));
12
12
  }
13
13
 
14
- function createPairings(currentElements, baselineElements) {
14
+ function pairByBoundingBox(currentIndices, baselineIndices, currentElements, baselineElements) {
15
15
  const pairings = [];
16
-
17
- for (let currentIndex = 0; currentIndex < currentElements.length; currentIndex++) {
18
- for (let baselineIndex = 0; baselineIndex < baselineElements.length; baselineIndex++) {
16
+
17
+ for (const currentIndex of currentIndices) {
18
+ for (const baselineIndex of baselineIndices) {
19
19
  const distance = calculateBoundingBoxDistance(
20
20
  currentElements[currentIndex].boundingRect,
21
21
  baselineElements[baselineIndex].boundingRect
22
22
  );
23
-
23
+
24
24
  pairings.push({
25
25
  currentIndex,
26
26
  baselineIndex,
@@ -34,7 +34,7 @@ function createPairings(currentElements, baselineElements) {
34
34
  const usedCurrentIndices = new Set();
35
35
  const usedBaselineIndices = new Set();
36
36
  const matchedPairs = [];
37
-
37
+
38
38
  for (const pairing of pairings) {
39
39
  if (!usedCurrentIndices.has(pairing.currentIndex) && !usedBaselineIndices.has(pairing.baselineIndex)) {
40
40
  usedCurrentIndices.add(pairing.currentIndex);
@@ -42,21 +42,134 @@ function createPairings(currentElements, baselineElements) {
42
42
  matchedPairs.push(pairing);
43
43
  }
44
44
  }
45
-
45
+
46
+ const unmatchedCurrent = [];
47
+ for (const currentIndex of currentIndices) {
48
+ if (!usedCurrentIndices.has(currentIndex)) {
49
+ unmatchedCurrent.push(currentIndex);
50
+ }
51
+ }
52
+
53
+ const unmatchedBaseline = [];
54
+ for (const baselineIndex of baselineIndices) {
55
+ if (!usedBaselineIndices.has(baselineIndex)) {
56
+ unmatchedBaseline.push(baselineIndex);
57
+ }
58
+ }
59
+
60
+ return {
61
+ matchedPairs,
62
+ unmatchedCurrent,
63
+ unmatchedBaseline
64
+ };
65
+ }
66
+
67
+ function createPairings(currentElements, baselineElements) {
68
+ const currentIndices = currentElements.map((_, index) => index);
69
+ const baselineIndices = baselineElements.map((_, index) => index);
70
+
71
+ return pairByBoundingBox(currentIndices, baselineIndices, currentElements, baselineElements);
72
+ }
73
+
74
+ function buildTextIndexMap(texts) {
75
+ const map = new Map();
76
+ texts.forEach((textElement, index) => {
77
+ const key = textElement.text;
78
+ if (!map.has(key)) {
79
+ map.set(key, []);
80
+ }
81
+ map.get(key).push(index);
82
+ });
83
+ return map;
84
+ }
85
+
86
+ function createTextPairings(currentTexts, baselineTexts) {
87
+ const currentTextMap = buildTextIndexMap(currentTexts);
88
+ const baselineTextMap = buildTextIndexMap(baselineTexts);
89
+
90
+ const usedCurrentIndices = new Set();
91
+ const usedBaselineIndices = new Set();
92
+ const matchedPairs = [];
93
+
94
+ for (const [text, currentIndices] of currentTextMap) {
95
+ const baselineIndices = baselineTextMap.get(text);
96
+ if (currentIndices.length === 1 && baselineIndices && baselineIndices.length === 1) {
97
+ const currentIndex = currentIndices[0];
98
+ const baselineIndex = baselineIndices[0];
99
+ matchedPairs.push({
100
+ currentIndex,
101
+ baselineIndex,
102
+ distance: 0
103
+ });
104
+ usedCurrentIndices.add(currentIndex);
105
+ usedBaselineIndices.add(baselineIndex);
106
+ }
107
+ }
108
+
109
+ for (const [text, currentIndices] of currentTextMap) {
110
+ const baselineIndices = baselineTextMap.get(text) || [];
111
+ const remainingCurrentIndices = currentIndices.filter(index => !usedCurrentIndices.has(index));
112
+ const remainingBaselineIndices = baselineIndices.filter(index => !usedBaselineIndices.has(index));
113
+
114
+ if (remainingCurrentIndices.length > 0 && remainingBaselineIndices.length > 0) {
115
+ const result = pairByBoundingBox(
116
+ remainingCurrentIndices,
117
+ remainingBaselineIndices,
118
+ currentTexts,
119
+ baselineTexts
120
+ );
121
+
122
+ for (const pairing of result.matchedPairs) {
123
+ matchedPairs.push(pairing);
124
+ usedCurrentIndices.add(pairing.currentIndex);
125
+ usedBaselineIndices.add(pairing.baselineIndex);
126
+ }
127
+ }
128
+ }
129
+
130
+ const remainingCurrentIndices = [];
131
+ for (let i = 0; i < currentTexts.length; i++) {
132
+ if (!usedCurrentIndices.has(i)) {
133
+ remainingCurrentIndices.push(i);
134
+ }
135
+ }
136
+
137
+ const remainingBaselineIndices = [];
138
+ for (let i = 0; i < baselineTexts.length; i++) {
139
+ if (!usedBaselineIndices.has(i)) {
140
+ remainingBaselineIndices.push(i);
141
+ }
142
+ }
143
+
144
+ if (remainingCurrentIndices.length > 0 && remainingBaselineIndices.length > 0) {
145
+ const result = pairByBoundingBox(
146
+ remainingCurrentIndices,
147
+ remainingBaselineIndices,
148
+ currentTexts,
149
+ baselineTexts
150
+ );
151
+
152
+ for (const pairing of result.matchedPairs) {
153
+ matchedPairs.push(pairing);
154
+ usedCurrentIndices.add(pairing.currentIndex);
155
+ usedBaselineIndices.add(pairing.baselineIndex);
156
+ }
157
+ }
158
+
46
159
  const unmatchedCurrent = [];
47
- for (let i = 0; i < currentElements.length; i++) {
160
+ for (let i = 0; i < currentTexts.length; i++) {
48
161
  if (!usedCurrentIndices.has(i)) {
49
162
  unmatchedCurrent.push(i);
50
163
  }
51
164
  }
52
-
165
+
53
166
  const unmatchedBaseline = [];
54
- for (let i = 0; i < baselineElements.length; i++) {
167
+ for (let i = 0; i < baselineTexts.length; i++) {
55
168
  if (!usedBaselineIndices.has(i)) {
56
169
  unmatchedBaseline.push(i);
57
170
  }
58
171
  }
59
-
172
+
60
173
  return {
61
174
  matchedPairs,
62
175
  unmatchedCurrent,
@@ -64,9 +177,10 @@ function createPairings(currentElements, baselineElements) {
64
177
  };
65
178
  }
66
179
 
67
- function compareBoundingBoxes(baselineRect, currentRect, strictPosition = true, strictSize = true) {
180
+ function compareBoundingBoxes(baselineRect, currentRect, options) {
68
181
  const differences = [];
69
-
182
+ const strictPosition = options?.strictPosition !== false;
183
+ const strictSize = options?.strictSize !== false;
70
184
  if (strictPosition) {
71
185
  const boundingBoxDifference = calculateBoundingBoxDistance(baselineRect, currentRect)
72
186
  if (boundingBoxDifference > 10) {
@@ -110,15 +224,15 @@ function replaceDigitsWithPlaceholder(text) {
110
224
  return text.replace(/\d+/g, '[digits]');
111
225
  }
112
226
 
113
- function compareTexts(baselineTexts, currentTexts, strictPosition = true, strictSize = true, maskDigits = false) {
114
- const { matchedPairs, unmatchedCurrent, unmatchedBaseline } = createPairings(currentTexts, baselineTexts);
115
-
227
+ function compareTexts(baselineTexts, currentTexts, options) {
228
+ const {matchedPairs, unmatchedCurrent, unmatchedBaseline} = createTextPairings(currentTexts, baselineTexts);
229
+
116
230
  for (const pairing of matchedPairs) {
117
231
  const baselineText = baselineTexts[pairing.baselineIndex];
118
232
  const currentText = currentTexts[pairing.currentIndex];
119
233
  currentText.differences = [];
120
234
 
121
-
235
+ const maskDigits = options?.maskDigits === true;
122
236
  let textsEqual = false
123
237
  if (maskDigits) {
124
238
  textsEqual = replaceDigitsWithPlaceholder(baselineText.text) !== replaceDigitsWithPlaceholder(currentText.text);
@@ -136,27 +250,28 @@ function compareTexts(baselineTexts, currentTexts, strictPosition = true, strict
136
250
  const boundingBoxDifferences = compareBoundingBoxes(
137
251
  baselineText.boundingRect,
138
252
  currentText.boundingRect,
139
- strictPosition,
140
- strictSize
253
+ options
141
254
  );
142
255
  currentText.differences = currentText.differences.concat(boundingBoxDifferences);
143
256
  }
144
257
  }
145
258
 
146
- for (const currentIndex of unmatchedCurrent) {
147
- const currentText = currentTexts[currentIndex];
148
- currentText.differences = [{
149
- type: 'extra_text',
150
- }];
151
- }
259
+ if (options?.extraAllowed !== true)
260
+ for (const currentIndex of unmatchedCurrent) {
261
+ const currentText = currentTexts[currentIndex];
262
+ currentText.differences = [{
263
+ type: 'extra_text',
264
+ }];
265
+ }
152
266
 
153
- for (const baselineIndex of unmatchedBaseline) {
154
- const baselineText = baselineTexts[baselineIndex];
155
- currentTexts.push(baselineText);
156
- baselineText.differences = [{
157
- type: 'missing_text',
158
- }];
159
- }
267
+ if (options.missingAllowed !== true)
268
+ for (const baselineIndex of unmatchedBaseline) {
269
+ const baselineText = baselineTexts[baselineIndex];
270
+ currentTexts.push(baselineText);
271
+ baselineText.differences = [{
272
+ type: 'missing_text',
273
+ }];
274
+ }
160
275
 
161
276
  for (const currentText of currentTexts) {
162
277
  if (currentText.differences?.length === 0) {
@@ -165,7 +280,7 @@ function compareTexts(baselineTexts, currentTexts, strictPosition = true, strict
165
280
  }
166
281
  }
167
282
 
168
- function compareElements(baselineElement, currentElement, strictPosition = true, strictSize = true, maskDigits = false) {
283
+ function compareElements(baselineElement, currentElement, options) {
169
284
  if (baselineElement.histogram && currentElement.histogram) {
170
285
  const dh = histogramDiff(baselineElement.histogram, currentElement.histogram);
171
286
  if (dh > 15) {
@@ -176,13 +291,12 @@ function compareElements(baselineElement, currentElement, strictPosition = true,
176
291
  }
177
292
 
178
293
  if (baselineElement.texts.length > 0 || currentElement.texts.length > 0) {
179
- compareTexts(baselineElement.texts, currentElement.texts, strictPosition, strictSize, maskDigits);
294
+ compareTexts(baselineElement.texts, currentElement.texts, options);
180
295
  } else {
181
296
  currentElement.differences = compareBoundingBoxes(
182
297
  baselineElement.boundingRect,
183
298
  currentElement.boundingRect,
184
- strictPosition,
185
- strictSize
299
+ options
186
300
  );
187
301
  }
188
302
  }
@@ -200,11 +314,7 @@ function compareWireframes(baselineWireframe, currentWireframe) {
200
314
 
201
315
  expect(baselineElementGroup.selector).toEqual(currentElementGroup.selector);
202
316
 
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(
317
+ const {matchedPairs, unmatchedCurrent, unmatchedBaseline} = createPairings(
208
318
  currentElementGroup.elements,
209
319
  baselineElementGroup.elements
210
320
  );
@@ -212,29 +322,31 @@ function compareWireframes(baselineWireframe, currentWireframe) {
212
322
  for (const pairing of matchedPairs) {
213
323
  const baselineElement = baselineElementGroup.elements[pairing.baselineIndex];
214
324
  const currentElement = currentElementGroup.elements[pairing.currentIndex];
215
- compareElements(baselineElement, currentElement, strictPosition, strictSize, maskDigits);
325
+ compareElements(baselineElement, currentElement, currentElementGroup.options);
216
326
  if (currentElement.differences?.length === 0) {
217
327
  currentElement.differences = undefined
218
328
  }
219
329
  }
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
- }
330
+
331
+ if (currentElementGroup.options?.extraAllowed !== true)
332
+ for (const currentIndex of unmatchedCurrent) {
333
+ const currentElement = currentElementGroup.elements[currentIndex];
334
+ currentElement.differences = [{
335
+ type: 'extra_element',
336
+ }];
337
+ }
338
+
339
+ if (currentElementGroup.options?.missingAllowed !== true)
340
+ for (const baselineIndex of unmatchedBaseline) {
341
+ const baselineElement = baselineElementGroup.elements[baselineIndex];
342
+ currentElementGroup.elements.push(baselineElement);
343
+ baselineElement.differences = [{
344
+ type: 'missing_element',
345
+ }];
346
+ }
235
347
  }
236
348
 
237
349
  return currentWireframe;
238
350
  }
239
351
 
240
- export { compareWireframes };
352
+ export { compareWireframes };
@@ -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;