flexysnap 0.1.3-alpha.1 → 0.1.4-alpha.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 +27 -4
- package/src/index.js +7 -1
- package/src/wireframeOutput.js +29 -0
- package/src/wireframeUtils.js +62 -28
package/package.json
CHANGED
package/src/annotateWireframe.js
CHANGED
|
@@ -16,6 +16,16 @@ function applyGrayscale(ctx, width, height) {
|
|
|
16
16
|
ctx.putImageData(imageData, 0, 0);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function getScrollOffset(wireframe) {
|
|
20
|
+
const extractionScrollPosition = wireframe.scrollPosition || { x: 0, y: 0 };
|
|
21
|
+
const screenshotScrollPosition = wireframe.screenshotScrollPosition || extractionScrollPosition;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
offsetX: extractionScrollPosition.x - screenshotScrollPosition.x,
|
|
25
|
+
offsetY: extractionScrollPosition.y - screenshotScrollPosition.y
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
async function annotateWireframeFile(wireframeFile, screenshotFile) {
|
|
20
30
|
if (!fs.existsSync(wireframeFile)) {
|
|
21
31
|
console.error(`Wireframe file not found: ${wireframeFile}`);
|
|
@@ -40,6 +50,12 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
|
|
|
40
50
|
ctx.drawImage(screenshot, 0, 0);
|
|
41
51
|
applyGrayscale(ctx, width, height);
|
|
42
52
|
|
|
53
|
+
const { offsetX, offsetY } = getScrollOffset(wireframe);
|
|
54
|
+
|
|
55
|
+
if (offsetX !== 0 || offsetY !== 0) {
|
|
56
|
+
console.log(`Compensating for scroll drift between extraction and screenshot: offsetX=${offsetX}, offsetY=${offsetY}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
const colors = {
|
|
44
60
|
box: { fill: 'rgba(220, 160, 40, 0.4)', stroke: '#E6A500' },
|
|
45
61
|
image: { fill: 'rgba(70, 180, 120, 0.4)', stroke: '#2EBC6F' },
|
|
@@ -52,16 +68,23 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
|
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
function drawBoundingBox(rect, colorKey, dashed = false) {
|
|
55
|
-
const
|
|
56
|
-
|
|
71
|
+
const adjustedRect = {
|
|
72
|
+
left: rect.left + offsetX,
|
|
73
|
+
right: rect.right + offsetX,
|
|
74
|
+
top: rect.top + offsetY,
|
|
75
|
+
bottom: rect.bottom + offsetY
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const boxWidth = adjustedRect.right - adjustedRect.left;
|
|
79
|
+
const boxHeight = adjustedRect.bottom - adjustedRect.top;
|
|
57
80
|
const color = colors[colorKey];
|
|
58
81
|
|
|
59
82
|
ctx.fillStyle = color.fill;
|
|
60
|
-
ctx.fillRect(
|
|
83
|
+
ctx.fillRect(adjustedRect.left, adjustedRect.top, boxWidth, boxHeight);
|
|
61
84
|
ctx.strokeStyle = color.stroke;
|
|
62
85
|
ctx.lineWidth = 2;
|
|
63
86
|
ctx.setLineDash(dashed ? [6, 4] : []);
|
|
64
|
-
ctx.strokeRect(
|
|
87
|
+
ctx.strokeRect(adjustedRect.left, adjustedRect.top, boxWidth, boxHeight);
|
|
65
88
|
ctx.setLineDash([]);
|
|
66
89
|
}
|
|
67
90
|
|
package/src/index.js
CHANGED
|
@@ -23,4 +23,10 @@ export {
|
|
|
23
23
|
|
|
24
24
|
export { areWireframesStable } from './wireframeStability.js';
|
|
25
25
|
|
|
26
|
-
export { expectWireframe, getRGBHistogramFromBuffer } from './wireframeUtils.js';
|
|
26
|
+
export { expectWireframe, getRGBHistogramFromBuffer } from './wireframeUtils.js';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
setWireframeOutputRoot,
|
|
30
|
+
getWireframeOutputRoot,
|
|
31
|
+
resolveWireframeOutputDir
|
|
32
|
+
} from './wireframeOutput.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
let outputRoot = process.cwd();
|
|
4
|
+
|
|
5
|
+
function setWireframeOutputRoot(rootPath) {
|
|
6
|
+
outputRoot = rootPath || process.cwd();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getWireframeOutputRoot() {
|
|
10
|
+
return outputRoot;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveWireframeOutputDir(outputDir) {
|
|
14
|
+
if (!outputDir || typeof outputDir !== 'string') {
|
|
15
|
+
throw new Error('resolveWireframeOutputDir requires a non-empty string outputDir.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (path.isAbsolute(outputDir)) {
|
|
19
|
+
return outputDir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return path.join(outputRoot, outputDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
setWireframeOutputRoot,
|
|
27
|
+
getWireframeOutputRoot,
|
|
28
|
+
resolveWireframeOutputDir
|
|
29
|
+
};
|
package/src/wireframeUtils.js
CHANGED
|
@@ -3,6 +3,9 @@ import path from 'path';
|
|
|
3
3
|
import { logTimestamp } from './testUtils.js';
|
|
4
4
|
import { loadImage, createCanvas } from 'canvas';
|
|
5
5
|
import { areWireframesStable } from './wireframeStability.js';
|
|
6
|
+
import { resolveWireframeOutputDir } from './wireframeOutput.js';
|
|
7
|
+
|
|
8
|
+
const FLEXYSNAP_ELEMENT_ID_ATTRIBUTE = 'data-flexysnap-id';
|
|
6
9
|
|
|
7
10
|
async function getRGBHistogramFromBuffer(buffer) {
|
|
8
11
|
const image = await loadImage(buffer);
|
|
@@ -37,7 +40,7 @@ function delay(milliseconds) {
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
async function extractWireframe(page, elementGroups) {
|
|
40
|
-
const wireframeData = await page.evaluate(async ({elementGroups}) => {
|
|
43
|
+
const wireframeData = await page.evaluate(async ({elementGroups, elementIdAttribute}) => {
|
|
41
44
|
|
|
42
45
|
function createBoundingRect(element) {
|
|
43
46
|
const rect = element.getBoundingClientRect();
|
|
@@ -80,7 +83,8 @@ async function extractWireframe(page, elementGroups) {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
|
|
83
|
-
for (
|
|
86
|
+
for (let groupIndex = 0; groupIndex < elementGroups.length; groupIndex++) {
|
|
87
|
+
const elementGroup = elementGroups[groupIndex];
|
|
84
88
|
elementGroup.strictPosition = elementGroup.strictPosition !== false;
|
|
85
89
|
elementGroup.elements = [];
|
|
86
90
|
|
|
@@ -159,18 +163,26 @@ async function extractWireframe(page, elementGroups) {
|
|
|
159
163
|
}
|
|
160
164
|
}
|
|
161
165
|
|
|
162
|
-
|
|
166
|
+
const elementData = {
|
|
163
167
|
index: index,
|
|
164
168
|
boundingRect: createBoundingRect(element),
|
|
165
169
|
texts: texts,
|
|
166
170
|
type: elementGroup.type
|
|
167
|
-
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (elementGroup.type === 'image') {
|
|
174
|
+
const elementId = `fsnap-g${groupIndex}-e${index}`;
|
|
175
|
+
element.setAttribute(elementIdAttribute, elementId);
|
|
176
|
+
elementData.elementId = elementId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
elementGroup.elements.push(elementData);
|
|
168
180
|
index += 1;
|
|
169
181
|
}
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
return elementGroups;
|
|
173
|
-
}, {elementGroups});
|
|
185
|
+
}, {elementGroups, elementIdAttribute: FLEXYSNAP_ELEMENT_ID_ATTRIBUTE});
|
|
174
186
|
|
|
175
187
|
const scrollPosition = await page.evaluate(() => ({
|
|
176
188
|
x: window.scrollX,
|
|
@@ -179,8 +191,8 @@ async function extractWireframe(page, elementGroups) {
|
|
|
179
191
|
|
|
180
192
|
for (const elementGroup of wireframeData) {
|
|
181
193
|
for (const element of elementGroup.elements) {
|
|
182
|
-
if (element.type === 'image') {
|
|
183
|
-
const locator = page.locator(
|
|
194
|
+
if (element.type === 'image' && element.elementId) {
|
|
195
|
+
const locator = page.locator(`[${FLEXYSNAP_ELEMENT_ID_ATTRIBUTE}="${element.elementId}"]`);
|
|
184
196
|
await locator.waitFor({state: 'visible'});
|
|
185
197
|
|
|
186
198
|
try {
|
|
@@ -199,14 +211,28 @@ async function extractWireframe(page, elementGroups) {
|
|
|
199
211
|
} catch {
|
|
200
212
|
element.type = 'box'
|
|
201
213
|
}
|
|
214
|
+
|
|
215
|
+
delete element.elementId;
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
218
|
}
|
|
205
219
|
|
|
206
|
-
await page.evaluate(({scrollPosition}) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
await page.evaluate(({scrollPosition, elementIdAttribute}) => {
|
|
221
|
+
for (const el of document.querySelectorAll(`[${elementIdAttribute}]`)) {
|
|
222
|
+
el.removeAttribute(elementIdAttribute);
|
|
223
|
+
}
|
|
224
|
+
window.scrollTo({top: scrollPosition.y, left: scrollPosition.x, behavior: 'instant'});
|
|
225
|
+
}, {scrollPosition, elementIdAttribute: FLEXYSNAP_ELEMENT_ID_ATTRIBUTE});
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await page.waitForFunction((expectedScrollPosition) => {
|
|
229
|
+
return window.scrollX === expectedScrollPosition.x && window.scrollY === expectedScrollPosition.y;
|
|
230
|
+
}, scrollPosition, {timeout: 2000});
|
|
231
|
+
} catch {
|
|
232
|
+
logTimestamp('Scroll position did not settle back to the extraction position.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {wireframeData, scrollPosition};
|
|
210
236
|
}
|
|
211
237
|
|
|
212
238
|
function cloneElementGroups(elementGroups) {
|
|
@@ -216,17 +242,20 @@ function cloneElementGroups(elementGroups) {
|
|
|
216
242
|
async function extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount) {
|
|
217
243
|
let previousWireframeData = null;
|
|
218
244
|
let currentWireframeData = null;
|
|
245
|
+
let currentScrollPosition = null;
|
|
219
246
|
|
|
220
247
|
for (let attempt = 0; attempt < maxRetryCount; attempt++) {
|
|
221
248
|
const extractionStartTime = Date.now();
|
|
222
|
-
|
|
249
|
+
const extractionResult = await extractWireframe(page, cloneElementGroups(elementGroups));
|
|
250
|
+
currentWireframeData = extractionResult.wireframeData;
|
|
251
|
+
currentScrollPosition = extractionResult.scrollPosition;
|
|
223
252
|
const extractionDuration = Date.now() - extractionStartTime;
|
|
224
253
|
logTimestamp(`Wireframe candidate captured after ${extractionDuration/1000} seconds.`)
|
|
225
254
|
|
|
226
255
|
if (previousWireframeData !== null &&
|
|
227
256
|
areWireframesStable(previousWireframeData, currentWireframeData)) {
|
|
228
257
|
logTimestamp(`Wireframe stabilized after ${attempt + 1} extraction(s)`);
|
|
229
|
-
return currentWireframeData;
|
|
258
|
+
return {wireframeData: currentWireframeData, scrollPosition: currentScrollPosition};
|
|
230
259
|
}
|
|
231
260
|
|
|
232
261
|
previousWireframeData = currentWireframeData;
|
|
@@ -239,37 +268,42 @@ async function extractStableWireframe(page, elementGroups, retryDelay, maxRetryC
|
|
|
239
268
|
}
|
|
240
269
|
|
|
241
270
|
logTimestamp(`Wireframe did not stabilize within ${maxRetryCount} extraction(s)`);
|
|
242
|
-
return currentWireframeData;
|
|
271
|
+
return {wireframeData: currentWireframeData, scrollPosition: currentScrollPosition};
|
|
243
272
|
}
|
|
244
273
|
|
|
245
|
-
async function expectWireframe(page, elementGroups,
|
|
246
|
-
|
|
247
|
-
|
|
274
|
+
async function expectWireframe(page, elementGroups, outputDir, outputFile, outputName, options = {}) {
|
|
275
|
+
const { retryDelay = 1000, maxRetryCount = 10, metadata = {} } = options;
|
|
276
|
+
|
|
277
|
+
logTimestamp(`Starting wireframe capture for: ${outputFile}`);
|
|
278
|
+
const { wireframeData, scrollPosition } = await extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount);
|
|
248
279
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
280
|
+
const screenshotScrollPosition = await page.evaluate(() => ({
|
|
281
|
+
x: window.scrollX,
|
|
282
|
+
y: window.scrollY
|
|
283
|
+
}));
|
|
252
284
|
|
|
253
285
|
const wireframeOutput = {
|
|
254
286
|
name: outputName,
|
|
255
287
|
timestamp: new Date().toISOString(),
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
elementGroups: wireframeData
|
|
288
|
+
scrollPosition,
|
|
289
|
+
screenshotScrollPosition,
|
|
290
|
+
elementGroups: wireframeData,
|
|
291
|
+
...metadata
|
|
259
292
|
};
|
|
260
293
|
|
|
294
|
+
const resolvedDir = resolveWireframeOutputDir(outputDir);
|
|
295
|
+
fs.mkdirSync(resolvedDir, {recursive: true});
|
|
296
|
+
|
|
261
297
|
const fileName = `${outputFile}.json`;
|
|
262
|
-
const filePath = path.join(
|
|
263
|
-
const fileDir = path.dirname(filePath);
|
|
264
|
-
fs.mkdirSync(fileDir, {recursive: true});
|
|
298
|
+
const filePath = path.join(resolvedDir, fileName);
|
|
265
299
|
fs.writeFileSync(filePath, JSON.stringify(wireframeOutput, null, 2));
|
|
266
300
|
logTimestamp(`Wireframe captured and saved to: ${fileName}`);
|
|
267
301
|
|
|
268
|
-
const screenshotPath = path.join(
|
|
302
|
+
const screenshotPath = path.join(resolvedDir, `${outputFile}.png`);
|
|
269
303
|
await page.screenshot({ path: screenshotPath });
|
|
270
304
|
logTimestamp(`Wireframe screenshot saved to: ${screenshotPath}`);
|
|
271
305
|
|
|
272
|
-
return {wireframeOutput, screenshotPath};
|
|
306
|
+
return {wireframeOutput, screenshotPath, outputDir: resolvedDir};
|
|
273
307
|
}
|
|
274
308
|
|
|
275
309
|
export { expectWireframe, getRGBHistogramFromBuffer };
|