@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 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 a PNG Buffer.
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 result = await driver.sendAndGetDevToolsCommand('Page.captureScreenshot', { captureBeyondViewport: true, format: 'png' });
165
- if (result && typeof result.data === 'string' && result.data.length > 0) {
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.10",
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.4.0"
52
+ "@testivai/witness": "2.5.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/jest": "^30.0.0",