@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 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
- const base64 = await browser.takeScreenshot();
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
- // The function-form across WDIO 8/9 has subtle typing differences;
54
- // the string form is stable.
55
- 'return document.documentElement.outerHTML;');
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.1.2",
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.4"
56
+ "@testivai/witness": "1.2.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@types/jest": "^29.0.0",
59
+ "@types/jest": "^30.0.0",
60
60
  "@types/node": "^20.0.0",
61
- "jest": "^29.0.0",
62
- "ts-jest": "^29.2.5",
63
- "typescript": "^5.0.0",
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": {