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 +1 -1
- package/src/annotateWireframe.js +28 -7
- package/src/wireframeComparison.js +170 -58
- package/src/wireframeUtils.js +13 -6
package/package.json
CHANGED
package/src/annotateWireframe.js
CHANGED
|
@@ -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
|
|
67
|
-
|
|
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 =
|
|
119
|
+
const colorKey = determineColorKey(element, 'box');
|
|
99
120
|
drawBoundingBox(rect, colorKey, dashed);
|
|
100
121
|
} else if (element.type === 'image') {
|
|
101
|
-
const colorKey =
|
|
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 =
|
|
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
|
|
14
|
+
function pairByBoundingBox(currentIndices, baselineIndices, currentElements, baselineElements) {
|
|
15
15
|
const pairings = [];
|
|
16
|
-
|
|
17
|
-
for (
|
|
18
|
-
for (
|
|
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 <
|
|
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 <
|
|
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,
|
|
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,
|
|
114
|
-
const {
|
|
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
|
-
|
|
140
|
-
strictSize
|
|
253
|
+
options
|
|
141
254
|
);
|
|
142
255
|
currentText.differences = currentText.differences.concat(boundingBoxDifferences);
|
|
143
256
|
}
|
|
144
257
|
}
|
|
145
258
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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,
|
|
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,
|
|
294
|
+
compareTexts(baselineElement.texts, currentElement.texts, options);
|
|
180
295
|
} else {
|
|
181
296
|
currentElement.differences = compareBoundingBoxes(
|
|
182
297
|
baselineElement.boundingRect,
|
|
183
298
|
currentElement.boundingRect,
|
|
184
|
-
|
|
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
|
|
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,
|
|
325
|
+
compareElements(baselineElement, currentElement, currentElementGroup.options);
|
|
216
326
|
if (currentElement.differences?.length === 0) {
|
|
217
327
|
currentElement.differences = undefined
|
|
218
328
|
}
|
|
219
329
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 };
|
package/src/wireframeUtils.js
CHANGED
|
@@ -105,9 +105,13 @@ function scaleWireframeData(wireframeData, scaleFactor) {
|
|
|
105
105
|
function createEmptyElementGroupResult(elementGroup) {
|
|
106
106
|
return {
|
|
107
107
|
...elementGroup,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
201
|
-
elementGroup.
|
|
202
|
-
elementGroup.
|
|
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;
|