flexysnap 0.2.3-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 +1 -1
- package/src/wireframeComparison.js +9 -16
- package/src/wireframeUtils.js +130 -90
package/package.json
CHANGED
|
@@ -126,8 +126,7 @@ function compareTexts(baselineTexts, currentTexts, strictPosition = true, strict
|
|
|
126
126
|
textsEqual = baselineText.text !== currentText.text;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
if (
|
|
130
|
-
//expect.soft(currentText.text).toEqual(baselineText.text);
|
|
129
|
+
if (textsEqual) {
|
|
131
130
|
currentText.differences.push({
|
|
132
131
|
type: 'text_mismatch',
|
|
133
132
|
baseline: baselineText.text,
|
|
@@ -167,16 +166,8 @@ function compareTexts(baselineTexts, currentTexts, strictPosition = true, strict
|
|
|
167
166
|
}
|
|
168
167
|
|
|
169
168
|
function compareElements(baselineElement, currentElement, strictPosition = true, strictSize = true, maskDigits = false) {
|
|
170
|
-
currentElement.differences = compareBoundingBoxes(
|
|
171
|
-
baselineElement.boundingRect,
|
|
172
|
-
currentElement.boundingRect,
|
|
173
|
-
strictPosition,
|
|
174
|
-
strictSize
|
|
175
|
-
);
|
|
176
|
-
|
|
177
169
|
if (baselineElement.histogram && currentElement.histogram) {
|
|
178
170
|
const dh = histogramDiff(baselineElement.histogram, currentElement.histogram);
|
|
179
|
-
//expect.soft(dh, 'Histogram difference').toBeLessThanOrEqual(10);
|
|
180
171
|
if (dh > 15) {
|
|
181
172
|
currentElement.differences.push({
|
|
182
173
|
type: 'histogram_difference',
|
|
@@ -186,6 +177,13 @@ function compareElements(baselineElement, currentElement, strictPosition = true,
|
|
|
186
177
|
|
|
187
178
|
if (baselineElement.texts.length > 0 || currentElement.texts.length > 0) {
|
|
188
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
|
+
);
|
|
189
187
|
}
|
|
190
188
|
}
|
|
191
189
|
|
|
@@ -205,11 +203,6 @@ function compareWireframes(baselineWireframe, currentWireframe) {
|
|
|
205
203
|
const strictPosition = currentElementGroup.strictPosition !== false;
|
|
206
204
|
const strictSize = currentElementGroup.strictSize !== false;
|
|
207
205
|
const maskDigits = currentElementGroup.maskDigits === true;
|
|
208
|
-
|
|
209
|
-
//TODO: REMOVE later
|
|
210
|
-
if (currentElementGroup.differences?.length === 0) {
|
|
211
|
-
currentElementGroup.differences = undefined
|
|
212
|
-
}
|
|
213
206
|
|
|
214
207
|
const { matchedPairs, unmatchedCurrent, unmatchedBaseline } = createPairings(
|
|
215
208
|
currentElementGroup.elements,
|
|
@@ -244,4 +237,4 @@ function compareWireframes(baselineWireframe, currentWireframe) {
|
|
|
244
237
|
return currentWireframe;
|
|
245
238
|
}
|
|
246
239
|
|
|
247
|
-
export { compareWireframes };
|
|
240
|
+
export { compareWireframes };
|
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,
|