@testivai/witness-playwright 1.4.0 → 1.5.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/dist/capture/element-map.d.ts +37 -0
- package/dist/capture/element-map.js +123 -0
- package/dist/snapshot.js +14 -0
- package/package.json +2 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element-map capture — the layout + computed-style digest that powers
|
|
3
|
+
* element attribution, shift classification, and the style fingerprint
|
|
4
|
+
* on the comparison side (@testivai/witness).
|
|
5
|
+
*
|
|
6
|
+
* `collectElementMap` is a SELF-CONTAINED function (no imports, no
|
|
7
|
+
* closures) so it can be serialized into the page with
|
|
8
|
+
* `(${collectElementMap.toString()})(document, window, max)` — and unit
|
|
9
|
+
* tested in Node against a duck-typed DOM. Keep it dependency-free.
|
|
10
|
+
*/
|
|
11
|
+
export interface CollectedElement {
|
|
12
|
+
path: string;
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
styleHash: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Walk the document and produce [{path, rect, styleHash}] in document
|
|
21
|
+
* coordinates (DPR-scaled to match full-page screenshot pixels).
|
|
22
|
+
*
|
|
23
|
+
* - `path` is deterministic: tag + first class + :nth-of-type when the
|
|
24
|
+
* element has same-tag element siblings.
|
|
25
|
+
* - Elements smaller than 4×4 px and invisible elements are skipped.
|
|
26
|
+
* - The walk is depth-first and capped at `maxElements`.
|
|
27
|
+
* - `styleHash` digests a fixed, documented list of computed properties
|
|
28
|
+
* (FNV-1a 32-bit, hex) — deterministic for identical rendering.
|
|
29
|
+
*/
|
|
30
|
+
export declare function collectElementMap(doc: Document, win: Window & typeof globalThis, maxElements: number): CollectedElement[];
|
|
31
|
+
/** Default element cap — bounds capture time and elements.json size. */
|
|
32
|
+
export declare const DEFAULT_MAX_ELEMENTS = 3000;
|
|
33
|
+
/**
|
|
34
|
+
* The page-side expression evaluated by the adapter. Serializes the
|
|
35
|
+
* collector so page code and unit-tested code are the same function.
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildElementMapExpression(maxElements?: number): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Element-map capture — the layout + computed-style digest that powers
|
|
4
|
+
* element attribution, shift classification, and the style fingerprint
|
|
5
|
+
* on the comparison side (@testivai/witness).
|
|
6
|
+
*
|
|
7
|
+
* `collectElementMap` is a SELF-CONTAINED function (no imports, no
|
|
8
|
+
* closures) so it can be serialized into the page with
|
|
9
|
+
* `(${collectElementMap.toString()})(document, window, max)` — and unit
|
|
10
|
+
* tested in Node against a duck-typed DOM. Keep it dependency-free.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.DEFAULT_MAX_ELEMENTS = void 0;
|
|
14
|
+
exports.collectElementMap = collectElementMap;
|
|
15
|
+
exports.buildElementMapExpression = buildElementMapExpression;
|
|
16
|
+
/**
|
|
17
|
+
* Walk the document and produce [{path, rect, styleHash}] in document
|
|
18
|
+
* coordinates (DPR-scaled to match full-page screenshot pixels).
|
|
19
|
+
*
|
|
20
|
+
* - `path` is deterministic: tag + first class + :nth-of-type when the
|
|
21
|
+
* element has same-tag element siblings.
|
|
22
|
+
* - Elements smaller than 4×4 px and invisible elements are skipped.
|
|
23
|
+
* - The walk is depth-first and capped at `maxElements`.
|
|
24
|
+
* - `styleHash` digests a fixed, documented list of computed properties
|
|
25
|
+
* (FNV-1a 32-bit, hex) — deterministic for identical rendering.
|
|
26
|
+
*/
|
|
27
|
+
function collectElementMap(doc, win, maxElements) {
|
|
28
|
+
var STYLE_PROPS = [
|
|
29
|
+
'color', 'background-color', 'background-image',
|
|
30
|
+
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
|
|
31
|
+
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
|
32
|
+
'border-top-style', 'border-bottom-style', 'border-radius',
|
|
33
|
+
'font-family', 'font-size', 'font-weight', 'font-style', 'line-height',
|
|
34
|
+
'text-align', 'text-transform', 'letter-spacing',
|
|
35
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
36
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
37
|
+
'display', 'opacity', 'visibility', 'box-shadow',
|
|
38
|
+
];
|
|
39
|
+
function fnv1a(str) {
|
|
40
|
+
var h = 0x811c9dc5;
|
|
41
|
+
for (var i = 0; i < str.length; i++) {
|
|
42
|
+
h ^= str.charCodeAt(i);
|
|
43
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
44
|
+
}
|
|
45
|
+
return ('0000000' + h.toString(16)).slice(-8);
|
|
46
|
+
}
|
|
47
|
+
function segment(el) {
|
|
48
|
+
var tag = el.tagName.toLowerCase();
|
|
49
|
+
var cls = '';
|
|
50
|
+
if (el.classList && el.classList.length > 0) {
|
|
51
|
+
cls = '.' + el.classList[0];
|
|
52
|
+
}
|
|
53
|
+
var parent = el.parentElement;
|
|
54
|
+
if (!parent)
|
|
55
|
+
return tag + cls;
|
|
56
|
+
var sameTag = 0;
|
|
57
|
+
var index = 0;
|
|
58
|
+
for (var i = 0; i < parent.children.length; i++) {
|
|
59
|
+
var sib = parent.children[i];
|
|
60
|
+
if (sib.tagName === el.tagName) {
|
|
61
|
+
sameTag++;
|
|
62
|
+
if (sib === el)
|
|
63
|
+
index = sameTag;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return sameTag > 1 ? tag + cls + ':nth-of-type(' + index + ')' : tag + cls;
|
|
67
|
+
}
|
|
68
|
+
function pathOf(el, stopAt) {
|
|
69
|
+
var parts = [];
|
|
70
|
+
var node = el;
|
|
71
|
+
while (node && node !== stopAt.parentElement) {
|
|
72
|
+
parts.unshift(segment(node));
|
|
73
|
+
node = node.parentElement;
|
|
74
|
+
}
|
|
75
|
+
return parts.join(' > ');
|
|
76
|
+
}
|
|
77
|
+
var dpr = win.devicePixelRatio || 1;
|
|
78
|
+
var scrollX = win.scrollX || 0;
|
|
79
|
+
var scrollY = win.scrollY || 0;
|
|
80
|
+
var out = [];
|
|
81
|
+
var body = doc.body;
|
|
82
|
+
if (!body)
|
|
83
|
+
return out;
|
|
84
|
+
var stack = [body];
|
|
85
|
+
while (stack.length > 0 && out.length < maxElements) {
|
|
86
|
+
var el = stack.pop();
|
|
87
|
+
var rect = el.getBoundingClientRect();
|
|
88
|
+
if (rect.width >= 4 && rect.height >= 4) {
|
|
89
|
+
var styleParts = [];
|
|
90
|
+
try {
|
|
91
|
+
var cs = win.getComputedStyle(el);
|
|
92
|
+
for (var p = 0; p < STYLE_PROPS.length; p++) {
|
|
93
|
+
styleParts.push(STYLE_PROPS[p] + ':' + cs.getPropertyValue(STYLE_PROPS[p]));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
// styleHash stays a digest of the empty string — still deterministic
|
|
98
|
+
}
|
|
99
|
+
out.push({
|
|
100
|
+
path: pathOf(el, body),
|
|
101
|
+
x: Math.round((rect.x + scrollX) * dpr),
|
|
102
|
+
y: Math.round((rect.y + scrollY) * dpr),
|
|
103
|
+
width: Math.round(rect.width * dpr),
|
|
104
|
+
height: Math.round(rect.height * dpr),
|
|
105
|
+
styleHash: fnv1a(styleParts.join(';')),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Push children in reverse so the walk stays document-ordered
|
|
109
|
+
for (var c = el.children.length - 1; c >= 0; c--) {
|
|
110
|
+
stack.push(el.children[c]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** Default element cap — bounds capture time and elements.json size. */
|
|
116
|
+
exports.DEFAULT_MAX_ELEMENTS = 3000;
|
|
117
|
+
/**
|
|
118
|
+
* The page-side expression evaluated by the adapter. Serializes the
|
|
119
|
+
* collector so page code and unit-tested code are the same function.
|
|
120
|
+
*/
|
|
121
|
+
function buildElementMapExpression(maxElements = exports.DEFAULT_MAX_ELEMENTS) {
|
|
122
|
+
return `(${collectElementMap.toString()})(document, window, ${maxElements})`;
|
|
123
|
+
}
|
package/dist/snapshot.js
CHANGED
|
@@ -44,6 +44,7 @@ const url_1 = require("url");
|
|
|
44
44
|
const sharp_1 = __importDefault(require("sharp"));
|
|
45
45
|
const loader_1 = require("./config/loader");
|
|
46
46
|
const ignore_selectors_1 = require("./config/ignore-selectors");
|
|
47
|
+
const element_map_1 = require("./capture/element-map");
|
|
47
48
|
const stabilize_1 = require("./config/stabilize");
|
|
48
49
|
/**
|
|
49
50
|
* Generates a safe filename from a URL.
|
|
@@ -463,6 +464,19 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
463
464
|
catch {
|
|
464
465
|
// metadata is an enhancement; the screenshot path never depends on it
|
|
465
466
|
}
|
|
467
|
+
// Element map: layout + computed-style digest for every visible
|
|
468
|
+
// element, powering attribution ("WHICH element changed"), exact
|
|
469
|
+
// shift classification, and the style fingerprint on the compare
|
|
470
|
+
// side. Best-effort — the screenshot path never depends on it.
|
|
471
|
+
try {
|
|
472
|
+
const elementMap = await page.evaluate((0, element_map_1.buildElementMapExpression)());
|
|
473
|
+
if (Array.isArray(elementMap) && elementMap.length > 0) {
|
|
474
|
+
await fs.writeJson(path.join(localSnapshotDir, 'elements.json'), elementMap);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// missing elements.json only disables attribution for this capture
|
|
479
|
+
}
|
|
466
480
|
}
|
|
467
481
|
// 2. Dump page structure (HTML) - skip in local mode
|
|
468
482
|
// @renamed: domPath → structurePath (IP protection)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-playwright",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Playwright sensor for Testivai Visual Regression Test system",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"sharp": "^0.34.5",
|
|
49
49
|
"simple-git": "^3.21.0",
|
|
50
50
|
"@testivai/common": "0.2.2",
|
|
51
|
-
"@testivai/witness": "1.
|
|
51
|
+
"@testivai/witness": "1.6.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@playwright/test": "^1.60.0",
|