@testivai/witness-webdriverio 0.1.2 → 0.2.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/README.md +7 -1
- package/dist/types.d.ts +21 -0
- package/dist/witness.js +99 -4
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -27,7 +27,11 @@ Create `.testivai/config.json` at your project root:
|
|
|
27
27
|
"mode": "local",
|
|
28
28
|
"threshold": 0.1,
|
|
29
29
|
"reportDir": "visual-report",
|
|
30
|
-
"autoOpen": false
|
|
30
|
+
"autoOpen": false,
|
|
31
|
+
"maxDiffPercent": 0,
|
|
32
|
+
"noiseAutoPass": false,
|
|
33
|
+
"stabilize": true,
|
|
34
|
+
"ignoreSelectors": []
|
|
31
35
|
}
|
|
32
36
|
```
|
|
33
37
|
|
|
@@ -113,6 +117,8 @@ Captures a screenshot + DOM and writes them as a temp snapshot.
|
|
|
113
117
|
| `browser` | WebdriverIO browser | Must expose `takeScreenshot()` and (for DOM) `execute()`. |
|
|
114
118
|
| `name` | string | Snapshot name. Becomes `.testivai/temp/<name>/` and the key in the report. |
|
|
115
119
|
| `options.skipDom` | boolean | Skip DOM capture for this snapshot. |
|
|
120
|
+
| `options.ignoreSelectors` | string[] | Elements hidden (`visibility: hidden`) for this capture; merged with the global `ignoreSelectors` from `.testivai/config.json`. |
|
|
121
|
+
| `options.stabilize` | boolean | Override capture stabilization (animations frozen, caret hidden, fonts awaited). Default: `true` (or the global `stabilize` setting). |
|
|
116
122
|
|
|
117
123
|
### `TestivaiService`
|
|
118
124
|
|
package/dist/types.d.ts
CHANGED
|
@@ -29,6 +29,27 @@ export interface WitnessOptions {
|
|
|
29
29
|
* known to be expensive or unstable.
|
|
30
30
|
*/
|
|
31
31
|
skipDom?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* CSS selectors for elements to hide (`visibility: hidden`) for the
|
|
34
|
+
* duration of the capture, so dynamic content (timestamps, ads, live
|
|
35
|
+
* widgets) never contributes to the pixel diff. Merged with the global
|
|
36
|
+
* `ignoreSelectors` list from `.testivai/config.json`.
|
|
37
|
+
*/
|
|
38
|
+
ignoreSelectors?: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Variant key for multi-browser / multi-viewport runs. Folded into the
|
|
41
|
+
* snapshot name (`<name>__<variant>`) so parallel capabilities don't
|
|
42
|
+
* overwrite each other's baselines. Example: set it per capability in
|
|
43
|
+
* wdio.conf.ts (`'chrome-1280'`, `'firefox-mobile'`).
|
|
44
|
+
*/
|
|
45
|
+
variant?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Stabilize the page before capture: disable CSS animations/transitions,
|
|
48
|
+
* hide the text caret, and wait for web fonts to finish loading — the top
|
|
49
|
+
* causes of flaky visual diffs. Default: true (also configurable globally
|
|
50
|
+
* via `stabilize` in `.testivai/config.json`; this per-call value wins).
|
|
51
|
+
*/
|
|
52
|
+
stabilize?: boolean;
|
|
32
53
|
}
|
|
33
54
|
/**
|
|
34
55
|
* Configuration passed to the WDIO service.
|
package/dist/witness.js
CHANGED
|
@@ -22,6 +22,64 @@
|
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.witness = witness;
|
|
24
24
|
const witness_1 = require("@testivai/witness");
|
|
25
|
+
/** id of the style element injected for the duration of a capture */
|
|
26
|
+
const STYLE_ID = '__testivai_capture_css__';
|
|
27
|
+
/**
|
|
28
|
+
* Freezes CSS animations/transitions, hides the caret, and forces instant
|
|
29
|
+
* scrolling — the top causes of flaky visual diffs. Mirrors the Playwright
|
|
30
|
+
* adapter's STABILIZE_CSS.
|
|
31
|
+
*/
|
|
32
|
+
const STABILIZE_CSS = '*, *::before, *::after { animation-duration: 0.001s !important; animation-delay: 0s !important; ' +
|
|
33
|
+
'animation-iteration-count: 1 !important; transition-duration: 0.001s !important; ' +
|
|
34
|
+
'transition-delay: 0s !important; caret-color: transparent !important; scroll-behavior: auto !important; }';
|
|
35
|
+
/** Build the ignoreSelectors CSS block (visibility preserves layout). */
|
|
36
|
+
function buildIgnoreCss(selectors) {
|
|
37
|
+
return selectors.map((s) => `${s} { visibility: hidden !important; }`).join('\n');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Inject a <style> tag with the given CSS. Returns true when injection
|
|
41
|
+
* succeeded. Uses the inline-string form of execute for WDIO 8/9 parity.
|
|
42
|
+
*/
|
|
43
|
+
async function injectCaptureCss(browser, css) {
|
|
44
|
+
try {
|
|
45
|
+
await browser.execute(`var el = document.createElement('style');` +
|
|
46
|
+
`el.id = ${JSON.stringify(STYLE_ID)};` +
|
|
47
|
+
`el.textContent = ${JSON.stringify(css)};` +
|
|
48
|
+
`document.head.appendChild(el);`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false; // capture proceeds without stabilization/ignores
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function removeCaptureCss(browser) {
|
|
56
|
+
try {
|
|
57
|
+
await browser.execute(`var el = document.getElementById(${JSON.stringify(STYLE_ID)});` +
|
|
58
|
+
`if (el) el.remove();`);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// best-effort cleanup
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Wait (bounded at ~3s) for web fonts to finish loading so the capture never
|
|
66
|
+
* shows a fallback font. Polled from Node because classic WebDriver execute
|
|
67
|
+
* does not await page-side promises.
|
|
68
|
+
*/
|
|
69
|
+
async function waitForFonts(browser) {
|
|
70
|
+
const deadline = Date.now() + 3000;
|
|
71
|
+
for (;;) {
|
|
72
|
+
try {
|
|
73
|
+
const status = await browser.execute(`return document.fonts ? document.fonts.status : 'loaded';`);
|
|
74
|
+
if (status !== 'loading' || Date.now() >= deadline)
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return; // no FontFaceSet or execute failed — proceed
|
|
79
|
+
}
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
25
83
|
/**
|
|
26
84
|
* Capture a screenshot + DOM and write them as a temp snapshot.
|
|
27
85
|
*
|
|
@@ -38,8 +96,40 @@ async function witness(browser, name, options = {}) {
|
|
|
38
96
|
if (!browser || typeof browser.takeScreenshot !== 'function') {
|
|
39
97
|
throw new Error('testivai.witness: browser argument must expose takeScreenshot()');
|
|
40
98
|
}
|
|
99
|
+
// Multi-capability runs: fold the variant into the name so capabilities
|
|
100
|
+
// don't overwrite each other's baselines (same mechanism as the Playwright
|
|
101
|
+
// adapter's multi-project handling).
|
|
102
|
+
if (options.variant) {
|
|
103
|
+
name = `${name}__${options.variant.replace(/[^a-z0-9_-]+/gi, '_').toLowerCase()}`;
|
|
104
|
+
}
|
|
105
|
+
// 0. Prepare the page: stabilization CSS (animations/caret/fonts — the
|
|
106
|
+
// flake killers) + ignoreSelectors from config.json and this call,
|
|
107
|
+
// injected as one style tag and removed after the capture.
|
|
108
|
+
const localConfig = (0, witness_1.loadLocalConfig)(process.cwd());
|
|
109
|
+
const stabilize = options.stabilize ?? localConfig.stabilize;
|
|
110
|
+
const ignoreSelectors = [
|
|
111
|
+
...new Set([...(localConfig.ignoreSelectors ?? []), ...(options.ignoreSelectors ?? [])]),
|
|
112
|
+
];
|
|
113
|
+
const cssParts = [];
|
|
114
|
+
if (stabilize)
|
|
115
|
+
cssParts.push(STABILIZE_CSS);
|
|
116
|
+
if (ignoreSelectors.length > 0)
|
|
117
|
+
cssParts.push(buildIgnoreCss(ignoreSelectors));
|
|
118
|
+
let injected = false;
|
|
119
|
+
if (cssParts.length > 0 && typeof browser.execute === 'function') {
|
|
120
|
+
injected = await injectCaptureCss(browser, cssParts.join('\n'));
|
|
121
|
+
if (stabilize)
|
|
122
|
+
await waitForFonts(browser);
|
|
123
|
+
}
|
|
41
124
|
// 1. Capture screenshot (base64 PNG → Buffer)
|
|
42
|
-
|
|
125
|
+
let base64;
|
|
126
|
+
try {
|
|
127
|
+
base64 = await browser.takeScreenshot();
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
if (injected)
|
|
131
|
+
await removeCaptureCss(browser);
|
|
132
|
+
}
|
|
43
133
|
if (typeof base64 !== 'string' || base64.length === 0) {
|
|
44
134
|
throw new Error(`testivai.witness("${name}"): browser.takeScreenshot() returned an empty value`);
|
|
45
135
|
}
|
|
@@ -50,9 +140,14 @@ async function witness(browser, name, options = {}) {
|
|
|
50
140
|
try {
|
|
51
141
|
const result = await browser.execute(
|
|
52
142
|
// Inline string form for maximum WDIO sync/async compatibility.
|
|
53
|
-
//
|
|
54
|
-
// the
|
|
55
|
-
|
|
143
|
+
// ignoreSelectors excludes elements from the pixel diff, so they
|
|
144
|
+
// are excluded from the DOM/text signal too (consistent semantic).
|
|
145
|
+
`var clone = document.documentElement.cloneNode(true);` +
|
|
146
|
+
`var sels = ${JSON.stringify(ignoreSelectors)};` +
|
|
147
|
+
`for (var i = 0; i < sels.length; i++) {` +
|
|
148
|
+
` try { clone.querySelectorAll(sels[i]).forEach(function(el){ el.remove(); }); } catch (e) {}` +
|
|
149
|
+
`}` +
|
|
150
|
+
`return clone.outerHTML;`);
|
|
56
151
|
if (typeof result === 'string' && result.length > 0) {
|
|
57
152
|
dom = result;
|
|
58
153
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-webdriverio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "WebdriverIO 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",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@testivai/witness": "1.0
|
|
56
|
+
"@testivai/witness": "1.2.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@types/jest": "^
|
|
59
|
+
"@types/jest": "^30.0.0",
|
|
60
60
|
"@types/node": "^20.0.0",
|
|
61
|
-
"jest": "^
|
|
62
|
-
"ts-jest": "^29.
|
|
63
|
-
"typescript": "^
|
|
61
|
+
"jest": "^30.4.2",
|
|
62
|
+
"ts-jest": "^29.4.11",
|
|
63
|
+
"typescript": "^6.0.3",
|
|
64
64
|
"webdriverio": "^9.0.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|