flexysnap 0.1.0
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/README.md +175 -0
- package/package.json +55 -0
- package/src/cli.js +22 -0
- package/src/index.js +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# flexysnap
|
|
2
|
+
|
|
3
|
+
Structural wireframe snapshot testing for Playwright — built for e-commerce.
|
|
4
|
+
|
|
5
|
+
Traditional pixel-diffing tools flag every layout shift as a failure, even a 2px nudge from a font render or a shifting badge. Real webshops also have content that changes on purpose: cross-sell blocks, promo banners, stock counters. `flexysnap` takes a different approach. Instead of comparing raw pixels, it extracts a structural wireframe from the page — bounding boxes, text content, and image color histograms — then compares that wireframe against a baseline. You test what actually matters: is the layout intact, is the text correct, did an image visually change — without drowning in false positives from anti-aliasing noise.
|
|
6
|
+
|
|
7
|
+
## Why flexysnap
|
|
8
|
+
|
|
9
|
+
- **Structural diffing** — captures element bounding boxes, text nodes, and image histograms instead of raw pixels, so sub-pixel font rendering and anti-aliasing never fail your suite.
|
|
10
|
+
- **Position tolerance** — element groups can be marked `strictPosition: false` to check size only, ignoring exact placement for content that legitimately moves.
|
|
11
|
+
- **Stability detection** — wireframes are re-captured until the layout settles, so lazy-loaded images, animations, and reflow don't produce flaky baselines.
|
|
12
|
+
- **Built on Playwright** — works with your existing Playwright config, fixtures, and test runner. No new browser automation layer to learn.
|
|
13
|
+
- **Rich HTML reports** — generate an interactive report with baseline/current image sliders, annotated wireframe overlays, and categorized differences (text, image, layout).
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install --save-dev flexysnap
|
|
19
|
+
npx playwright install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Concepts
|
|
23
|
+
|
|
24
|
+
### Element groups
|
|
25
|
+
|
|
26
|
+
You describe the page as a list of **element groups**. Each group targets a CSS selector and declares a type:
|
|
27
|
+
|
|
28
|
+
| Type | What it captures |
|
|
29
|
+
|---------|--------------------------------------------------------------|
|
|
30
|
+
| `box` | Bounding box only (layout/size) |
|
|
31
|
+
| `image` | Bounding box plus an RGB color histogram for visual comparison |
|
|
32
|
+
| `text` | Bounding box plus every visible direct text node inside it |
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const elementGroups = [
|
|
36
|
+
{ selector: '.hero-banner', type: 'image' },
|
|
37
|
+
{ selector: '.product-title', type: 'text' },
|
|
38
|
+
{ selector: '.cross-sell-carousel', type: 'box', strictPosition: false },
|
|
39
|
+
{ selector: '.price', type: 'text', textIgnoreClasses: ['screen-reader-text'] }
|
|
40
|
+
];
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `strictPosition: false` — compare element size only, not exact position.
|
|
44
|
+
- `textIgnoreClasses` — skip text found inside elements carrying these class names.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
### Capturing a wireframe
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { test } from '@playwright/test';
|
|
52
|
+
import { expectWireframe } from 'flexysnap/wireframeUtils.js';
|
|
53
|
+
|
|
54
|
+
const elementGroups = [
|
|
55
|
+
{ selector: '.hero-banner', type: 'image' },
|
|
56
|
+
{ selector: '.product-title', type: 'text' },
|
|
57
|
+
{ selector: '.cross-sell-carousel', type: 'box', strictPosition: false }
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
test('product page wireframe', async ({ page }) => {
|
|
61
|
+
await page.goto('https://example-shop.com/products/example-item');
|
|
62
|
+
|
|
63
|
+
await expectWireframe(
|
|
64
|
+
page,
|
|
65
|
+
elementGroups,
|
|
66
|
+
'product-config', // config name
|
|
67
|
+
'product-page', // output file basename
|
|
68
|
+
'Product Page' // human-readable name
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`expectWireframe` captures a stable wireframe (re-sampling until the layout settles), writes a JSON wireframe and a PNG screenshot into:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
wireframes/test/<TEST_TYPE>/<config>/<DEVICE_TYPE>/<USER_TYPE>/
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
1. **Wireframe extraction** — `flexysnap` walks the DOM in the browser, collecting bounding boxes for each matched element, text nodes for `text` groups, and RGB histograms (via `sharp`) for `image` groups.
|
|
82
|
+
2. **Stability loop** — the wireframe is captured repeatedly until consecutive captures are stable (element counts match, text is unchanged, and total bounding-box area drift stays under 3%). This defeats lazy loading and animation flakiness.
|
|
83
|
+
3. **Baseline comparison** — element groups from the current run are paired with the baseline using nearest bounding-box matching. Each pair is diffed for layout shifts, text mismatches, and histogram (image) differences. Unmatched elements are reported as extra or missing.
|
|
84
|
+
|
|
85
|
+
## Environment variables
|
|
86
|
+
|
|
87
|
+
Wireframe paths are namespaced by environment variables so the same tests can run across devices and user roles:
|
|
88
|
+
|
|
89
|
+
| Variable | Example | Purpose |
|
|
90
|
+
|---------------|--------------|----------------------------------|
|
|
91
|
+
| `TEST_TYPE` | `smoke` | Top-level test grouping |
|
|
92
|
+
| `TEST_CONFIG` | `production` | Configuration name |
|
|
93
|
+
| `DEVICE_TYPE` | `mobile` | Device / viewport identifier |
|
|
94
|
+
| `USER_TYPE` | `guest` | User role identifier |
|
|
95
|
+
|
|
96
|
+
## Comparing against a baseline
|
|
97
|
+
|
|
98
|
+
The comparison suite reads baseline and current wireframes and asserts they match:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
TEST_TYPE=smoke TEST_CONFIG=production DEVICE_TYPE=mobile USER_TYPE=guest \
|
|
102
|
+
npx playwright test compare-wireframes.spec.js
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
It reads from:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
wireframes/baseline/<TEST_TYPE>/<TEST_CONFIG>/<DEVICE_TYPE>/<USER_TYPE>/
|
|
109
|
+
wireframes/test/<TEST_TYPE>/<TEST_CONFIG>/<DEVICE_TYPE>/<USER_TYPE>/
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each matched wireframe is diffed and re-written with annotated `differences`, then every difference is asserted as a soft failure so the whole page is reported at once.
|
|
113
|
+
|
|
114
|
+
## Difference types
|
|
115
|
+
|
|
116
|
+
| Type | Meaning |
|
|
117
|
+
|------------------------|----------------------------------------------------|
|
|
118
|
+
| `layout_shift` | Bounding box moved beyond the position tolerance |
|
|
119
|
+
| `size_mismatch` | Size changed (position-tolerant groups) |
|
|
120
|
+
| `text_mismatch` | Text content differs between baseline and current |
|
|
121
|
+
| `extra_text` | Text present now but not in baseline |
|
|
122
|
+
| `missing_text` | Text present in baseline but not now |
|
|
123
|
+
| `histogram_difference` | Image content changed visually |
|
|
124
|
+
| `extra_element` | Element present now but not in baseline |
|
|
125
|
+
| `missing_element` | Element present in baseline but not now |
|
|
126
|
+
|
|
127
|
+
## Annotating screenshots
|
|
128
|
+
|
|
129
|
+
Overlay the captured wireframe onto its screenshot to visualize what was checked and what differed:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# annotate a single pair
|
|
133
|
+
node annotateWireframe.mjs wireframe.json screenshot.png
|
|
134
|
+
|
|
135
|
+
# annotate every matching .json/.png pair in a folder
|
|
136
|
+
node annotateWireframe.mjs ./wireframes/test/smoke/production/mobile/guest
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Boxes are color-coded: text (blue), image (green), box (amber), and error (red) for any element carrying differences. Dashed borders indicate position-tolerant groups. Output is written as `<name>_wireframe.png`.
|
|
140
|
+
|
|
141
|
+
## Generating an HTML report
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
node generate-report.js <baseline-folder> <current-folder> [output-file]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
node generate-report.js \
|
|
151
|
+
wireframes/baseline/smoke/production/mobile/guest \
|
|
152
|
+
wireframes/test/smoke/production/mobile/guest \
|
|
153
|
+
report.html
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The report includes:
|
|
157
|
+
|
|
158
|
+
- Summary statistics (elements checked, total / text / image / layout differences) with click-to-jump navigation.
|
|
159
|
+
- A baseline↔current comparison slider (via cocoen) when both images are available.
|
|
160
|
+
- A global toggle between **original** and **annotated** screenshots.
|
|
161
|
+
- A per-wireframe list of detected differences, with empty sections auto-collapsed.
|
|
162
|
+
|
|
163
|
+
## Roadmap
|
|
164
|
+
|
|
165
|
+
- [ ] Region auto-detection for common e-commerce patterns (cart, checkout, PDP)
|
|
166
|
+
- [ ] GitHub Actions annotation integration
|
|
167
|
+
- [ ] Plugin-change tracking integrated into the HTML report
|
|
168
|
+
|
|
169
|
+
## Contributing
|
|
170
|
+
|
|
171
|
+
Issues and pull requests are welcome. Please open an issue before submitting large changes so we can discuss the approach first.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flexysnap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Flexible visual snapshot testing for Playwright, built for e-commerce.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"playwright",
|
|
7
|
+
"visual-testing",
|
|
8
|
+
"snapshot-testing",
|
|
9
|
+
"screenshot-testing",
|
|
10
|
+
"e-commerce",
|
|
11
|
+
"regression-testing",
|
|
12
|
+
"qa",
|
|
13
|
+
"testing"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://flexysnap.com",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/flexysnap/flexysnap/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/flexysnap/flexysnap.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Gabor Angyal",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./src/index.js",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.js"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"flexysnap": "./src/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "playwright test",
|
|
43
|
+
"lint": "eslint src"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@playwright/test": "^1.61.1",
|
|
47
|
+
"canvas": "^3.2.3",
|
|
48
|
+
"glob": "^13.0.6",
|
|
49
|
+
"playwright": "^1.61.1",
|
|
50
|
+
"sharp": "^0.35.3"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"eslint": "^9.9.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* flexysnap CLI — placeholder implementation.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx flexysnap update Regenerate baseline snapshots
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const [, , command] = process.argv;
|
|
11
|
+
|
|
12
|
+
switch (command) {
|
|
13
|
+
case 'update':
|
|
14
|
+
console.log('flexysnap: baseline update not implemented yet.');
|
|
15
|
+
break;
|
|
16
|
+
default:
|
|
17
|
+
console.log('flexysnap');
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log('Usage:');
|
|
20
|
+
console.log(' flexysnap update Regenerate baseline snapshots');
|
|
21
|
+
break;
|
|
22
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flexysnap — flexible visual snapshot testing for Playwright.
|
|
3
|
+
*
|
|
4
|
+
* This is a placeholder entry point. Replace with the real
|
|
5
|
+
* snapshot-comparison implementation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Capture and compare a snapshot for the given Playwright page.
|
|
10
|
+
*
|
|
11
|
+
* @param {import('@playwright/test').Page} page
|
|
12
|
+
* @param {string} name - snapshot identifier, used for the stored baseline file
|
|
13
|
+
* @param {object} [options]
|
|
14
|
+
* @param {number} [options.tolerance=0.02] - allowed pixel-drift ratio (0-1)
|
|
15
|
+
* @param {Record<string, 'strict'|'presence'|'ignore'>} [options.regions] - per-selector comparison policy
|
|
16
|
+
*/
|
|
17
|
+
export async function expectSnapshot(page, name, options = {}) {
|
|
18
|
+
const { tolerance = 0.02, regions = {} } = options;
|
|
19
|
+
|
|
20
|
+
// TODO: implement capture + diff logic (sharp/canvas-based comparison).
|
|
21
|
+
throw new Error(
|
|
22
|
+
`expectSnapshot("${name}") is not implemented yet. tolerance=${tolerance}, regions=${JSON.stringify(regions)}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default { expectSnapshot };
|