flexysnap 0.1.2-alpha.3 → 0.1.3-alpha.2

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.2-alpha.3",
3
+ "version": "0.1.3-alpha.2",
4
4
  "description": "Flexible visual snapshot testing for Playwright, built for e-commerce.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -44,10 +44,9 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@playwright/test": "^1.61.1",
47
- "canvas": "^3.2.3",
48
- "sharp": "^0.35.3"
47
+ "canvas": "^3.2.3"
49
48
  },
50
49
  "devDependencies": {
51
50
  "eslint": "^9.9.0"
52
51
  }
53
- }
52
+ }
@@ -1,7 +1,30 @@
1
- import sharp from 'sharp';
2
1
  import fs from 'fs';
3
2
  import path from 'path';
4
- import { createCanvas } from 'canvas';
3
+ import { createCanvas, loadImage } from 'canvas';
4
+
5
+ function applyGrayscale(ctx, width, height) {
6
+ const imageData = ctx.getImageData(0, 0, width, height);
7
+ const data = imageData.data;
8
+
9
+ for (let i = 0; i < data.length; i += 4) {
10
+ const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
11
+ data[i] = gray;
12
+ data[i + 1] = gray;
13
+ data[i + 2] = gray;
14
+ }
15
+
16
+ ctx.putImageData(imageData, 0, 0);
17
+ }
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
+ }
5
28
 
6
29
  async function annotateWireframeFile(wireframeFile, screenshotFile) {
7
30
  if (!fs.existsSync(wireframeFile)) {
@@ -17,13 +40,22 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
17
40
  const wireframeContent = fs.readFileSync(wireframeFile, 'utf-8');
18
41
  const wireframe = JSON.parse(wireframeContent);
19
42
 
20
- const metadata = await sharp(screenshotFile).metadata();
21
- const width = metadata.width;
22
- const height = metadata.height;
43
+ const screenshot = await loadImage(screenshotFile);
44
+ const width = screenshot.width;
45
+ const height = screenshot.height;
23
46
 
24
47
  const canvas = createCanvas(width, height);
25
48
  const ctx = canvas.getContext('2d');
26
49
 
50
+ ctx.drawImage(screenshot, 0, 0);
51
+ applyGrayscale(ctx, width, height);
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
+
27
59
  const colors = {
28
60
  box: { fill: 'rgba(220, 160, 40, 0.4)', stroke: '#E6A500' },
29
61
  image: { fill: 'rgba(70, 180, 120, 0.4)', stroke: '#2EBC6F' },
@@ -36,16 +68,23 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
36
68
  }
37
69
 
38
70
  function drawBoundingBox(rect, colorKey, dashed = false) {
39
- const boxWidth = rect.right - rect.left;
40
- 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;
41
80
  const color = colors[colorKey];
42
81
 
43
82
  ctx.fillStyle = color.fill;
44
- ctx.fillRect(rect.left, rect.top, boxWidth, boxHeight);
83
+ ctx.fillRect(adjustedRect.left, adjustedRect.top, boxWidth, boxHeight);
45
84
  ctx.strokeStyle = color.stroke;
46
85
  ctx.lineWidth = 2;
47
86
  ctx.setLineDash(dashed ? [6, 4] : []);
48
- ctx.strokeRect(rect.left, rect.top, boxWidth, boxHeight);
87
+ ctx.strokeRect(adjustedRect.left, adjustedRect.top, boxWidth, boxHeight);
49
88
  ctx.setLineDash([]);
50
89
  }
51
90
 
@@ -70,19 +109,11 @@ async function annotateWireframeFile(wireframeFile, screenshotFile) {
70
109
  }
71
110
  }
72
111
 
73
- const overlayBuffer = canvas.toBuffer('image/png');
74
-
75
112
  const outputDir = path.dirname(screenshotFile);
76
113
  const basename = path.basename(screenshotFile, '.png');
77
114
  const outputPath = path.join(outputDir, `${basename}_wireframe.png`);
78
115
 
79
- await sharp(screenshotFile)
80
- .grayscale()
81
- .composite([{
82
- input: overlayBuffer,
83
- blend: 'over'
84
- }])
85
- .toFile(outputPath);
116
+ fs.writeFileSync(outputPath, canvas.toBuffer('image/png'));
86
117
 
87
118
  console.log(`Annotated wireframe saved to: ${outputPath}`);
88
119
  }
