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 +2 -2
- package/src/wireframeComparison.js +7 -13
- package/src/wireframeUtils.js +130 -90
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flexysnap",
|
|
3
|
-
"version": "0.2.
|
|
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,
|
package/src/wireframeUtils.js
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
elementGroup
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
elementGroup.strictPosition = elementGroup.strictPosition !== false;
|
|
201
|
+
elementGroup.strictSize = elementGroup.strictSize !== false;
|
|
202
|
+
elementGroup.maskDigits = elementGroup.maskDigits !== false;
|
|
203
|
+
elementGroup.elements = [];
|
|
196
204
|
|
|
197
|
-
|
|
205
|
+
let index = 0;
|
|
206
|
+
const matchedElements = document.querySelectorAll(elementGroup.selector);
|
|
198
207
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
for (const element of matchedElements) {
|
|
209
|
+
if (!isElementVisible(element))
|
|
210
|
+
continue;
|
|
202
211
|
|
|
203
|
-
|
|
204
|
-
if (text.length === 0)
|
|
205
|
-
continue;
|
|
212
|
+
const texts = [];
|
|
206
213
|
|
|
207
|
-
|
|
208
|
-
other !== descendant && other.contains(descendant)
|
|
209
|
-
);
|
|
214
|
+
if (elementGroup.type === 'text') {
|
|
210
215
|
|
|
211
|
-
|
|
212
|
-
continue;
|
|
216
|
+
const descendantArray = getElementsWithDirectText(element, elementGroup.textIgnoreClasses)
|
|
213
217
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
222
|
-
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
|
|
235
|
-
|
|
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
|
|
240
|
-
}
|
|
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,
|