@testivai/witness-selenium 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 TestivAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @testivai/witness-selenium
2
+
3
+ Selenium WebDriver adapter for [TestivAI Witness](https://github.com/mcbuddy/testivai-oss) — local-first visual regression testing with pixel + DOM comparison. No cloud account, nothing uploaded.
4
+
5
+ Shares baselines, tolerances, the HTML report, and the PR approval flow with every other TestivAI adapter (Playwright, WebdriverIO, Python, Java) through one on-disk contract.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i -D @testivai/witness-selenium @testivai/witness selenium-webdriver
11
+ ```
12
+
13
+ ## Use
14
+
15
+ ```js
16
+ const { Builder } = require('selenium-webdriver');
17
+ const { testivai } = require('@testivai/witness-selenium');
18
+
19
+ const driver = await new Builder().forBrowser('chrome').build();
20
+ await driver.get('http://localhost:3000');
21
+ await testivai.witness(driver, 'homepage');
22
+ await driver.quit();
23
+ ```
24
+
25
+ Then compare against baselines and render the report:
26
+
27
+ ```bash
28
+ npx testivai report # add --fail-on-diff in CI
29
+ npx testivai approve --all # promote current captures to baselines
30
+ ```
31
+
32
+ Works inside any runner (Jest, Mocha, plain Node scripts) — the adapter only needs the `driver` object.
33
+
34
+ ## Options
35
+
36
+ ```js
37
+ await testivai.witness(driver, 'pricing', {
38
+ ignoreSelectors: ['.live-chat', '[data-testid="ad"]'], // hidden from pixels AND the DOM snapshot
39
+ variant: 'firefox', // baselines keyed as pricing__firefox
40
+ stabilize: false, // skip animation-freeze + font wait for this capture
41
+ skipDom: true, // pixel diff only, no noise hint
42
+ });
43
+ ```
44
+
45
+ Global settings (thresholds, tolerances, `ignoreSelectors`) live in `.testivai/config.json` — see the [main docs](https://github.com/mcbuddy/testivai-oss/tree/main/docs).
46
+
47
+ ## Full-page screenshots
48
+
49
+ - **Chrome / Edge** — true full-page capture via CDP (`captureBeyondViewport`), the same mechanism Playwright uses. Automatic; nothing to configure.
50
+ - **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.)
51
+
52
+ ## Stabilized captures
53
+
54
+ Before every capture (default on): CSS animations/transitions complete instantly at their final state, the caret is hidden, smooth scrolling is disabled, and web fonts are awaited (bounded 10s). These are the top causes of flaky visual diffs.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @testivai/witness-selenium
3
+ *
4
+ * Selenium WebDriver adapter for the TestivAI Witness OSS toolkit. Pairs
5
+ * with `@testivai/witness` (the local CLI + diff engine) and shares the
6
+ * same `.testivai/baselines/` layout as every other TestivAI adapter.
7
+ *
8
+ * import { testivai } from '@testivai/witness-selenium';
9
+ * await testivai.witness(driver, 'homepage');
10
+ *
11
+ * Then run `npx testivai report` (add `--fail-on-diff` in CI) to compare
12
+ * against committed baselines and render visual-report/index.html.
13
+ */
14
+ import { witness } from './witness';
15
+ export { witness } from './witness';
16
+ export type { WitnessDriver, WitnessOptions } from './types';
17
+ /**
18
+ * Convenience namespace mirroring @testivai/witness-playwright's API:
19
+ *
20
+ * await testivai.witness(driver, 'homepage');
21
+ */
22
+ export declare const testivai: {
23
+ witness: typeof witness;
24
+ };
25
+ export declare const VERSION: string;
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * @testivai/witness-selenium
4
+ *
5
+ * Selenium WebDriver adapter for the TestivAI Witness OSS toolkit. Pairs
6
+ * with `@testivai/witness` (the local CLI + diff engine) and shares the
7
+ * same `.testivai/baselines/` layout as every other TestivAI adapter.
8
+ *
9
+ * import { testivai } from '@testivai/witness-selenium';
10
+ * await testivai.witness(driver, 'homepage');
11
+ *
12
+ * Then run `npx testivai report` (add `--fail-on-diff` in CI) to compare
13
+ * against committed baselines and render visual-report/index.html.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.VERSION = exports.testivai = exports.witness = void 0;
17
+ const witness_1 = require("./witness");
18
+ var witness_2 = require("./witness");
19
+ Object.defineProperty(exports, "witness", { enumerable: true, get: function () { return witness_2.witness; } });
20
+ /**
21
+ * Convenience namespace mirroring @testivai/witness-playwright's API:
22
+ *
23
+ * await testivai.witness(driver, 'homepage');
24
+ */
25
+ exports.testivai = {
26
+ witness: witness_1.witness,
27
+ };
28
+ exports.VERSION = require('../package.json').version;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Public types for @testivai/witness-selenium.
3
+ */
4
+ /**
5
+ * Minimal subset of the selenium-webdriver `WebDriver` the witness function
6
+ * relies on. Structural typing means consumers can pass any Selenium-shaped
7
+ * object (real driver or mock) without us pulling `selenium-webdriver`'s
8
+ * types into our public surface.
9
+ */
10
+ export interface WitnessDriver {
11
+ /**
12
+ * Capture a screenshot of the current viewport. Returns a base64 PNG,
13
+ * matching selenium-webdriver's `driver.takeScreenshot()` return shape.
14
+ * Used as the fallback when no full-page mechanism is available.
15
+ */
16
+ takeScreenshot(): Promise<string>;
17
+ /**
18
+ * Execute a script in the page context. We only use the string form with
19
+ * trailing args (`arguments[0]`… inside the script).
20
+ */
21
+ executeScript<T = unknown>(script: string, ...args: unknown[]): Promise<T>;
22
+ /**
23
+ * Chromium-only (chrome.Driver / edge.Driver): raw CDP command. Presence
24
+ * of this method is how the adapter detects that full-page capture via
25
+ * `Page.captureScreenshot` (captureBeyondViewport) is available — the
26
+ * same mechanism Playwright uses under the hood. Absent on Firefox and
27
+ * Safari drivers, which fall back to the viewport screenshot.
28
+ */
29
+ sendAndGetDevToolsCommand?<T = unknown>(cmd: string, params?: object): Promise<T>;
30
+ }
31
+ /**
32
+ * Options for a single witness() call. All optional.
33
+ */
34
+ export interface WitnessOptions {
35
+ /**
36
+ * Skip DOM capture even if the adapter would normally write dom.html
37
+ * alongside the screenshot. Use for pages where DOM serialization is
38
+ * known to be expensive or unstable.
39
+ */
40
+ skipDom?: boolean;
41
+ /**
42
+ * CSS selectors for elements to hide (`visibility: hidden`) for the
43
+ * duration of the capture, so dynamic content (timestamps, ads, live
44
+ * widgets) never contributes to the pixel diff. Merged with the global
45
+ * `ignoreSelectors` list from `.testivai/config.json`. Matched elements
46
+ * are also excluded from the DOM snapshot (one consistent semantic).
47
+ */
48
+ ignoreSelectors?: string[];
49
+ /**
50
+ * Variant key for multi-browser / multi-viewport runs. Folded into the
51
+ * snapshot name (`<name>__<variant>`) so parallel sessions don't
52
+ * overwrite each other's baselines. Example: `'firefox'`, `'chrome-1280'`.
53
+ */
54
+ variant?: string;
55
+ /**
56
+ * Stabilize the page before capture: disable CSS animations/transitions,
57
+ * hide the caret, and wait for web fonts. Overrides the `stabilize`
58
+ * setting from `.testivai/config.json` (default true).
59
+ */
60
+ stabilize?: boolean;
61
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Public types for @testivai/witness-selenium.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @testivai/witness-selenium — capture function
3
+ *
4
+ * The user-facing call inside a Selenium (JavaScript) test:
5
+ *
6
+ * import { testivai } from '@testivai/witness-selenium';
7
+ * await testivai.witness(driver, 'homepage');
8
+ *
9
+ * Captures a screenshot and the page DOM through Selenium's public APIs,
10
+ * then writes both into `.testivai/temp/<name>/` using the same
11
+ * BaselineStore layout as every other TestivAI adapter. Run
12
+ * `npx testivai report` after the tests to compare against baselines.
13
+ *
14
+ * Full-page screenshots per browser:
15
+ * - Chromium (Chrome/Edge): CDP `Page.captureScreenshot` with
16
+ * captureBeyondViewport via `sendAndGetDevToolsCommand` — the same
17
+ * mechanism Playwright uses under the hood
18
+ * - Firefox/Safari: the JS bindings expose no full-page API, so the
19
+ * capture is viewport-only (keep the window sized to what you want
20
+ * compared; baselines are consistent run-to-run either way)
21
+ */
22
+ import type { WitnessDriver, WitnessOptions } from './types';
23
+ /**
24
+ * Capture a screenshot + DOM and write them as a temp snapshot.
25
+ *
26
+ * @param driver selenium-webdriver WebDriver (must expose
27
+ * `takeScreenshot()` and `executeScript()`).
28
+ * @param name Snapshot name. Becomes the directory under
29
+ * `.testivai/temp/<name>/` and the key in the rendered report.
30
+ * @param options Per-call overrides.
31
+ */
32
+ export declare function witness(driver: WitnessDriver, name: string, options?: WitnessOptions): Promise<void>;
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ /**
3
+ * @testivai/witness-selenium — capture function
4
+ *
5
+ * The user-facing call inside a Selenium (JavaScript) test:
6
+ *
7
+ * import { testivai } from '@testivai/witness-selenium';
8
+ * await testivai.witness(driver, 'homepage');
9
+ *
10
+ * Captures a screenshot and the page DOM through Selenium's public APIs,
11
+ * then writes both into `.testivai/temp/<name>/` using the same
12
+ * BaselineStore layout as every other TestivAI adapter. Run
13
+ * `npx testivai report` after the tests to compare against baselines.
14
+ *
15
+ * Full-page screenshots per browser:
16
+ * - Chromium (Chrome/Edge): CDP `Page.captureScreenshot` with
17
+ * captureBeyondViewport via `sendAndGetDevToolsCommand` — the same
18
+ * mechanism Playwright uses under the hood
19
+ * - Firefox/Safari: the JS bindings expose no full-page API, so the
20
+ * capture is viewport-only (keep the window sized to what you want
21
+ * compared; baselines are consistent run-to-run either way)
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.witness = witness;
25
+ const witness_1 = require("@testivai/witness");
26
+ /** id of the style element injected for the duration of a capture */
27
+ const STYLE_ID = '__testivai_capture_css__';
28
+ /**
29
+ * Freezes CSS animations/transitions, hides the caret, and forces instant
30
+ * scrolling — the top causes of flaky visual diffs. Near-zero durations
31
+ * (not `none`) let entry animations COMPLETE at their final state.
32
+ * Mirrors the Playwright adapter's STABILIZE_CSS.
33
+ */
34
+ const STABILIZE_CSS = '*, *::before, *::after { animation-duration: 0.001s !important; animation-delay: 0s !important; ' +
35
+ 'animation-iteration-count: 1 !important; transition-duration: 0.001s !important; ' +
36
+ 'transition-delay: 0s !important; caret-color: transparent !important; scroll-behavior: auto !important; }';
37
+ /** Build the ignoreSelectors CSS block (visibility preserves layout). */
38
+ function buildIgnoreCss(selectors) {
39
+ return selectors.map((s) => `${s} { visibility: hidden !important; }`).join('\n');
40
+ }
41
+ async function injectCaptureCss(driver, css) {
42
+ try {
43
+ await driver.executeScript(`var el = document.createElement('style');` +
44
+ `el.id = arguments[0]; el.textContent = arguments[1];` +
45
+ `document.head.appendChild(el);`, STYLE_ID, css);
46
+ return true;
47
+ }
48
+ catch {
49
+ return false; // locked-down page; capture proceeds without stabilization
50
+ }
51
+ }
52
+ async function removeCaptureCss(driver) {
53
+ try {
54
+ await driver.executeScript(`var el = document.getElementById(arguments[0]); if (el) el.remove();`, STYLE_ID);
55
+ }
56
+ catch {
57
+ // best-effort cleanup
58
+ }
59
+ }
60
+ /**
61
+ * Wait (bounded at 10s) for web fonts to finish loading so the capture
62
+ * never shows a fallback font — a fallback-font capture diffs 30%+.
63
+ */
64
+ async function waitForFonts(driver) {
65
+ const deadline = Date.now() + 10000;
66
+ for (;;) {
67
+ try {
68
+ const ready = await driver.executeScript(`return document.fonts ? document.fonts.status !== 'loading' : true;`);
69
+ if (ready || Date.now() >= deadline)
70
+ return;
71
+ }
72
+ catch {
73
+ return; // no FontFaceSet or executeScript failed — proceed
74
+ }
75
+ await new Promise((resolve) => setTimeout(resolve, 100));
76
+ }
77
+ }
78
+ /**
79
+ * Full-page screenshot where the driver supports it, viewport otherwise.
80
+ * Returns a PNG Buffer.
81
+ */
82
+ async function captureScreenshot(driver) {
83
+ // Chromium drivers expose raw CDP; captureBeyondViewport gives a true
84
+ // full-page capture without resizing the window (which breaks 100vh
85
+ // layouts — the reason we never use the resize trick).
86
+ if (typeof driver.sendAndGetDevToolsCommand === 'function') {
87
+ try {
88
+ const result = await driver.sendAndGetDevToolsCommand('Page.captureScreenshot', { captureBeyondViewport: true, format: 'png' });
89
+ if (result && typeof result.data === 'string' && result.data.length > 0) {
90
+ return Buffer.from(result.data, 'base64');
91
+ }
92
+ }
93
+ catch {
94
+ // fall through to the viewport screenshot
95
+ }
96
+ }
97
+ const base64 = await driver.takeScreenshot();
98
+ if (typeof base64 !== 'string' || base64.length === 0) {
99
+ throw new Error('testivai.witness: driver.takeScreenshot() returned an empty value');
100
+ }
101
+ return Buffer.from(base64, 'base64');
102
+ }
103
+ /**
104
+ * Capture a screenshot + DOM and write them as a temp snapshot.
105
+ *
106
+ * @param driver selenium-webdriver WebDriver (must expose
107
+ * `takeScreenshot()` and `executeScript()`).
108
+ * @param name Snapshot name. Becomes the directory under
109
+ * `.testivai/temp/<name>/` and the key in the rendered report.
110
+ * @param options Per-call overrides.
111
+ */
112
+ async function witness(driver, name, options = {}) {
113
+ if (!name || typeof name !== 'string') {
114
+ throw new Error('testivai.witness: snapshot name is required and must be a non-empty string');
115
+ }
116
+ if (!driver || typeof driver.takeScreenshot !== 'function') {
117
+ throw new Error('testivai.witness: driver argument must expose takeScreenshot()');
118
+ }
119
+ // Multi-browser runs: fold the variant into the name so sessions don't
120
+ // overwrite each other's baselines (same mechanism as the Playwright
121
+ // adapter's multi-project handling).
122
+ if (options.variant) {
123
+ name = `${name}__${options.variant.replace(/[^a-z0-9_-]+/gi, '_').toLowerCase()}`;
124
+ }
125
+ // 0. Prepare the page: stabilization CSS (animations/caret/fonts — the
126
+ // flake killers) + ignoreSelectors from config.json and this call,
127
+ // injected as one style tag and removed after the capture.
128
+ const localConfig = (0, witness_1.loadLocalConfig)(process.cwd());
129
+ const stabilize = options.stabilize ?? localConfig.stabilize;
130
+ const ignoreSelectors = [
131
+ ...new Set([...(localConfig.ignoreSelectors ?? []), ...(options.ignoreSelectors ?? [])]),
132
+ ];
133
+ const cssParts = [];
134
+ if (stabilize)
135
+ cssParts.push(STABILIZE_CSS);
136
+ if (ignoreSelectors.length > 0)
137
+ cssParts.push(buildIgnoreCss(ignoreSelectors));
138
+ let injected = false;
139
+ if (cssParts.length > 0 && typeof driver.executeScript === 'function') {
140
+ injected = await injectCaptureCss(driver, cssParts.join('\n'));
141
+ if (stabilize)
142
+ await waitForFonts(driver);
143
+ }
144
+ // 1. Capture screenshot (full-page on Chromium, viewport elsewhere)
145
+ let screenshot;
146
+ try {
147
+ screenshot = await captureScreenshot(driver);
148
+ }
149
+ finally {
150
+ if (injected)
151
+ await removeCaptureCss(driver);
152
+ }
153
+ // 2. Capture DOM (best-effort — never break the screenshot path)
154
+ let dom;
155
+ if (!options.skipDom && typeof driver.executeScript === 'function') {
156
+ try {
157
+ const result = await driver.executeScript(
158
+ // ignoreSelectors excludes elements from the pixel diff, so they
159
+ // are excluded from the DOM/text signal too (consistent semantic).
160
+ `var clone = document.documentElement.cloneNode(true);` +
161
+ `var sels = arguments[0] || [];` +
162
+ `for (var i = 0; i < sels.length; i++) {` +
163
+ ` try { clone.querySelectorAll(sels[i]).forEach(function(el){ el.remove(); }); } catch (e) {}` +
164
+ `}` +
165
+ `return clone.outerHTML;`, ignoreSelectors);
166
+ if (typeof result === 'string' && result.length > 0) {
167
+ dom = result;
168
+ }
169
+ }
170
+ catch {
171
+ // Suppressed by design. DOM capture failure means the noise hint
172
+ // is unavailable for this snapshot; pixel diff still works.
173
+ }
174
+ }
175
+ // 3. Write to .testivai/temp/<name>/
176
+ const store = new witness_1.BaselineStore(process.cwd());
177
+ store.writeTemp(name, screenshot, dom);
178
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@testivai/witness-selenium",
3
+ "version": "0.1.0",
4
+ "description": "Selenium WebDriver adapter for TestivAI Witness — local-first visual regression with pixel + DOM comparison",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "keywords": [
14
+ "selenium",
15
+ "selenium-webdriver",
16
+ "visual-regression",
17
+ "testing",
18
+ "screenshot-testing",
19
+ "testivai"
20
+ ],
21
+ "author": "TestivAI",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/mcbuddy/testivai-oss.git",
26
+ "directory": "packages/selenium"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/mcbuddy/testivai-oss/issues"
30
+ },
31
+ "homepage": "https://github.com/mcbuddy/testivai-oss/tree/main/packages/selenium#readme",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "peerDependencies": {
44
+ "selenium-webdriver": ">=4.6.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "selenium-webdriver": {
48
+ "optional": false
49
+ }
50
+ },
51
+ "dependencies": {
52
+ "@testivai/witness": "1.3.1"
53
+ },
54
+ "devDependencies": {
55
+ "@types/jest": "^30.0.0",
56
+ "@types/node": "^20.0.0",
57
+ "jest": "^30.4.2",
58
+ "ts-jest": "^29.4.11",
59
+ "typescript": "^6.0.3"
60
+ },
61
+ "scripts": {
62
+ "build": "tsc",
63
+ "clean": "rm -rf dist",
64
+ "dev": "tsc --watch",
65
+ "test": "jest",
66
+ "test:unit": "jest",
67
+ "test:watch": "jest --watch",
68
+ "test:coverage": "jest --coverage"
69
+ }
70
+ }