flexysnap 0.2.4-alpha.1 → 0.2.5

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.4-alpha.1",
3
+ "version": "0.2.5",
4
4
  "description": "Flexible visual snapshot testing for Playwright, built for e-commerce.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -49,4 +49,4 @@
49
49
  "devDependencies": {
50
50
  "eslint": "^9.9.0"
51
51
  }
52
- }
52
+ }
@@ -166,16 +166,8 @@ function compareTexts(baselineTexts, currentTexts, strictPosition = true, strict
166
166
  }
167
167
 
168
168
  function compareElements(baselineElement, currentElement, strictPosition = true, strictSize = true, maskDigits = false) {
169
- currentElement.differences = compareBoundingBoxes(
170
- baselineElement.boundingRect,
171
- currentElement.boundingRect,
172
- strictPosition,
173
- strictSize
174
- );
175
-
176
169
  if (baselineElement.histogram && currentElement.histogram) {
177
170
  const dh = histogramDiff(baselineElement.histogram, currentElement.histogram);
178
- //expect.soft(dh, 'Histogram difference').toBeLessThanOrEqual(10);
179
171
  if (dh > 15) {
180
172
  currentElement.differences.push({
181
173
  type: 'histogram_difference',
@@ -185,6 +177,13 @@ function compareElements(baselineElement, currentElement, strictPosition = true,
185
177
 
186
178
  if (baselineElement.texts.length > 0 || currentElement.texts.length > 0) {
187
179
  compareTexts(baselineElement.texts, currentElement.texts, strictPosition, strictSize, maskDigits);
180
+ } else {
181
+ currentElement.differences = compareBoundingBoxes(
182
+ baselineElement.boundingRect,
183
+ currentElement.boundingRect,
184
+ strictPosition,
185
+ strictSize
186
+ );
188
187
  }
189
188
  }
190
189
 
@@ -204,11 +203,6 @@ function compareWireframes(baselineWireframe, currentWireframe) {
204
203
  const strictPosition = currentElementGroup.strictPosition !== false;
205
204
  const strictSize = currentElementGroup.strictSize !== false;
206
205
  const maskDigits = currentElementGroup.maskDigits === true;
207
-
208
- //TODO: REMOVE later
209
- if (currentElementGroup.differences?.length === 0) {
210
- currentElementGroup.differences = undefined
211
- }
212
206
 
213
207
  const { matchedPairs, unmatchedCurrent, unmatchedBaseline } = createPairings(
214
208
  currentElementGroup.elements,
@@ -6,6 +6,7 @@ import { areWireframesStable } from './wireframeStability.js';
6
6
  import { resolveWireframeOutputDir } from './wireframeOutput.js';
7
7
 
8
8
  const FLEXYSNAP_ELEMENT_ID_ATTRIBUTE = 'data-flexysnap-id';
9
+ const ELEMENT_GROUP_EXTRACTION_TIMEOUT_MS = 5000;
9
10
 
10
11
  async function getRGBHistogramFromBuffer(buffer) {
11
12
  const image = await loadImage(buffer);
@@ -39,6 +40,15 @@ function delay(milliseconds) {
39
40
  return new Promise(resolve => setTimeout(resolve, milliseconds));
40
41
  }
41
42
 
43
+ function withTimeout(promise, milliseconds, timeoutMessage) {
44
+ let timeoutHandle;
45
+ const timeoutPromise = new Promise((_, reject) => {
46
+ timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), milliseconds);
47
+ });
48
+
49
+ return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutHandle));
50
+ }
51
+
42
52
  async function getDeviceScaleFactor(page) {
43
53
  try {
44
54
  const scaleFactor = await page.evaluate(() => window.devicePixelRatio);
@@ -92,66 +102,62 @@ function scaleWireframeData(wireframeData, scaleFactor) {
92
102
  return wireframeData;
93
103
  }
94
104
 
95
- async function extractWireframe(page, elementGroups) {
96
- const wireframeData = await page.evaluate(async ({elementGroups, elementIdAttribute}) => {
97
-
98
- function createBoundingRect(element) {
99
- const rect = element.getBoundingClientRect();
100
- return {
101
- top: Math.round(rect.top),
102
- left: Math.round(rect.left),
103
- bottom: Math.round(rect.bottom),
104
- right: Math.round(rect.right)
105
- };
106
- }
107
-
108
- function getTextNodeBoundingBox(textNode) {
109
- const range = document.createRange();
110
- range.selectNodeContents(textNode);
111
- const rect = range.getBoundingClientRect();
112
- return {
113
- top: Math.round(rect.top),
114
- left: Math.round(rect.left),
115
- bottom: Math.round(rect.bottom),
116
- right: Math.round(rect.right)
117
- };
118
- }
119
-
120
- function isElementVisible(element) {
121
- const style = window.getComputedStyle(element);
122
- if (style.display === 'none' ||
123
- style.visibility === 'hidden' ||
124
- style.opacity === '0' ||
125
- element.offsetWidth <= 0 ||
126
- element.offsetHeight <= 0) {
127
- return false;
128
- }
129
-
130
- const rect = element.getBoundingClientRect()
131
-
132
- return rect.right >= 0 &&
133
- rect.left < window.innerWidth &&
134
- rect.bottom >= 0 &&
135
- rect.top < window.innerHeight;
136
- }
105
+ function createEmptyElementGroupResult(elementGroup) {
106
+ return {
107
+ ...elementGroup,
108
+ strictPosition: elementGroup.strictPosition !== false,
109
+ strictSize: elementGroup.strictSize !== false,
110
+ maskDigits: elementGroup.maskDigits !== false,
111
+ elements: []
112
+ };
113
+ }
137
114
 
115
+ async function extractElementGroupData(page, elementGroup, groupIndex, elementIdAttribute) {
116
+ const extractionStartTime = Date.now();
138
117
 
139
- for (let groupIndex = 0; groupIndex < elementGroups.length; groupIndex++) {
140
- const elementGroup = elementGroups[groupIndex];
141
- elementGroup.strictPosition = elementGroup.strictPosition !== false;
142
- elementGroup.strictSize = elementGroup.strictSize !== false;
143
- elementGroup.maskDigits = elementGroup.maskDigits !== false;
144
- elementGroup.elements = [];
118
+ try {
119
+ const result = await withTimeout(
120
+ page.evaluate(({elementGroup, groupIndex, elementIdAttribute}) => {
121
+
122
+ function createBoundingRect(element) {
123
+ const rect = element.getBoundingClientRect();
124
+ return {
125
+ top: Math.round(rect.top),
126
+ left: Math.round(rect.left),
127
+ bottom: Math.round(rect.bottom),
128
+ right: Math.round(rect.right)
129
+ };
130
+ }
145
131
 
146
- let index = 0;
147
- const matchedElements = document.querySelectorAll(elementGroup.selector);
132
+ function getTextNodeBoundingBox(textNode) {
133
+ const range = document.createRange();
134
+ range.selectNodeContents(textNode);
135
+ const rect = range.getBoundingClientRect();
136
+ return {
137
+ top: Math.round(rect.top),
138
+ left: Math.round(rect.left),
139
+ bottom: Math.round(rect.bottom),
140
+ right: Math.round(rect.right)
141
+ };
142
+ }
148
143
 
149
- for (const element of matchedElements) {
150
- if (!isElementVisible(element))
151
- continue;
144
+ function isElementVisible(element) {
145
+ const style = window.getComputedStyle(element);
146
+ if (style.display === 'none' ||
147
+ style.visibility === 'hidden' ||
148
+ style.opacity === '0' ||
149
+ element.offsetWidth <= 0 ||
150
+ element.offsetHeight <= 0) {
151
+ return false;
152
+ }
152
153
 
154
+ const rect = element.getBoundingClientRect()
153
155
 
154
- const texts = [];
156
+ return rect.right >= 0 &&
157
+ rect.left < window.innerWidth &&
158
+ rect.bottom >= 0 &&
159
+ rect.top < window.innerHeight;
160
+ }
155
161
 
156
162
  function getElementsWithDirectText(root, textIgnoreClasses) {
157
163
  const results = [];
@@ -168,7 +174,6 @@ async function extractWireframe(page, elementGroups) {
168
174
 
169
175
  if (textIgnoreClasses) {
170
176
  let ignored = false;
171
- console.log(textIgnoreClasses);
172
177
  let parent = walker.currentNode.parentElement;
173
178
  while (parent != null && !ignored) {
174
179
  for (const className of parent.classList) {
@@ -192,52 +197,87 @@ async function extractWireframe(page, elementGroups) {
192
197
  return results;
193
198
  }
194
199
 
195
- if (elementGroup.type === 'text') {
200
+ elementGroup.strictPosition = elementGroup.strictPosition !== false;
201
+ elementGroup.strictSize = elementGroup.strictSize !== false;
202
+ elementGroup.maskDigits = elementGroup.maskDigits !== false;
203
+ elementGroup.elements = [];
196
204
 
197
- const descendantArray = getElementsWithDirectText(element, elementGroup.textIgnoreClasses)
205
+ let index = 0;
206
+ const matchedElements = document.querySelectorAll(elementGroup.selector);
198
207
 
199
- for (const descendant of descendantArray) {
200
- if (!isElementVisible(descendant.parentElement))
201
- continue;
208
+ for (const element of matchedElements) {
209
+ if (!isElementVisible(element))
210
+ continue;
202
211
 
203
- const text = descendant.textContent.trim().replaceAll(/\s+/g, ' ')
204
- if (text.length === 0)
205
- continue;
212
+ const texts = [];
206
213
 
207
- const isNested = descendantArray.some(other =>
208
- other !== descendant && other.contains(descendant)
209
- );
214
+ if (elementGroup.type === 'text') {
210
215
 
211
- if (isNested)
212
- continue;
216
+ const descendantArray = getElementsWithDirectText(element, elementGroup.textIgnoreClasses)
213
217
 
214
- texts.push({
215
- text: text,
216
- boundingRect: getTextNodeBoundingBox(descendant)
217
- });
218
+ for (const descendant of descendantArray) {
219
+ if (!isElementVisible(descendant.parentElement))
220
+ continue;
221
+
222
+ const text = descendant.textContent.trim().replaceAll(/\s+/g, ' ')
223
+ if (text.length === 0)
224
+ continue;
225
+
226
+ const isNested = descendantArray.some(other =>
227
+ other !== descendant && other.contains(descendant)
228
+ );
229
+
230
+ if (isNested)
231
+ continue;
232
+
233
+ texts.push({
234
+ text: text,
235
+ boundingRect: getTextNodeBoundingBox(descendant)
236
+ });
237
+ }
238
+ }
239
+
240
+ const elementData = {
241
+ index: index,
242
+ boundingRect: createBoundingRect(element),
243
+ texts: texts,
244
+ type: elementGroup.type
245
+ };
246
+
247
+ if (elementGroup.type === 'image') {
248
+ const elementId = `fsnap-g${groupIndex}-e${index}`;
249
+ element.setAttribute(elementIdAttribute, elementId);
250
+ elementData.elementId = elementId;
218
251
  }
219
- }
220
252
 
221
- const elementData = {
222
- index: index,
223
- boundingRect: createBoundingRect(element),
224
- texts: texts,
225
- type: elementGroup.type
226
- };
227
-
228
- if (elementGroup.type === 'image') {
229
- const elementId = `fsnap-g${groupIndex}-e${index}`;
230
- element.setAttribute(elementIdAttribute, elementId);
231
- elementData.elementId = elementId;
253
+ elementGroup.elements.push(elementData);
254
+ index += 1;
232
255
  }
233
256
 
234
- elementGroup.elements.push(elementData);
235
- index += 1;
236
- }
237
- }
257
+ return elementGroup;
258
+ }, {elementGroup, groupIndex, elementIdAttribute}),
259
+ ELEMENT_GROUP_EXTRACTION_TIMEOUT_MS,
260
+ `Timed out extracting element group with selector "${elementGroup.selector}"`
261
+ );
262
+
263
+ const extractionDuration = Date.now() - extractionStartTime;
264
+ logTimestamp(`Extracted element group "${elementGroup.selector}" in ${extractionDuration/1000} seconds.`);
238
265
 
239
- return elementGroups;
240
- }, {elementGroups, elementIdAttribute: FLEXYSNAP_ELEMENT_ID_ATTRIBUTE});
266
+ return result;
267
+ } catch (error) {
268
+ const extractionDuration = Date.now() - extractionStartTime;
269
+ logTimestamp(`Failed to extract element group "${elementGroup.selector}" after ${extractionDuration/1000} seconds: ${error.message}`);
270
+ return createEmptyElementGroupResult(elementGroup);
271
+ }
272
+ }
273
+
274
+ async function extractWireframe(page, elementGroups) {
275
+ const wireframeData = [];
276
+
277
+ for (let groupIndex = 0; groupIndex < elementGroups.length; groupIndex++) {
278
+ const elementGroupData = await extractElementGroupData(page, elementGroups[groupIndex], groupIndex, FLEXYSNAP_ELEMENT_ID_ATTRIBUTE);
279
+ wireframeData.push(elementGroupData);
280
+ }
241
281
 
242
282
  const scrollPosition = await page.evaluate(() => ({
243
283
  x: window.scrollX,