@testivai/witness-webdriverio 0.3.1 → 0.4.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 +15 -5
- package/dist/element-map.d.ts +25 -0
- package/dist/element-map.js +39 -0
- package/dist/types.d.ts +8 -0
- package/dist/witness.d.ts +3 -3
- package/dist/witness.js +69 -4
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -95,27 +95,37 @@ npx testivai approve --all
|
|
|
95
95
|
|
|
96
96
|
Approve writes the temp capture over the baseline and backs the previous baseline up to `.testivai/baselines/<name>/.previous/`. `npx testivai approve --undo "homepage"` reverts.
|
|
97
97
|
|
|
98
|
-
## DOM noise hint
|
|
98
|
+
## DOM noise hint and style check
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Alongside the screenshot the adapter writes `dom.html` and `elements.json`, the element map: each element's box and a digest of its computed styles, collected by the same function the Playwright and Selenium adapters use.
|
|
101
101
|
|
|
102
|
-
DOM
|
|
102
|
+
- When pixels differ but the DOM is structurally identical and no computed style changed, the report flags the change as "likely render noise" (anti-aliasing, font hinting, sub-pixel layout).
|
|
103
|
+
- When the DOM is identical but computed styles differ, it reports a style-only change, which is real, and names the restyled elements.
|
|
104
|
+
- When the DOM also differs, it shows added / removed / attribute-change counts so you can decide whether the change is intentional.
|
|
105
|
+
|
|
106
|
+
The element map also powers region attribution (which selectors a diff region covers) and page-shift detection.
|
|
107
|
+
|
|
108
|
+
WebDriver's screenshot is the **viewport**, not the full page, so the element map keeps only the elements inside the captured viewport, in screenshot coordinates. Size the browser window to what you want compared, and capture at the same scroll position each run.
|
|
109
|
+
|
|
110
|
+
Both captures happen automatically and are best-effort: if either fails, the pixel diff still runs. To skip them for a single snapshot:
|
|
103
111
|
|
|
104
112
|
```ts
|
|
105
|
-
await testivai.witness(browser, 'no-dom', { skipDom: true });
|
|
113
|
+
await testivai.witness(browser, 'no-dom', { skipDom: true, skipElementMap: true });
|
|
106
114
|
```
|
|
107
115
|
|
|
108
116
|
## API
|
|
109
117
|
|
|
110
118
|
### `testivai.witness(browser, name, options?)`
|
|
111
119
|
|
|
112
|
-
Captures a screenshot
|
|
120
|
+
Captures a viewport screenshot, the DOM and the element map, and writes them as a temp snapshot.
|
|
113
121
|
|
|
114
122
|
| Param | Type | Description |
|
|
115
123
|
|---|---|---|
|
|
116
124
|
| `browser` | WebdriverIO browser | Must expose `takeScreenshot()` and (for DOM) `execute()`. |
|
|
117
125
|
| `name` | string | Snapshot name. Becomes `.testivai/temp/<name>/` and the key in the report. |
|
|
118
126
|
| `options.skipDom` | boolean | Skip DOM capture for this snapshot. |
|
|
127
|
+
| `options.skipElementMap` | boolean | Skip the element map for this snapshot (no style check, region attribution or page-shift detection). |
|
|
128
|
+
| `options.maxElements` | number | Cap on elements collected for the element map. Default: 3000. |
|
|
119
129
|
| `options.ignoreSelectors` | string[] | Elements hidden (`visibility: hidden`) for this capture; merged with the global `ignoreSelectors` from `.testivai/config.json`. |
|
|
120
130
|
| `options.stabilize` | boolean | Override capture stabilization (animations frozen, caret hidden, fonts awaited). Default: `true` (or the global `stabilize` setting). |
|
|
121
131
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CollectedElement } from '@testivai/witness';
|
|
2
|
+
/** What the page reports: the shared collector's map plus where the viewport was. */
|
|
3
|
+
export interface PageElementMap {
|
|
4
|
+
/** From the shared collector: document coordinates in device pixels. */
|
|
5
|
+
elements: CollectedElement[];
|
|
6
|
+
scrollX: number;
|
|
7
|
+
scrollY: number;
|
|
8
|
+
viewportWidth: number;
|
|
9
|
+
viewportHeight: number;
|
|
10
|
+
dpr: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Page-side script: the same collector the Playwright and Selenium adapters
|
|
14
|
+
* run (so all lanes produce identical maps), plus the viewport it was taken
|
|
15
|
+
* in. WebdriverIO string scripts need an explicit `return`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function elementMapScript(maxElements: number, ignoreSelectors: string[]): string;
|
|
18
|
+
export declare function isPageElementMap(value: unknown): value is PageElementMap;
|
|
19
|
+
/**
|
|
20
|
+
* WebDriver's Take Screenshot captures the viewport, while the collector
|
|
21
|
+
* reports document coordinates. Keep only the elements inside the captured
|
|
22
|
+
* viewport, moved into screenshot coordinates, so the map describes the
|
|
23
|
+
* pixels that were actually compared.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toViewportElementMap(page: PageElementMap): CollectedElement[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.elementMapScript = elementMapScript;
|
|
4
|
+
exports.isPageElementMap = isPageElementMap;
|
|
5
|
+
exports.toViewportElementMap = toViewportElementMap;
|
|
6
|
+
const witness_1 = require("@testivai/witness");
|
|
7
|
+
/**
|
|
8
|
+
* Page-side script: the same collector the Playwright and Selenium adapters
|
|
9
|
+
* run (so all lanes produce identical maps), plus the viewport it was taken
|
|
10
|
+
* in. WebdriverIO string scripts need an explicit `return`.
|
|
11
|
+
*/
|
|
12
|
+
function elementMapScript(maxElements, ignoreSelectors) {
|
|
13
|
+
return (`return { elements: ${(0, witness_1.buildElementMapExpression)(maxElements, ignoreSelectors)}, ` +
|
|
14
|
+
'scrollX: window.scrollX || 0, scrollY: window.scrollY || 0, ' +
|
|
15
|
+
'viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, ' +
|
|
16
|
+
'dpr: window.devicePixelRatio || 1 };');
|
|
17
|
+
}
|
|
18
|
+
function isPageElementMap(value) {
|
|
19
|
+
if (!value || typeof value !== 'object')
|
|
20
|
+
return false;
|
|
21
|
+
const v = value;
|
|
22
|
+
return (Array.isArray(v.elements) &&
|
|
23
|
+
['scrollX', 'scrollY', 'viewportWidth', 'viewportHeight', 'dpr'].every((k) => typeof v[k] === 'number'));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* WebDriver's Take Screenshot captures the viewport, while the collector
|
|
27
|
+
* reports document coordinates. Keep only the elements inside the captured
|
|
28
|
+
* viewport, moved into screenshot coordinates, so the map describes the
|
|
29
|
+
* pixels that were actually compared.
|
|
30
|
+
*/
|
|
31
|
+
function toViewportElementMap(page) {
|
|
32
|
+
const offsetX = Math.round(page.scrollX * page.dpr);
|
|
33
|
+
const offsetY = Math.round(page.scrollY * page.dpr);
|
|
34
|
+
const width = Math.round(page.viewportWidth * page.dpr);
|
|
35
|
+
const height = Math.round(page.viewportHeight * page.dpr);
|
|
36
|
+
return page.elements
|
|
37
|
+
.map((e) => ({ ...e, x: e.x - offsetX, y: e.y - offsetY }))
|
|
38
|
+
.filter((e) => e.x < width && e.x + e.width > 0 && e.y < height && e.y + e.height > 0);
|
|
39
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -29,6 +29,14 @@ export interface WitnessOptions {
|
|
|
29
29
|
* known to be expensive or unstable.
|
|
30
30
|
*/
|
|
31
31
|
skipDom?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Skip the element map (each element's box and a digest of its computed
|
|
34
|
+
* styles). Without it the report falls back to the pixel and DOM layers:
|
|
35
|
+
* no style check, region attribution or page-shift detection.
|
|
36
|
+
*/
|
|
37
|
+
skipElementMap?: boolean;
|
|
38
|
+
/** Cap on elements collected for the element map. Default: 3000. */
|
|
39
|
+
maxElements?: number;
|
|
32
40
|
/**
|
|
33
41
|
* CSS selectors for elements to hide (`visibility: hidden`) for the
|
|
34
42
|
* duration of the capture, so dynamic content (timestamps, ads, live
|
package/dist/witness.d.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* import { testivai } from '@testivai/witness-webdriverio';
|
|
7
7
|
* await testivai.witness(browser, 'homepage');
|
|
8
8
|
*
|
|
9
|
-
* Captures
|
|
10
|
-
* page DOM
|
|
11
|
-
* then writes
|
|
9
|
+
* Captures the viewport screenshot via `browser.takeScreenshot()` (WebDriver's
|
|
10
|
+
* Take Screenshot is viewport-sized), the page DOM, and the element map via
|
|
11
|
+
* `browser.execute(...)`, then writes them into `.testivai/temp/<name>/` using the same
|
|
12
12
|
* BaselineStore layout as @testivai/witness-playwright. The TestivaiService
|
|
13
13
|
* (registered in wdio.conf.ts) runs `compareAll` + `generateReport` after
|
|
14
14
|
* all tests finish.
|
package/dist/witness.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* import { testivai } from '@testivai/witness-webdriverio';
|
|
8
8
|
* await testivai.witness(browser, 'homepage');
|
|
9
9
|
*
|
|
10
|
-
* Captures
|
|
11
|
-
* page DOM
|
|
12
|
-
* then writes
|
|
10
|
+
* Captures the viewport screenshot via `browser.takeScreenshot()` (WebDriver's
|
|
11
|
+
* Take Screenshot is viewport-sized), the page DOM, and the element map via
|
|
12
|
+
* `browser.execute(...)`, then writes them into `.testivai/temp/<name>/` using the same
|
|
13
13
|
* BaselineStore layout as @testivai/witness-playwright. The TestivaiService
|
|
14
14
|
* (registered in wdio.conf.ts) runs `compareAll` + `generateReport` after
|
|
15
15
|
* all tests finish.
|
|
@@ -19,9 +19,45 @@
|
|
|
19
19
|
* point of the per-framework adapter — using the framework's stable public
|
|
20
20
|
* API instead of fighting Chrome-launch coordination.
|
|
21
21
|
*/
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
22
55
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
56
|
exports.witness = witness;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
24
59
|
const witness_1 = require("@testivai/witness");
|
|
60
|
+
const element_map_1 = require("./element-map");
|
|
25
61
|
/** id of the style element injected for the duration of a capture */
|
|
26
62
|
const STYLE_ID = '__testivai_capture_css__';
|
|
27
63
|
/**
|
|
@@ -157,7 +193,36 @@ async function witness(browser, name, options = {}) {
|
|
|
157
193
|
// is unavailable for this snapshot; pixel diff still works.
|
|
158
194
|
}
|
|
159
195
|
}
|
|
160
|
-
// 3.
|
|
196
|
+
// 3. Capture the element map (best-effort — same contract as the DOM).
|
|
197
|
+
// It powers the style check, region attribution and page-shift
|
|
198
|
+
// detection on the compare side. The screenshot is the viewport, so the
|
|
199
|
+
// map keeps only the elements inside it, in screenshot coordinates.
|
|
200
|
+
let elementMap;
|
|
201
|
+
if (!options.skipElementMap && typeof browser.execute === 'function') {
|
|
202
|
+
try {
|
|
203
|
+
const result = await browser.execute((0, element_map_1.elementMapScript)(options.maxElements ?? witness_1.DEFAULT_MAX_ELEMENTS, ignoreSelectors));
|
|
204
|
+
if ((0, element_map_1.isPageElementMap)(result)) {
|
|
205
|
+
const inView = (0, element_map_1.toViewportElementMap)(result);
|
|
206
|
+
if (inView.length > 0)
|
|
207
|
+
elementMap = inView;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Suppressed by design, exactly like DOM capture: without a map the
|
|
212
|
+
// report falls back to the pixel and DOM layers instead of failing.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// 4. Write to .testivai/temp/<name>/
|
|
161
216
|
const store = new witness_1.BaselineStore(process.cwd());
|
|
162
217
|
store.writeTemp(name, screenshot, dom);
|
|
218
|
+
if (elementMap) {
|
|
219
|
+
try {
|
|
220
|
+
const tempDir = path.join(process.cwd(), '.testivai', 'temp', name);
|
|
221
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
222
|
+
fs.writeFileSync(path.join(tempDir, 'elements.json'), JSON.stringify(elementMap));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// A map we cannot persist is not worth failing a capture over.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
163
228
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-webdriverio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "WebdriverIO adapter for TestivAI Witness — local-first visual regression with pixel + DOM comparison",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -54,15 +54,15 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@testivai/witness": "2.
|
|
57
|
+
"@testivai/witness": "2.1.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/jest": "^30.0.0",
|
|
61
61
|
"@types/node": "^20.0.0",
|
|
62
|
-
"jest": "^30.
|
|
62
|
+
"jest": "^30.5.1",
|
|
63
63
|
"ts-jest": "^29.4.12",
|
|
64
64
|
"typescript": "^6.0.3",
|
|
65
|
-
"webdriverio": "^9.
|
|
65
|
+
"webdriverio": "^9.31.7"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc",
|