@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 +18 -0
- package/dist/witness.js +120 -2
- package/package.json +2 -2
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.
|
|
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.
|
|
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.
|
|
52
|
+
"@testivai/witness": "1.13.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/jest": "^30.0.0",
|