@testivai/witness-selenium 0.3.10 → 0.3.12
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 +1 -1
- package/dist/capture/full-page.d.ts +30 -0
- package/dist/capture/full-page.js +65 -0
- package/dist/types.d.ts +6 -0
- package/dist/witness.js +56 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ All of these stay `changed` unless you configure a pass criterion; `noiseAutoPas
|
|
|
57
57
|
|
|
58
58
|
## Full-page screenshots
|
|
59
59
|
|
|
60
|
-
- **Chrome / Edge** — true full-page capture via CDP (`captureBeyondViewport`), the same mechanism Playwright uses. Automatic; nothing to configure.
|
|
60
|
+
- **Chrome / Edge** — true full-page capture via CDP (`captureBeyondViewport`), the same mechanism Playwright uses. Automatic; nothing to configure. Chrome can return a frame painted for the wrong viewport while it sets up that capture, so the adapter waits for a freshly rendered frame, then captures until two captures in a row are identical (at most four). A stable page costs one extra screenshot; `TESTIVAI_DEBUG=true` logs when captures differed.
|
|
61
61
|
- **Firefox / Safari** — the Selenium JS bindings expose no full-page API, so captures are viewport-sized. Set the window to the size you want compared; runs are consistent either way. (The Python and Java TestivAI adapters do full-page on Firefox via its native API.)
|
|
62
62
|
|
|
63
63
|
## Stabilized captures
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type CaptureLog } from '@testivai/witness';
|
|
2
|
+
import type { WitnessDriver } from '../types';
|
|
3
|
+
/** A Chromium driver: one that can send raw CDP commands. */
|
|
4
|
+
export type CdpDriver = Required<Pick<WitnessDriver, 'sendAndGetDevToolsCommand'>>;
|
|
5
|
+
/**
|
|
6
|
+
* Full-page screenshot through CDP: wait for a fresh frame, measure the
|
|
7
|
+
* content, then capture beyond the viewport until two captures in a row match
|
|
8
|
+
* (at most MAX_STABLE_CAPTURES from @testivai/witness).
|
|
9
|
+
*
|
|
10
|
+
* Chrome can hand back a frame painted with styles resolved against the wrong
|
|
11
|
+
* viewport while it sets up a beyond-viewport capture: narrow-viewport media
|
|
12
|
+
* rules, a collapsed 100vh hero, or em text drawn at a `max-width: 480px`
|
|
13
|
+
* body size. The page measures correctly but the pixels do not, so an
|
|
14
|
+
* unchanged page flips between "changed" and "passed" across runs. See
|
|
15
|
+
* capture/stable-capture.ts in @testivai/witness, which the standalone CLI
|
|
16
|
+
* and the Playwright adapter share.
|
|
17
|
+
*
|
|
18
|
+
* Selenium reaches the page only through `sendAndGetDevToolsCommand`, so the
|
|
19
|
+
* frame wait is CDP `Runtime.evaluate` with `awaitPromise`. Every command gets
|
|
20
|
+
* an explicit params object instead of relying on the bindings' default.
|
|
21
|
+
*
|
|
22
|
+
* A stable page costs one extra screenshot.
|
|
23
|
+
*
|
|
24
|
+
* @param driver A Chromium (Chrome/Edge) driver.
|
|
25
|
+
* @param log Optional debug sink.
|
|
26
|
+
* @returns The PNG bytes of the kept capture.
|
|
27
|
+
* @throws When the page reports no usable size or a capture returns no data,
|
|
28
|
+
* so the caller can fall back to a viewport screenshot.
|
|
29
|
+
*/
|
|
30
|
+
export declare function captureFullPageScreenshot(driver: CdpDriver, log?: CaptureLog): Promise<Buffer>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.captureFullPageScreenshot = captureFullPageScreenshot;
|
|
4
|
+
const witness_1 = require("@testivai/witness");
|
|
5
|
+
/**
|
|
6
|
+
* Full-page screenshot through CDP: wait for a fresh frame, measure the
|
|
7
|
+
* content, then capture beyond the viewport until two captures in a row match
|
|
8
|
+
* (at most MAX_STABLE_CAPTURES from @testivai/witness).
|
|
9
|
+
*
|
|
10
|
+
* Chrome can hand back a frame painted with styles resolved against the wrong
|
|
11
|
+
* viewport while it sets up a beyond-viewport capture: narrow-viewport media
|
|
12
|
+
* rules, a collapsed 100vh hero, or em text drawn at a `max-width: 480px`
|
|
13
|
+
* body size. The page measures correctly but the pixels do not, so an
|
|
14
|
+
* unchanged page flips between "changed" and "passed" across runs. See
|
|
15
|
+
* capture/stable-capture.ts in @testivai/witness, which the standalone CLI
|
|
16
|
+
* and the Playwright adapter share.
|
|
17
|
+
*
|
|
18
|
+
* Selenium reaches the page only through `sendAndGetDevToolsCommand`, so the
|
|
19
|
+
* frame wait is CDP `Runtime.evaluate` with `awaitPromise`. Every command gets
|
|
20
|
+
* an explicit params object instead of relying on the bindings' default.
|
|
21
|
+
*
|
|
22
|
+
* A stable page costs one extra screenshot.
|
|
23
|
+
*
|
|
24
|
+
* @param driver A Chromium (Chrome/Edge) driver.
|
|
25
|
+
* @param log Optional debug sink.
|
|
26
|
+
* @returns The PNG bytes of the kept capture.
|
|
27
|
+
* @throws When the page reports no usable size or a capture returns no data,
|
|
28
|
+
* so the caller can fall back to a viewport screenshot.
|
|
29
|
+
*/
|
|
30
|
+
async function captureFullPageScreenshot(driver, log) {
|
|
31
|
+
const waitOnce = async () => {
|
|
32
|
+
const evaluated = await driver.sendAndGetDevToolsCommand('Runtime.evaluate', {
|
|
33
|
+
expression: witness_1.FRESH_FRAME_EXPRESSION,
|
|
34
|
+
awaitPromise: true,
|
|
35
|
+
returnByValue: true,
|
|
36
|
+
});
|
|
37
|
+
return evaluated?.result?.value === true;
|
|
38
|
+
};
|
|
39
|
+
const settle = () => (0, witness_1.settleFreshFrame)(waitOnce, log);
|
|
40
|
+
await settle();
|
|
41
|
+
const metrics = await driver.sendAndGetDevToolsCommand('Page.getLayoutMetrics', {});
|
|
42
|
+
const size = metrics?.cssContentSize ?? metrics?.contentSize;
|
|
43
|
+
const width = Math.ceil(size?.width ?? 0);
|
|
44
|
+
const height = Math.ceil(size?.height ?? 0);
|
|
45
|
+
if (!(width > 0 && height > 0)) {
|
|
46
|
+
throw new Error(`page reported unusable content size ${width}x${height}`);
|
|
47
|
+
}
|
|
48
|
+
log?.(`page ${width}x${height}, viewport ${metrics.cssLayoutViewport?.clientWidth}x${metrics.cssLayoutViewport?.clientHeight}`);
|
|
49
|
+
const data = await (0, witness_1.captureUntilStable)({
|
|
50
|
+
capture: async () => {
|
|
51
|
+
const shot = await driver.sendAndGetDevToolsCommand('Page.captureScreenshot', {
|
|
52
|
+
format: 'png',
|
|
53
|
+
captureBeyondViewport: true,
|
|
54
|
+
clip: { x: 0, y: 0, width, height, scale: 1 },
|
|
55
|
+
});
|
|
56
|
+
if (typeof shot?.data !== 'string' || shot.data.length === 0) {
|
|
57
|
+
throw new Error('Page.captureScreenshot returned no data');
|
|
58
|
+
}
|
|
59
|
+
return shot.data;
|
|
60
|
+
},
|
|
61
|
+
settle,
|
|
62
|
+
log,
|
|
63
|
+
});
|
|
64
|
+
return Buffer.from(data, 'base64');
|
|
65
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -27,6 +27,12 @@ export interface WitnessDriver {
|
|
|
27
27
|
* Safari drivers, which fall back to the viewport screenshot.
|
|
28
28
|
*/
|
|
29
29
|
sendAndGetDevToolsCommand?<T = unknown>(cmd: string, params?: object): Promise<T>;
|
|
30
|
+
/**
|
|
31
|
+
* selenium-webdriver's `driver.getCapabilities()`. Optional: when present,
|
|
32
|
+
* the browser, its version and the driver are recorded as capture
|
|
33
|
+
* provenance in the capture's metadata.json.
|
|
34
|
+
*/
|
|
35
|
+
getCapabilities?(): Promise<unknown>;
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
32
38
|
* Options for a single witness() call. All optional.
|
package/dist/witness.js
CHANGED
|
@@ -58,6 +58,9 @@ exports.witness = witness;
|
|
|
58
58
|
const witness_1 = require("@testivai/witness");
|
|
59
59
|
const fs = __importStar(require("fs"));
|
|
60
60
|
const path = __importStar(require("path"));
|
|
61
|
+
const full_page_1 = require("./capture/full-page");
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
63
|
+
const ADAPTER_VERSION = require('../package.json').version;
|
|
61
64
|
/** id of the style element injected for the duration of a capture */
|
|
62
65
|
const STYLE_ID = '__testivai_capture_css__';
|
|
63
66
|
/**
|
|
@@ -153,18 +156,22 @@ async function waitForFonts(driver) {
|
|
|
153
156
|
}
|
|
154
157
|
/**
|
|
155
158
|
* Full-page screenshot where the driver supports it, viewport otherwise.
|
|
156
|
-
* Returns
|
|
159
|
+
* Returns the PNG and the capture method it came from.
|
|
157
160
|
*/
|
|
158
161
|
async function captureScreenshot(driver) {
|
|
159
162
|
// Chromium drivers expose raw CDP; captureBeyondViewport gives a true
|
|
160
163
|
// full-page capture without resizing the window (which breaks 100vh
|
|
161
|
-
// layouts — the reason we never use the resize trick).
|
|
164
|
+
// layouts — the reason we never use the resize trick). It waits for a
|
|
165
|
+
// fresh frame and captures until two in a row match: Chrome can return a
|
|
166
|
+
// frame painted for the wrong viewport while it sets up the capture
|
|
167
|
+
// (capture/full-page.ts).
|
|
162
168
|
if (typeof driver.sendAndGetDevToolsCommand === 'function') {
|
|
169
|
+
const captureLog = process.env.TESTIVAI_DEBUG === 'true'
|
|
170
|
+
? (message) => console.log(`[TestivAI] Full-page capture: ${message}`)
|
|
171
|
+
: undefined;
|
|
163
172
|
try {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
return Buffer.from(result.data, 'base64');
|
|
167
|
-
}
|
|
173
|
+
const png = await (0, full_page_1.captureFullPageScreenshot)(driver, captureLog);
|
|
174
|
+
return { png, method: witness_1.CAPTURE_METHODS.CDP_FULL_PAGE };
|
|
168
175
|
}
|
|
169
176
|
catch {
|
|
170
177
|
// fall through to the viewport screenshot
|
|
@@ -174,7 +181,7 @@ async function captureScreenshot(driver) {
|
|
|
174
181
|
if (typeof base64 !== 'string' || base64.length === 0) {
|
|
175
182
|
throw new Error('testivai.witness: driver.takeScreenshot() returned an empty value');
|
|
176
183
|
}
|
|
177
|
-
return Buffer.from(base64, 'base64');
|
|
184
|
+
return { png: Buffer.from(base64, 'base64'), method: witness_1.CAPTURE_METHODS.WEBDRIVER_VIEWPORT };
|
|
178
185
|
}
|
|
179
186
|
/**
|
|
180
187
|
* Capture a screenshot + DOM and write them as a temp snapshot.
|
|
@@ -198,10 +205,35 @@ async function witness(driver, name, options = {}) {
|
|
|
198
205
|
if (options.variant) {
|
|
199
206
|
name = `${name}__${options.variant.replace(/[^a-z0-9_-]+/gi, '_').toLowerCase()}`;
|
|
200
207
|
}
|
|
208
|
+
const localConfig = (0, witness_1.loadLocalConfig)(process.cwd());
|
|
209
|
+
// Capture provenance: what the page is rendered with, measured before
|
|
210
|
+
// anything is injected, plus the session's capabilities. Best-effort.
|
|
211
|
+
let renderProbe = null;
|
|
212
|
+
let capabilities = null;
|
|
213
|
+
if (typeof driver.executeScript === 'function') {
|
|
214
|
+
try {
|
|
215
|
+
renderProbe = await driver.executeScript(`return ${witness_1.RENDER_PROBE_EXPRESSION}`);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// no probe: no render settings recorded
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (typeof driver.getCapabilities === 'function') {
|
|
222
|
+
try {
|
|
223
|
+
capabilities = await driver.getCapabilities();
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// no capabilities: the probe's user agent is the fallback
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const browserName = (0, witness_1.browserFromCapabilities)(capabilities)?.browser.name ?? (() => {
|
|
230
|
+
const ua = (0, witness_1.parseRenderProbe)(renderProbe)?.userAgent;
|
|
231
|
+
return (ua ? (0, witness_1.browserFromUserAgent)(ua)?.name : undefined) ?? 'unknown';
|
|
232
|
+
})();
|
|
233
|
+
name = (0, witness_1.applyEnvironmentPolicyToName)(name, { browser: { name: browserName }, os: { platform: process.platform } }, localConfig.environmentPolicy);
|
|
201
234
|
// 0. Prepare the page: stabilization CSS (animations/caret/fonts — the
|
|
202
235
|
// flake killers) + ignoreSelectors from config.json and this call,
|
|
203
236
|
// injected as one style tag and removed after the capture.
|
|
204
|
-
const localConfig = (0, witness_1.loadLocalConfig)(process.cwd());
|
|
205
237
|
const stabilize = options.stabilize ?? localConfig.stabilize;
|
|
206
238
|
const ignoreSelectors = [
|
|
207
239
|
...new Set([...(localConfig.ignoreSelectors ?? []), ...(options.ignoreSelectors ?? [])]),
|
|
@@ -221,8 +253,9 @@ async function witness(driver, name, options = {}) {
|
|
|
221
253
|
}
|
|
222
254
|
// 1. Capture screenshot (full-page on Chromium, viewport elsewhere)
|
|
223
255
|
let screenshot;
|
|
256
|
+
let captureMethod;
|
|
224
257
|
try {
|
|
225
|
-
screenshot = await captureScreenshot(driver);
|
|
258
|
+
({ png: screenshot, method: captureMethod } = await captureScreenshot(driver));
|
|
226
259
|
}
|
|
227
260
|
finally {
|
|
228
261
|
if (injected)
|
|
@@ -275,6 +308,20 @@ async function witness(driver, name, options = {}) {
|
|
|
275
308
|
// 4. Write to .testivai/temp/<name>/
|
|
276
309
|
const store = new witness_1.BaselineStore(process.cwd());
|
|
277
310
|
store.writeTemp(name, screenshot, dom);
|
|
311
|
+
const environment = (0, witness_1.webDriverEnvironment)({
|
|
312
|
+
capabilities,
|
|
313
|
+
probe: renderProbe,
|
|
314
|
+
adapter: { name: '@testivai/witness-selenium', version: ADAPTER_VERSION, captureMethod },
|
|
315
|
+
framework: { name: 'selenium-webdriver', version: (0, witness_1.installedPackageVersion)('selenium-webdriver') },
|
|
316
|
+
});
|
|
317
|
+
if (environment) {
|
|
318
|
+
try {
|
|
319
|
+
store.writeTempMetadata(name, { name, environment });
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// without it the compare cannot tell rendering drift from noise for this capture
|
|
323
|
+
}
|
|
324
|
+
}
|
|
278
325
|
// 5. Shard manifest. A Selenium script has no end-of-run hook to attach to,
|
|
279
326
|
// so it is refreshed on every capture — which is exactly what lets a
|
|
280
327
|
// Selenium suite join the same sharded CI flow as Playwright. `complete`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-selenium",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
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": "2.
|
|
52
|
+
"@testivai/witness": "2.5.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/jest": "^30.0.0",
|