flexysnap 0.3.1 → 0.4.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.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Flexible visual snapshot testing for Playwright, built for e-commerce.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -60,10 +60,7 @@ 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' },
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' }
63
+ error: { fill: 'rgba(211, 47, 47, 0.4)', stroke: '#D32F2F' }
67
64
  };
68
65
 
69
66
  function determineColorKey(obj, defaultKey) {
@@ -71,24 +68,90 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
71
68
  return defaultKey;
72
69
  }
73
70
 
74
- const differenceTypes = obj.differences.map(difference => difference.type);
71
+ const hasError = obj.differences.some(difference => difference.kind === 'error');
75
72
 
76
- if (differenceTypes.includes('missing_text') || differenceTypes.includes('missing_element')) {
77
- return 'missing';
73
+ return hasError ? 'error' : defaultKey;
74
+ }
75
+
76
+ function hasDifferenceType(differences, types) {
77
+ if (!differences || !Array.isArray(differences) || differences.length === 0) {
78
+ return false;
78
79
  }
79
80
 
80
- if (differenceTypes.includes('extra_text') || differenceTypes.includes('extra_element')) {
81
- return 'extra';
81
+ return differences.some(difference => types.includes(difference.type));
82
+ }
83
+
84
+ function getLineDash(differences) {
85
+ const hasLayoutShift = hasDifferenceType(differences, ['layout_shift']);
86
+ const hasSizeMismatch = hasDifferenceType(differences, ['size_mismatch']);
87
+
88
+ if (hasLayoutShift && hasSizeMismatch) {
89
+ return [6, 2, 1, 2];
82
90
  }
83
91
 
84
- if (differenceTypes.includes('layout_shift') || differenceTypes.includes('size_mismatch')) {
85
- return 'shift';
92
+ if (hasLayoutShift) {
93
+ return [6, 4];
86
94
  }
87
95
 
88
- return 'error';
96
+ if (hasSizeMismatch) {
97
+ return [2, 2];
98
+ }
99
+
100
+ return [];
89
101
  }
90
102
 
91
- function drawBoundingBox(rect, colorKey, dashed = false) {
103
+ function drawHatchedFill(rect, width, height, fillColor) {
104
+ ctx.save();
105
+ ctx.beginPath();
106
+ ctx.rect(rect.left, rect.top, width, height);
107
+ ctx.clip();
108
+
109
+ ctx.strokeStyle = fillColor;
110
+ ctx.lineWidth = 2;
111
+ ctx.setLineDash([]);
112
+
113
+ const spacing = 6;
114
+ const diagonalReach = width + height;
115
+
116
+ for (let offset = -height; offset < diagonalReach; offset += spacing) {
117
+ ctx.beginPath();
118
+ ctx.moveTo(rect.left + offset, rect.top);
119
+ ctx.lineTo(rect.left + offset + height, rect.top + height);
120
+ ctx.stroke();
121
+ }
122
+
123
+ ctx.restore();
124
+ }
125
+
126
+ function drawDiagonalCross(rect, strokeColor) {
127
+ ctx.strokeStyle = strokeColor;
128
+ ctx.lineWidth = 2;
129
+ ctx.setLineDash([]);
130
+ ctx.beginPath();
131
+ ctx.moveTo(rect.left, rect.top);
132
+ ctx.lineTo(rect.right, rect.bottom);
133
+ ctx.moveTo(rect.right, rect.top);
134
+ ctx.lineTo(rect.left, rect.bottom);
135
+ ctx.stroke();
136
+ }
137
+
138
+ function drawUprightCross(rect, strokeColor) {
139
+ ctx.strokeStyle = strokeColor;
140
+ ctx.lineWidth = 2;
141
+ ctx.setLineDash([]);
142
+
143
+ const midX = (rect.left + rect.right) / 2;
144
+ const midY = (rect.top + rect.bottom) / 2;
145
+
146
+ ctx.beginPath();
147
+ ctx.moveTo(midX, rect.top);
148
+ ctx.lineTo(midX, rect.bottom);
149
+ ctx.moveTo(rect.left, midY);
150
+ ctx.lineTo(rect.right, midY);
151
+ ctx.stroke();
152
+ }
153
+
154
+ function drawBoundingBox(rect, colorKey, differences) {
92
155
  const adjustedRect = {
93
156
  left: rect.left + offsetX,
94
157
  right: rect.right + offsetX,
@@ -100,31 +163,42 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
100
163
  const boxHeight = adjustedRect.bottom - adjustedRect.top;
101
164
  const color = colors[colorKey];
102
165
 
103
- ctx.fillStyle = color.fill;
104
- ctx.fillRect(adjustedRect.left, adjustedRect.top, boxWidth, boxHeight);
166
+ if (hasDifferenceType(differences, ['text_mismatch', 'histogram_difference'])) {
167
+ drawHatchedFill(adjustedRect, boxWidth, boxHeight, color.fill);
168
+ } else {
169
+ ctx.fillStyle = color.fill;
170
+ ctx.fillRect(adjustedRect.left, adjustedRect.top, boxWidth, boxHeight);
171
+ }
172
+
105
173
  ctx.strokeStyle = color.stroke;
106
174
  ctx.lineWidth = 2;
107
- ctx.setLineDash(dashed ? [6, 4] : []);
175
+ ctx.setLineDash(getLineDash(differences));
108
176
  ctx.strokeRect(adjustedRect.left, adjustedRect.top, boxWidth, boxHeight);
109
177
  ctx.setLineDash([]);
178
+
179
+ if (hasDifferenceType(differences, ['missing_element', 'missing_text'])) {
180
+ drawDiagonalCross(adjustedRect, color.stroke);
181
+ }
182
+
183
+ if (hasDifferenceType(differences, ['extra_element', 'extra_text'])) {
184
+ drawUprightCross(adjustedRect, color.stroke);
185
+ }
110
186
  }
111
187
 
112
188
  for (const elementGroup of wireframe.elementGroups) {
113
- const dashed = elementGroup.options?.strictPosition === false;
114
-
115
189
  for (const element of elementGroup.elements) {
116
190
  const rect = element.boundingRect;
117
191
 
118
192
  if (element.type === 'box') {
119
193
  const colorKey = determineColorKey(element, 'box');
120
- drawBoundingBox(rect, colorKey, dashed);
194
+ drawBoundingBox(rect, colorKey, element.differences);
121
195
  } else if (element.type === 'image') {
122
196
  const colorKey = determineColorKey(element, 'image');
123
- drawBoundingBox(rect, colorKey, dashed);
197
+ drawBoundingBox(rect, colorKey, element.differences);
124
198
  } else if (element.type === 'text' && element.texts && element.texts.length > 0) {
125
199
  for (const text of element.texts) {
126
200
  const colorKey = determineColorKey(text, 'text');
127
- drawBoundingBox(text.boundingRect, colorKey, dashed);
201
+ drawBoundingBox(text.boundingRect, colorKey, text.differences);
128
202
  }
129
203
  }
130
204
  }
@@ -186,6 +186,7 @@ function compareBoundingBoxes(baselineRect, currentRect, options) {
186
186
  if (boundingBoxDifference > 10) {
187
187
  differences.push({
188
188
  type: "layout_shift",
189
+ kind: 'error',
189
190
  difference: boundingBoxDifference
190
191
  });
191
192
  }
@@ -202,6 +203,7 @@ function compareBoundingBoxes(baselineRect, currentRect, options) {
202
203
  if (sizeDiff > 10) {
203
204
  differences.push({
204
205
  type: "size_mismatch",
206
+ kind: 'error',
205
207
  difference: sizeDiff
206
208
  });
207
209
  }
@@ -210,6 +212,7 @@ function compareBoundingBoxes(baselineRect, currentRect, options) {
210
212
  if (diffRatio > 1.1 || diffRatio < 0.9) {
211
213
  differences.push({
212
214
  type: "size_mismatch",
215
+ kind: 'note',
213
216
  difference: diffRatio
214
217
  });
215
218
  }
@@ -243,6 +246,7 @@ function compareTexts(baselineTexts, currentTexts, options) {
243
246
  if (textsEqual) {
244
247
  currentText.differences.push({
245
248
  type: 'text_mismatch',
249
+ kind: 'error',
246
250
  baseline: baselineText.text,
247
251
  current: currentText.text,
248
252
  });
@@ -256,22 +260,24 @@ function compareTexts(baselineTexts, currentTexts, options) {
256
260
  }
257
261
  }
258
262
 
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
- }
263
+ const allowExtraText = options?.allowExtra === true;
264
+ for (const currentIndex of unmatchedCurrent) {
265
+ const currentText = currentTexts[currentIndex];
266
+ currentText.differences = [{
267
+ type: 'extra_text',
268
+ kind: allowExtraText ? 'note' : 'error',
269
+ }];
270
+ }
266
271
 
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
- }
272
+ const allowMissingText = options?.allowMissing === true;
273
+ for (const baselineIndex of unmatchedBaseline) {
274
+ const baselineText = baselineTexts[baselineIndex];
275
+ currentTexts.push(baselineText);
276
+ baselineText.differences = [{
277
+ type: 'missing_text',
278
+ kind: allowMissingText ? 'note' : 'error',
279
+ }];
280
+ }
275
281
 
276
282
  for (const currentText of currentTexts) {
277
283
  if (currentText.differences?.length === 0) {
@@ -286,6 +292,7 @@ function compareElements(baselineElement, currentElement, options) {
286
292
  if (dh > 15) {
287
293
  currentElement.differences.push({
288
294
  type: 'histogram_difference',
295
+ kind: 'error',
289
296
  });
290
297
  }
291
298
  }
@@ -328,22 +335,24 @@ function compareWireframes(baselineWireframe, currentWireframe) {
328
335
  }
329
336
  }
330
337
 
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
+ const allowExtraElements = currentElementGroup.options?.allowExtra === true;
339
+ for (const currentIndex of unmatchedCurrent) {
340
+ const currentElement = currentElementGroup.elements[currentIndex];
341
+ currentElement.differences = [{
342
+ type: 'extra_element',
343
+ kind: allowExtraElements ? 'note' : 'error',
344
+ }];
345
+ }
338
346
 
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
- }
347
+ const allowMissingElements = currentElementGroup.options?.allowMissing === true;
348
+ for (const baselineIndex of unmatchedBaseline) {
349
+ const baselineElement = baselineElementGroup.elements[baselineIndex];
350
+ currentElementGroup.elements.push(baselineElement);
351
+ baselineElement.differences = [{
352
+ type: 'missing_element',
353
+ kind: allowMissingElements ? 'note' : 'error',
354
+ }];
355
+ }
347
356
  }
348
357
 
349
358
  return currentWireframe;
@@ -109,8 +109,8 @@ function createEmptyElementGroupResult(elementGroup) {
109
109
  strictPosition: elementGroup.options?.strictPosition !== false,
110
110
  strictSize: elementGroup.options?.strictSize !== false,
111
111
  maskDigits: elementGroup.options?.maskDigits !== false,
112
- extraAllowed: elementGroup.options?.extraAllowed === true,
113
- missingAllowed: elementGroup.options?.missingAllowed === true
112
+ allowExtra: elementGroup.options?.allowExtra === true,
113
+ allowMissing: elementGroup.options?.allowMissing === true
114
114
  },
115
115
  elements: []
116
116
  };
@@ -145,6 +145,13 @@ async function extractElementGroupData(page, elementGroup, groupIndex, elementId
145
145
  };
146
146
  }
147
147
 
148
+ function isRectWithinViewport(rect) {
149
+ return rect.right >= 0 &&
150
+ rect.left < window.innerWidth &&
151
+ rect.bottom >= 0 &&
152
+ rect.top < window.innerHeight;
153
+ }
154
+
148
155
  function isElementVisible(element) {
149
156
  const style = window.getComputedStyle(element);
150
157
  if (style.display === 'none' ||
@@ -155,12 +162,9 @@ async function extractElementGroupData(page, elementGroup, groupIndex, elementId
155
162
  return false;
156
163
  }
157
164
 
158
- const rect = element.getBoundingClientRect()
165
+ const rect = element.getBoundingClientRect();
159
166
 
160
- return rect.right >= 0 &&
161
- rect.left < window.innerWidth &&
162
- rect.bottom >= 0 &&
163
- rect.top < window.innerHeight;
167
+ return isRectWithinViewport(rect);
164
168
  }
165
169
 
166
170
  function getElementsWithDirectText(root, textIgnoreClasses) {
@@ -205,8 +209,8 @@ async function extractElementGroupData(page, elementGroup, groupIndex, elementId
205
209
  elementGroup.options.strictPosition = elementGroup.options.strictPosition !== false;
206
210
  elementGroup.options.strictSize = elementGroup.options.strictSize !== false;
207
211
  elementGroup.options.maskDigits = elementGroup.options.maskDigits !== false;
208
- elementGroup.options.extraAllowed = elementGroup.options.extraAllowed === true;
209
- elementGroup.options.missingAllowed = elementGroup.options.missingAllowed === true;
212
+ elementGroup.options.allowExtra = elementGroup.options.allowExtra === true;
213
+ elementGroup.options.allowMissing = elementGroup.options.allowMissing === true;
210
214
  elementGroup.elements = [];
211
215
 
212
216
  let index = 0;
@@ -237,9 +241,14 @@ async function extractElementGroupData(page, elementGroup, groupIndex, elementId
237
241
  if (isNested)
238
242
  continue;
239
243
 
244
+ const boundingRect = getTextNodeBoundingBox(descendant);
245
+
246
+ if (!isRectWithinViewport(boundingRect))
247
+ continue;
248
+
240
249
  texts.push({
241
250
  text: text,
242
- boundingRect: getTextNodeBoundingBox(descendant)
251
+ boundingRect: boundingRect
243
252
  });
244
253
  }
245
254
  }