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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexysnap",
3
- "version": "0.1.3-alpha.1",
3
+ "version": "0.1.4-alpha.1",
4
4
  "description": "Flexible visual snapshot testing for Playwright, built for e-commerce.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -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 boxWidth = rect.right - rect.left;
56
- const boxHeight = rect.bottom - rect.top;
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(rect.left, rect.top, boxWidth, boxHeight);
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(rect.left, rect.top, boxWidth, boxHeight);
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
+ };
@@ -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 (const elementGroup of elementGroups) {
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
- elementGroup.elements.push({
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(elementGroup.selector).nth(element.index);
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
- window.scrollTo(scrollPosition.x, scrollPosition.y);
208
- }, {scrollPosition});
209
- return wireframeData;
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
- currentWireframeData = await extractWireframe(page, cloneElementGroups(elementGroups));
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, configName, outputFile, outputName, retryDelay = 1000, maxRetryCount = 10) {
246
- logTimestamp(`Starting wireframe capture for: ${outputFile}`);
247
- const wireframeData = await extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount);
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 userType = process.env.USER_TYPE;
250
- const deviceType = process.env.DEVICE_TYPE;
251
- const testType = process.env.TEST_TYPE;
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
- deviceType: process.env.DEVICE_TYPE,
257
- userType: process.env.USER_TYPE,
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(process.cwd(), 'wireframes', 'test', testType, configName, deviceType, userType, fileName);
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(fileDir, `${outputFile}.png`);
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 };