@testivai/witness-selenium 0.1.10 → 0.3.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/dist/types.d.ts CHANGED
@@ -38,6 +38,24 @@ export interface WitnessOptions {
38
38
  * known to be expensive or unstable.
39
39
  */
40
40
  skipDom?: boolean;
41
+ /**
42
+ * Skip element-map capture. The map powers region→selector attribution,
43
+ * shift classification, and the computed-style fingerprint; skipping it
44
+ * leaves the pixel and DOM layers intact.
45
+ */
46
+ skipElementMap?: boolean;
47
+ /**
48
+ * Milliseconds the DOM must be free of mutations before capturing
49
+ * (default 150). Raise it for pages that stream content in.
50
+ */
51
+ settleQuietMs?: number;
52
+ /** Ceiling on waiting for the page to settle (default 5000ms). */
53
+ settleTimeoutMs?: number;
54
+ /**
55
+ * Cap on elements walked for the map (default 3000). Lower it on very
56
+ * large pages where the single executeScript round trip is costly.
57
+ */
58
+ maxElements?: number;
41
59
  /**
42
60
  * CSS selectors for elements to hide (`visibility: hidden`) for the
43
61
  * duration of the capture, so dynamic content (timestamps, ads, live
package/dist/witness.js CHANGED
@@ -20,9 +20,44 @@
20
20
  * capture is viewport-only (keep the window sized to what you want
21
21
  * compared; baselines are consistent run-to-run either way)
22
22
  */
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
23
56
  Object.defineProperty(exports, "__esModule", { value: true });
24
57
  exports.witness = witness;
25
58
  const witness_1 = require("@testivai/witness");
59
+ const fs = __importStar(require("fs"));
60
+ const path = __importStar(require("path"));
26
61
  /** id of the style element injected for the duration of a capture */
27
62
  const STYLE_ID = '__testivai_capture_css__';
28
63
  /**
@@ -61,6 +96,47 @@ async function removeCaptureCss(driver) {
61
96
  * Wait (bounded at 10s) for web fonts to finish loading so the capture
62
97
  * never shows a fallback font — a fallback-font capture diffs 30%+.
63
98
  */
99
+ /**
100
+ * Wait until the page has stopped changing: document complete, images finished,
101
+ * fonts loaded, and no DOM mutations for `quietMs`.
102
+ *
103
+ * Deliberately NOT network idle — Playwright's own docs mark that DISCOURAGED
104
+ * for testing, and it is the wrong signal anyway: a page with analytics beacons
105
+ * never goes quiet, while a network-idle page can still be animating.
106
+ *
107
+ * Always bounded. A page that never settles yields a capture with a debug note
108
+ * rather than a hung suite.
109
+ */
110
+ async function waitForSettled(driver, quietMs, timeoutMs) {
111
+ const expr = (0, witness_1.buildSettleProbeExpression)(quietMs);
112
+ const deadline = Date.now() + timeoutMs;
113
+ for (;;) {
114
+ try {
115
+ const state = await driver.executeScript(`return ${expr}`);
116
+ // A driver that cannot evaluate the probe returns nothing. Polling that
117
+ // to the timeout would add the full wait to EVERY capture, so treat an
118
+ // unusable answer as "cannot probe" and proceed immediately.
119
+ if (!state || typeof state.settled !== 'boolean')
120
+ return;
121
+ if (state.settled)
122
+ return;
123
+ if (Date.now() >= deadline)
124
+ return;
125
+ }
126
+ catch {
127
+ return; // probe unavailable — never block the capture
128
+ }
129
+ await new Promise((resolve) => setTimeout(resolve, 50));
130
+ }
131
+ }
132
+ async function stopSettleObserver(driver) {
133
+ try {
134
+ await driver.executeScript(witness_1.SETTLE_STOP_EXPRESSION);
135
+ }
136
+ catch {
137
+ // best-effort cleanup
138
+ }
139
+ }
64
140
  async function waitForFonts(driver) {
65
141
  const deadline = Date.now() + 10000;
66
142
  for (;;) {
@@ -138,8 +214,10 @@ async function witness(driver, name, options = {}) {
138
214
  let injected = false;
139
215
  if (cssParts.length > 0 && typeof driver.executeScript === 'function') {
140
216
  injected = await injectCaptureCss(driver, cssParts.join('\n'));
141
- if (stabilize)
217
+ if (stabilize) {
142
218
  await waitForFonts(driver);
219
+ await waitForSettled(driver, options.settleQuietMs ?? witness_1.DEFAULT_QUIET_MS, options.settleTimeoutMs ?? witness_1.DEFAULT_SETTLE_TIMEOUT_MS);
220
+ }
143
221
  }
144
222
  // 1. Capture screenshot (full-page on Chromium, viewport elsewhere)
145
223
  let screenshot;
@@ -149,6 +227,8 @@ async function witness(driver, name, options = {}) {
149
227
  finally {
150
228
  if (injected)
151
229
  await removeCaptureCss(driver);
230
+ if (stabilize)
231
+ await stopSettleObserver(driver);
152
232
  }
153
233
  // 2. Capture DOM (best-effort — never break the screenshot path)
154
234
  let dom;
@@ -172,7 +252,45 @@ async function witness(driver, name, options = {}) {
172
252
  // is unavailable for this snapshot; pixel diff still works.
173
253
  }
174
254
  }
175
- // 3. Write to .testivai/temp/<name>/
255
+ // 3. Capture the element map (best-effort — same contract as the DOM).
256
+ // This is what powers region→selector attribution, shift
257
+ // classification, and the computed-style fingerprint on the compare
258
+ // side. The injected function is the SAME one the Playwright adapter
259
+ // uses (`@testivai/witness`), so both lanes produce identical maps.
260
+ // Selenium's executeScript needs an explicit `return`.
261
+ let elementMap;
262
+ if (!options.skipElementMap && typeof driver.executeScript === 'function') {
263
+ try {
264
+ const expr = (0, witness_1.buildElementMapExpression)(options.maxElements ?? witness_1.DEFAULT_MAX_ELEMENTS, ignoreSelectors);
265
+ const result = await driver.executeScript(`return ${expr}`);
266
+ if (Array.isArray(result) && result.length > 0) {
267
+ elementMap = result;
268
+ }
269
+ }
270
+ catch {
271
+ // Suppressed by design, exactly like DOM capture: without a map the
272
+ // report falls back to pixel + DOM layers instead of failing.
273
+ }
274
+ }
275
+ // 4. Write to .testivai/temp/<name>/
176
276
  const store = new witness_1.BaselineStore(process.cwd());
177
277
  store.writeTemp(name, screenshot, dom);
278
+ // 5. Shard manifest. A Selenium script has no end-of-run hook to attach to,
279
+ // so it is refreshed on every capture — which is exactly what lets a
280
+ // Selenium suite join the same sharded CI flow as Playwright. `complete`
281
+ // stays false because this adapter cannot know the run finished.
282
+ const shard = (0, witness_1.parseShardEnv)(process.env.TESTIVAI_SHARD);
283
+ if (shard) {
284
+ (0, witness_1.writeShardManifest)(path.join(process.cwd(), '.testivai', 'temp'), shard);
285
+ }
286
+ if (elementMap !== undefined) {
287
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp', name);
288
+ try {
289
+ fs.mkdirSync(tempDir, { recursive: true });
290
+ fs.writeFileSync(path.join(tempDir, 'elements.json'), JSON.stringify(elementMap));
291
+ }
292
+ catch {
293
+ // A map we cannot persist is not worth failing a capture over.
294
+ }
295
+ }
178
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testivai/witness-selenium",
3
- "version": "0.1.10",
3
+ "version": "0.3.0",
4
4
  "description": "Selenium WebDriver 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",
@@ -49,7 +49,7 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@testivai/witness": "1.11.1"
52
+ "@testivai/witness": "1.13.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/jest": "^30.0.0",