@@ -1,18 +1,27 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { logTimestamp } from './testUtils.js';
4
- import sharp from 'sharp';
4
+ import { loadImage, createCanvas } from 'canvas';
5
5
  import { areWireframesStable } from './wireframeStability.js';
6
6
 
7
+ const FLEXYSNAP_ELEMENT_ID_ATTRIBUTE = 'data-flexysnap-id';
8
+
7
9
  async function getRGBHistogramFromBuffer(buffer) {
8
- const {data, info} = await sharp(buffer)
9
- .raw()
10
- .toBuffer({resolveWithObject: true});
10
+ const image = await loadImage(buffer);
11
+ const canvas = createCanvas(image.width, image.height);
12
+ const ctx = canvas.getContext('2d');
13
+ ctx.drawImage(image, 0, 0);
14
+
15
+ const { data } = ctx.getImageData(0, 0, image.width, image.height);
11
16
 
12
17
  const histogram = new Array(48).fill(0);
13
- const pixelCount = info.width * info.height;
18
+ const pixelCount = image.width * image.height;
19
+
20
+ if (pixelCount === 0) {
21
+ return histogram;
22
+ }
14
23
 
15
- for (let i = 0; i < data.length; i += info.channels) {
24
+ for (let i = 0; i < data.length; i += 4) {
16
25
  histogram[Math.floor(data[i] / 16)]++;
17
26
  histogram[Math.floor(data[i + 1] / 16) + 16]++;
18
27
  histogram[Math.floor(data[i + 2] / 16) + 32]++;
@@ -30,7 +39,7 @@ function delay(milliseconds) {
30
39
  }
31
40
 
32
41
  async function extractWireframe(page, elementGroups) {
33
- const wireframeData = await page.evaluate(async ({elementGroups}) => {
42
+ const wireframeData = await page.evaluate(async ({elementGroups, elementIdAttribute}) => {
34
43
 
35
44
  function createBoundingRect(element) {
36
45
  const rect = element.getBoundingClientRect();
@@ -73,7 +82,8 @@ async function extractWireframe(page, elementGroups) {
73
82
  }
74
83
 
75
84
 
76
- for (const elementGroup of elementGroups) {
85
+ for (let groupIndex = 0; groupIndex < elementGroups.length; groupIndex++) {
86
+ const elementGroup = elementGroups[groupIndex];
77
87
  elementGroup.strictPosition = elementGroup.strictPosition !== false;
78
88
  elementGroup.elements = [];
79
89
 
@@ -152,18 +162,26 @@ async function extractWireframe(page, elementGroups) {
152
162
  }
153
163
  }
154
164
 
155
- elementGroup.elements.push({
165
+ const elementData = {
156
166
  index: index,
157
167
  boundingRect: createBoundingRect(element),
158
168
  texts: texts,
159
169
  type: elementGroup.type
160
- });
170
+ };
171
+
172
+ if (elementGroup.type === 'image') {
173
+ const elementId = `fsnap-g${groupIndex}-e${index}`;
174
+ element.setAttribute(elementIdAttribute, elementId);
175
+ elementData.elementId = elementId;
176
+ }
177
+
178
+ elementGroup.elements.push(elementData);
161
179
  index += 1;
162
180
  }
163
181
  }
164
182
 
165
183
  return elementGroups;
166
- }, {elementGroups});
184
+ }, {elementGroups, elementIdAttribute: FLEXYSNAP_ELEMENT_ID_ATTRIBUTE});
167
185
 
168
186
  const scrollPosition = await page.evaluate(() => ({
169
187
  x: window.scrollX,
@@ -172,8 +190,8 @@ async function extractWireframe(page, elementGroups) {
172
190
 
173
191
  for (const elementGroup of wireframeData) {
174
192
  for (const element of elementGroup.elements) {
175
- if (element.type === 'image') {
176
- const locator = page.locator(elementGroup.selector).nth(element.index);
193
+ if (element.type === 'image' && element.elementId) {
194
+ const locator = page.locator(`[${FLEXYSNAP_ELEMENT_ID_ATTRIBUTE}="${element.elementId}"]`);
177
195
  await locator.waitFor({state: 'visible'});
178
196
 
179
197
  try {
@@ -192,14 +210,28 @@ async function extractWireframe(page, elementGroups) {
192
210
  } catch {
193
211
  element.type = 'box'
194
212
  }
213
+
214
+ delete element.elementId;
195
215
  }
196
216
  }
197
217
  }
198
218
 
199
- await page.evaluate(({scrollPosition}) => {
200
- window.scrollTo(scrollPosition.x, scrollPosition.y);
201
- }, {scrollPosition});
202
- return wireframeData;
219
+ await page.evaluate(({scrollPosition, elementIdAttribute}) => {
220
+ for (const el of document.querySelectorAll(`[${elementIdAttribute}]`)) {
221
+ el.removeAttribute(elementIdAttribute);
222
+ }
223
+ window.scrollTo({top: scrollPosition.y, left: scrollPosition.x, behavior: 'instant'});
224
+ }, {scrollPosition, elementIdAttribute: FLEXYSNAP_ELEMENT_ID_ATTRIBUTE});
225
+
226
+ try {
227
+ await page.waitForFunction((expectedScrollPosition) => {
228
+ return window.scrollX === expectedScrollPosition.x && window.scrollY === expectedScrollPosition.y;
229
+ }, scrollPosition, {timeout: 2000});
230
+ } catch {
231
+ logTimestamp('Scroll position did not settle back to the extraction position.');
232
+ }
233
+
234
+ return {wireframeData, scrollPosition};
203
235
  }
204
236
 
205
237
  function cloneElementGroups(elementGroups) {
@@ -209,17 +241,20 @@ function cloneElementGroups(elementGroups) {
209
241
  async function extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount) {
210
242
  let previousWireframeData = null;
211
243
  let currentWireframeData = null;
244
+ let currentScrollPosition = null;
212
245
 
213
246
  for (let attempt = 0; attempt < maxRetryCount; attempt++) {
214
247
  const extractionStartTime = Date.now();
215
- currentWireframeData = await extractWireframe(page, cloneElementGroups(elementGroups));
248
+ const extractionResult = await extractWireframe(page, cloneElementGroups(elementGroups));
249
+ currentWireframeData = extractionResult.wireframeData;
250
+ currentScrollPosition = extractionResult.scrollPosition;
216
251
  const extractionDuration = Date.now() - extractionStartTime;
217
252
  logTimestamp(`Wireframe candidate captured after ${extractionDuration/1000} seconds.`)
218
253
 
219
254
  if (previousWireframeData !== null &&
220
255
  areWireframesStable(previousWireframeData, currentWireframeData)) {
221
256
  logTimestamp(`Wireframe stabilized after ${attempt + 1} extraction(s)`);
222
- return currentWireframeData;
257
+ return {wireframeData: currentWireframeData, scrollPosition: currentScrollPosition};
223
258
  }
224
259
 
225
260
  previousWireframeData = currentWireframeData;
@@ -232,22 +267,29 @@ async function extractStableWireframe(page, elementGroups, retryDelay, maxRetryC
232
267
  }
233
268
 
234
269
  logTimestamp(`Wireframe did not stabilize within ${maxRetryCount} extraction(s)`);
235
- return currentWireframeData;
270
+ return {wireframeData: currentWireframeData, scrollPosition: currentScrollPosition};
236
271
  }
237
272
 
238
273
  async function expectWireframe(page, elementGroups, configName, outputFile, outputName, retryDelay = 1000, maxRetryCount = 10) {
239
274
  logTimestamp(`Starting wireframe capture for: ${outputFile}`);
240
- const wireframeData = await extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount);
275
+ const { wireframeData, scrollPosition } = await extractStableWireframe(page, elementGroups, retryDelay, maxRetryCount);
241
276
 
242
277
  const userType = process.env.USER_TYPE;
243
278
  const deviceType = process.env.DEVICE_TYPE;
244
279
  const testType = process.env.TEST_TYPE;
245
280
 
281
+ const screenshotScrollPosition = await page.evaluate(() => ({
282
+ x: window.scrollX,
283
+ y: window.scrollY
284
+ }));
285
+
246
286
  const wireframeOutput = {
247
287
  name: outputName,
248
288
  timestamp: new Date().toISOString(),
249
289
  deviceType: process.env.DEVICE_TYPE,
250
290
  userType: process.env.USER_TYPE,
291
+ scrollPosition,
292
+ screenshotScrollPosition,
251
293
  elementGroups: wireframeData
252
294
  };
253
295