@testivai/witness-webdriverio 0.3.3 → 0.4.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/README.md CHANGED
@@ -95,29 +95,40 @@ 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
- When pixels differ but the page DOM is structurally identical, the report flags the change as "likely render noise" (anti-aliasing, font hinting, sub-pixel layout). When the DOM is also different, the report shows added / removed / attribute-change counts so you can decide whether the change is intentional. Like the Playwright adapter, it writes `dom.html` alongside the screenshot.
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
- Unlike the Playwright adapter, this adapter does not capture an element map yet, so the computed-style check reports `styleCheck: "unavailable"`: a stylesheet-only change (identical DOM, different styles) cannot be told apart from render noise here, and element attribution and page-shift detection are not available.
102
+ - When pixels differ but the DOM is structurally identical, the computed styles were compared and match, and the diff is small and scattered, the report labels the change "likely render noise" (anti-aliasing, font hinting, sub-pixel layout): likely, not proven. When the diff is concentrated in one area or over 1% of pixels, it does not call it noise: that is a bad capture or a change the style check does not cover (size, overflow, transform, images).
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 is identical but the computed styles could not be compared (no element map on one side: `skipElementMap`, a blocked collector script, or a baseline captured without one), it says "styles not compared": could be render noise or a stylesheet-only change. It does not call that noise, and `noiseAutoPass` never passes it.
105
+ - When the DOM also differs, it shows added / removed / attribute-change counts so you can decide whether the change is intentional.
103
106
 
104
- DOM capture happens automatically. To skip it for a single snapshot:
107
+ These are labels on a `changed` snapshot. Nothing auto-passes unless you configure a pass criterion in `.testivai/config.json` (`maxDiffPercent`, `maxDiffPixels`, `noiseAutoPass`, `shiftTolerance`).
108
+
109
+ The element map also powers region attribution (which selectors a diff region covers) and page-shift detection.
110
+
111
+ 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.
112
+
113
+ Both captures happen automatically and are best-effort: if either fails, the pixel diff still runs. To skip them for a single snapshot:
105
114
 
106
115
  ```ts
107
- await testivai.witness(browser, 'no-dom', { skipDom: true });
116
+ await testivai.witness(browser, 'no-dom', { skipDom: true, skipElementMap: true });
108
117
  ```
109
118
 
110
119
  ## API
111
120
 
112
121
  ### `testivai.witness(browser, name, options?)`
113
122
 
114
- Captures a screenshot + DOM and writes them as a temp snapshot.
123
+ Captures a viewport screenshot, the DOM and the element map, and writes them as a temp snapshot.
115
124
 
116
125
  | Param | Type | Description |
117
126
  |---|---|---|
118
127
  | `browser` | WebdriverIO browser | Must expose `takeScreenshot()` and (for DOM) `execute()`. |
119
128
  | `name` | string | Snapshot name. Becomes `.testivai/temp/<name>/` and the key in the report. |
120
129
  | `options.skipDom` | boolean | Skip DOM capture for this snapshot. |
130
+ | `options.skipElementMap` | boolean | Skip the element map for this snapshot (no style check, region attribution or page-shift detection). |
131
+ | `options.maxElements` | number | Cap on elements collected for the element map. Default: 3000. |
121
132
  | `options.ignoreSelectors` | string[] | Elements hidden (`visibility: hidden`) for this capture; merged with the global `ignoreSelectors` from `.testivai/config.json`. |
122
133
  | `options.stabilize` | boolean | Override capture stabilization (animations frozen, caret hidden, fonts awaited). Default: `true` (or the global `stabilize` setting). |
123
134
 
@@ -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 a full-page screenshot via `browser.takeScreenshot()` and the
10
- * page DOM via `browser.execute(() => document.documentElement.outerHTML)`,
11
- * then writes both into `.testivai/temp/<name>/` using the same
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 a full-page screenshot via `browser.takeScreenshot()` and the
11
- * page DOM via `browser.execute(() => document.documentElement.outerHTML)`,
12
- * then writes both into `.testivai/temp/<name>/` using the same
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. Write to .testivai/temp/<name>/
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.3",
3
+ "version": "0.4.1",
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,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@testivai/witness": "2.1.0"
57
+ "@testivai/witness": "2.2.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/jest": "^30.0.0",