@testivai/witness-playwright 1.2.0 → 1.3.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
@@ -89,11 +89,14 @@ import { snapshot } from '@testivai/witness-playwright';
89
89
  // Basic usage
90
90
  await snapshot(page, testInfo, 'my-snapshot');
91
91
 
92
- // With custom config
92
+ // With per-snapshot overrides
93
93
  await snapshot(page, testInfo, 'checkout-page', {
94
- threshold: 0.05, // 5% difference tolerance
95
- fullPage: true, // Capture full page
94
+ ignoreSelectors: ['.live-chat', '[data-testid=clock]'], // hidden for this capture
95
+ stabilize: false, // opt out of animation-freeze + font-wait for this snapshot
96
96
  });
97
+
98
+ // Diff tolerances (maxDiffPercent, noiseAutoPass, ...) are project-level:
99
+ // set them in .testivai/config.json — see the repository README.
97
100
  ```
98
101
 
99
102
  ## CI/CD Integration
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Helpers for the ignoreSelectors feature.
3
+ *
4
+ * Three sources, merged with deduplication (last-write-wins dedup via Set):
5
+ * 1. .testivai/config.json — global OSS config (`ignoreSelectors` array)
6
+ * 2. testivai.config.ts/js — power-user project config (`ignoreSelectors`)
7
+ * 3. testivai.witness() call — per-snapshot override (`ignoreSelectors`)
8
+ *
9
+ * These functions are pure (no Playwright dependency) so they can be
10
+ * exercised in plain Jest unit tests without a browser.
11
+ */
12
+ import { TestivAIConfig, TestivAIProjectConfig } from '../types';
13
+ /**
14
+ * Read ignoreSelectors from `.testivai/config.json`.
15
+ * Returns `[]` when the file doesn't exist or is malformed.
16
+ *
17
+ * @param projectRoot Directory that contains the `.testivai/` folder
18
+ */
19
+ export declare function readWitnessConfigSelectors(projectRoot: string): string[];
20
+ /**
21
+ * Collect and deduplicate ignoreSelectors from all three sources.
22
+ *
23
+ * Order of precedence for deduplication: earlier sources win (Set preserves
24
+ * insertion order, so the first occurrence of any selector survives).
25
+ *
26
+ * @param projectRoot Path to the project root (where `.testivai/` lives)
27
+ * @param projectConfig Loaded `testivai.config.ts` project config
28
+ * @param effectiveConfig Merged per-snapshot TestivAIConfig
29
+ */
30
+ export declare function collectIgnoreSelectors(projectRoot: string, projectConfig: TestivAIProjectConfig, effectiveConfig: TestivAIConfig): string[];
31
+ /**
32
+ * Build a `<style>` tag content that hides every given selector via
33
+ * `visibility: hidden !important`.
34
+ *
35
+ * Returns an empty string when the selectors list is empty so callers can
36
+ * guard with `if (css)` before calling `page.addStyleTag`.
37
+ */
38
+ export declare function buildIgnoreSelectorsCSS(selectors: string[]): string;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Helpers for the ignoreSelectors feature.
4
+ *
5
+ * Three sources, merged with deduplication (last-write-wins dedup via Set):
6
+ * 1. .testivai/config.json — global OSS config (`ignoreSelectors` array)
7
+ * 2. testivai.config.ts/js — power-user project config (`ignoreSelectors`)
8
+ * 3. testivai.witness() call — per-snapshot override (`ignoreSelectors`)
9
+ *
10
+ * These functions are pure (no Playwright dependency) so they can be
11
+ * exercised in plain Jest unit tests without a browser.
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.readWitnessConfigSelectors = readWitnessConfigSelectors;
48
+ exports.collectIgnoreSelectors = collectIgnoreSelectors;
49
+ exports.buildIgnoreSelectorsCSS = buildIgnoreSelectorsCSS;
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ /**
53
+ * Read ignoreSelectors from `.testivai/config.json`.
54
+ * Returns `[]` when the file doesn't exist or is malformed.
55
+ *
56
+ * @param projectRoot Directory that contains the `.testivai/` folder
57
+ */
58
+ function readWitnessConfigSelectors(projectRoot) {
59
+ try {
60
+ const configPath = path.join(projectRoot, '.testivai', 'config.json');
61
+ if (!fs.existsSync(configPath))
62
+ return [];
63
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
64
+ return Array.isArray(raw.ignoreSelectors) ? raw.ignoreSelectors : [];
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ /**
71
+ * Collect and deduplicate ignoreSelectors from all three sources.
72
+ *
73
+ * Order of precedence for deduplication: earlier sources win (Set preserves
74
+ * insertion order, so the first occurrence of any selector survives).
75
+ *
76
+ * @param projectRoot Path to the project root (where `.testivai/` lives)
77
+ * @param projectConfig Loaded `testivai.config.ts` project config
78
+ * @param effectiveConfig Merged per-snapshot TestivAIConfig
79
+ */
80
+ function collectIgnoreSelectors(projectRoot, projectConfig, effectiveConfig) {
81
+ const fromWitnessConfig = readWitnessConfigSelectors(projectRoot);
82
+ const fromProjectConfig = projectConfig.ignoreSelectors ?? [];
83
+ const fromSnapshot = effectiveConfig.ignoreSelectors ?? [];
84
+ return [...new Set([...fromWitnessConfig, ...fromProjectConfig, ...fromSnapshot])];
85
+ }
86
+ /**
87
+ * Build a `<style>` tag content that hides every given selector via
88
+ * `visibility: hidden !important`.
89
+ *
90
+ * Returns an empty string when the selectors list is empty so callers can
91
+ * guard with `if (css)` before calling `page.addStyleTag`.
92
+ */
93
+ function buildIgnoreSelectorsCSS(selectors) {
94
+ if (selectors.length === 0)
95
+ return '';
96
+ return selectors.map(s => `${s} { visibility: hidden !important; }`).join('\n');
97
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Capture stabilization — neutralize the top causes of flaky visual diffs
3
+ * before a screenshot is taken:
4
+ *
5
+ * 1. CSS animations & transitions (elements caught mid-motion)
6
+ * 2. The blinking text caret
7
+ * 3. Web fonts still loading (fallback font rendered in the capture)
8
+ * 4. Smooth scrolling still settling
9
+ *
10
+ * Resolution order for the on/off switch (first defined wins):
11
+ * 1. per-snapshot testivai.witness(..., { stabilize })
12
+ * 2. project testivai.config.ts → stabilize
13
+ * 3. global .testivai/config.json → stabilize
14
+ * 4. default true
15
+ */
16
+ import type { Page } from '@playwright/test';
17
+ import { TestivAIConfig, TestivAIProjectConfig } from '../types';
18
+ /**
19
+ * CSS injected for the duration of the capture. Near-zero durations (not
20
+ * `none`) let every animation/transition COMPLETE instantly at its final
21
+ * state — pages whose content starts hidden and reveals via entry
22
+ * animations or class transitions render fully. (`animation: none` would
23
+ * freeze them at the hidden initial state.)
24
+ */
25
+ export declare const STABILIZE_CSS = "*, *::before, *::after {\n animation-duration: 0.001s !important;\n animation-delay: 0s !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.001s !important;\n transition-delay: 0s !important;\n caret-color: transparent !important;\n scroll-behavior: auto !important;\n}";
26
+ /** Read the global `stabilize` flag from `.testivai/config.json`, if set. */
27
+ export declare function readWitnessConfigStabilize(projectRoot: string): boolean | undefined;
28
+ /** Resolve the effective stabilize flag across all three config sources. */
29
+ export declare function resolveStabilize(projectRoot: string, projectConfig: TestivAIProjectConfig, testConfig?: TestivAIConfig): boolean;
30
+ /**
31
+ * Wait for web fonts to finish loading, bounded at 3s so a hanging font
32
+ * request can never stall the capture. Best-effort: errors are swallowed —
33
+ * a missing FontFaceSet API just means no wait.
34
+ */
35
+ export declare function waitForFonts(page: Page): Promise<void>;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * Capture stabilization — neutralize the top causes of flaky visual diffs
4
+ * before a screenshot is taken:
5
+ *
6
+ * 1. CSS animations & transitions (elements caught mid-motion)
7
+ * 2. The blinking text caret
8
+ * 3. Web fonts still loading (fallback font rendered in the capture)
9
+ * 4. Smooth scrolling still settling
10
+ *
11
+ * Resolution order for the on/off switch (first defined wins):
12
+ * 1. per-snapshot testivai.witness(..., { stabilize })
13
+ * 2. project testivai.config.ts → stabilize
14
+ * 3. global .testivai/config.json → stabilize
15
+ * 4. default true
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.STABILIZE_CSS = void 0;
52
+ exports.readWitnessConfigStabilize = readWitnessConfigStabilize;
53
+ exports.resolveStabilize = resolveStabilize;
54
+ exports.waitForFonts = waitForFonts;
55
+ const fs = __importStar(require("fs"));
56
+ const path = __importStar(require("path"));
57
+ /**
58
+ * CSS injected for the duration of the capture. Near-zero durations (not
59
+ * `none`) let every animation/transition COMPLETE instantly at its final
60
+ * state — pages whose content starts hidden and reveals via entry
61
+ * animations or class transitions render fully. (`animation: none` would
62
+ * freeze them at the hidden initial state.)
63
+ */
64
+ exports.STABILIZE_CSS = `*, *::before, *::after {
65
+ animation-duration: 0.001s !important;
66
+ animation-delay: 0s !important;
67
+ animation-iteration-count: 1 !important;
68
+ transition-duration: 0.001s !important;
69
+ transition-delay: 0s !important;
70
+ caret-color: transparent !important;
71
+ scroll-behavior: auto !important;
72
+ }`;
73
+ /** Read the global `stabilize` flag from `.testivai/config.json`, if set. */
74
+ function readWitnessConfigStabilize(projectRoot) {
75
+ try {
76
+ const configPath = path.join(projectRoot, '.testivai', 'config.json');
77
+ if (!fs.existsSync(configPath))
78
+ return undefined;
79
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
80
+ return typeof raw.stabilize === 'boolean' ? raw.stabilize : undefined;
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ /** Resolve the effective stabilize flag across all three config sources. */
87
+ function resolveStabilize(projectRoot, projectConfig, testConfig) {
88
+ if (typeof testConfig?.stabilize === 'boolean')
89
+ return testConfig.stabilize;
90
+ if (typeof projectConfig.stabilize === 'boolean')
91
+ return projectConfig.stabilize;
92
+ const fromWitnessConfig = readWitnessConfigStabilize(projectRoot);
93
+ if (typeof fromWitnessConfig === 'boolean')
94
+ return fromWitnessConfig;
95
+ return true;
96
+ }
97
+ /**
98
+ * Wait for web fonts to finish loading, bounded at 3s so a hanging font
99
+ * request can never stall the capture. Best-effort: errors are swallowed —
100
+ * a missing FontFaceSet API just means no wait.
101
+ */
102
+ async function waitForFonts(page) {
103
+ try {
104
+ await page.evaluate(() => Promise.race([
105
+ document.fonts?.ready,
106
+ new Promise((resolve) => setTimeout(resolve, 3000)),
107
+ ]));
108
+ }
109
+ catch {
110
+ // Page navigated or evaluate failed — capture proceeds unstabilized fonts
111
+ }
112
+ }
@@ -1,5 +1,17 @@
1
1
  import { Page, TestInfo } from '@playwright/test';
2
2
  import { TestivAIConfig } from './types';
3
+ /**
4
+ * Compute the effective snapshot name, folding in a project variant when the
5
+ * Playwright config runs multiple projects. Without this, two projects (e.g.
6
+ * chromium-desktop and mobile-safari) capturing the same snapshot name would
7
+ * overwrite each other's baselines under .testivai/baselines/<name>/.
8
+ *
9
+ * Single-project configs are untouched: 'homepage' stays 'homepage'.
10
+ * Multi-project configs get 'homepage__chromium', 'homepage__mobile-safari'.
11
+ * The variant lives in the NAME, so the on-disk layout, results.json schema,
12
+ * report, approve CLI, and PR-comment approvals all work unchanged.
13
+ */
14
+ export declare function effectiveSnapshotName(baseName: string, testInfo: TestInfo): string;
3
15
  /**
4
16
  * Captures a snapshot of the page, including a screenshot, DOM, and layout data.
5
17
  * The evidence is stored in a temporary directory for the reporter to process later.
package/dist/snapshot.js CHANGED
@@ -36,12 +36,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.effectiveSnapshotName = effectiveSnapshotName;
39
40
  exports.snapshot = snapshot;
40
41
  const fs = __importStar(require("fs-extra"));
41
42
  const path = __importStar(require("path"));
42
43
  const url_1 = require("url");
43
44
  const sharp_1 = __importDefault(require("sharp"));
44
45
  const loader_1 = require("./config/loader");
46
+ const ignore_selectors_1 = require("./config/ignore-selectors");
47
+ const stabilize_1 = require("./config/stabilize");
45
48
  /**
46
49
  * Generates a safe filename from a URL.
47
50
  * @param pageUrl The URL of the page.
@@ -62,6 +65,27 @@ function getSnapshotNameFromUrl(pageUrl) {
62
65
  return 'snapshot';
63
66
  }
64
67
  }
68
+ /**
69
+ * Compute the effective snapshot name, folding in a project variant when the
70
+ * Playwright config runs multiple projects. Without this, two projects (e.g.
71
+ * chromium-desktop and mobile-safari) capturing the same snapshot name would
72
+ * overwrite each other's baselines under .testivai/baselines/<name>/.
73
+ *
74
+ * Single-project configs are untouched: 'homepage' stays 'homepage'.
75
+ * Multi-project configs get 'homepage__chromium', 'homepage__mobile-safari'.
76
+ * The variant lives in the NAME, so the on-disk layout, results.json schema,
77
+ * report, approve CLI, and PR-comment approvals all work unchanged.
78
+ */
79
+ function effectiveSnapshotName(baseName, testInfo) {
80
+ const projects = testInfo?.config?.projects;
81
+ const projectName = testInfo?.project?.name;
82
+ if (!Array.isArray(projects) || projects.length <= 1)
83
+ return baseName;
84
+ if (typeof projectName !== 'string' || projectName.length === 0)
85
+ return baseName;
86
+ const safeVariant = projectName.replace(/[^a-z0-9_-]+/gi, '_').toLowerCase();
87
+ return `${baseName}__${safeVariant}`;
88
+ }
65
89
  /**
66
90
  * Captures a snapshot of the page, including a screenshot, DOM, and layout data.
67
91
  * The evidence is stored in a temporary directory for the reporter to process later.
@@ -88,7 +112,7 @@ async function snapshot(page, testInfo, name, config) {
88
112
  }
89
113
  const outputDir = path.join(process.cwd(), '.testivai', 'temp');
90
114
  await fs.ensureDir(outputDir);
91
- const snapshotName = name || getSnapshotNameFromUrl(page.url());
115
+ const snapshotName = effectiveSnapshotName(name || getSnapshotNameFromUrl(page.url()), testInfo);
92
116
  const timestamp = Date.now();
93
117
  const safeName = snapshotName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
94
118
  const baseFilename = `${timestamp}_${safeName}`;
@@ -96,29 +120,31 @@ async function snapshot(page, testInfo, name, config) {
96
120
  const screenshotPath = path.join(outputDir, `${baseFilename}.png`);
97
121
  // In local mode: hide elements matching ignoreSelectors before the screenshot
98
122
  // so dynamic content (version badges, timestamps, ads) doesn't cause false diffs.
99
- // Sources (merged, deduped):
123
+ // Sources (merged, deduped) — see config/ignore-selectors.ts:
100
124
  // 1. .testivai/config.json → ignoreSelectors (global, OSS config)
101
125
  // 2. testivai.config.ts → ignoreSelectors (global, power users)
102
126
  // 3. testivai.witness(...) → { ignoreSelectors } (per-snapshot override)
103
127
  let ignoreStyleEl = null;
104
128
  if (isLocalMode) {
105
- // Read global ignores from .testivai/config.json
106
- let witnessConfigIgnore = [];
129
+ const allSelectors = (0, ignore_selectors_1.collectIgnoreSelectors)(process.cwd(), projectConfig, effectiveConfig);
130
+ const css = (0, ignore_selectors_1.buildIgnoreSelectorsCSS)(allSelectors);
131
+ if (css) {
132
+ ignoreStyleEl = await page.addStyleTag({ content: css });
133
+ }
134
+ }
135
+ // Stabilize the page before capture (all modes): freeze CSS animations and
136
+ // transitions, hide the caret, and wait for web fonts — the top sources of
137
+ // flaky pixel diffs. Injected as CSS so every capture path (native
138
+ // screenshot, CDP captureScreenshot, scroll-and-stitch) is covered.
139
+ let stabilizeStyleEl = null;
140
+ if ((0, stabilize_1.resolveStabilize)(process.cwd(), projectConfig, config)) {
107
141
  try {
108
- const witnessConfigPath = path.join(process.cwd(), '.testivai', 'config.json');
109
- if (fs.existsSync(witnessConfigPath)) {
110
- const raw = JSON.parse(fs.readFileSync(witnessConfigPath, 'utf-8'));
111
- witnessConfigIgnore = raw.ignoreSelectors ?? [];
112
- }
142
+ stabilizeStyleEl = await page.addStyleTag({ content: stabilize_1.STABILIZE_CSS });
113
143
  }
114
- catch { /* ignore malformed config */ }
115
- const playwrightConfigIgnore = projectConfig.ignoreSelectors ?? [];
116
- const perSnapshotIgnore = effectiveConfig.ignoreSelectors ?? [];
117
- const allSelectors = [...new Set([...witnessConfigIgnore, ...playwrightConfigIgnore, ...perSnapshotIgnore])];
118
- if (allSelectors.length > 0) {
119
- const css = allSelectors.map(s => `${s} { visibility: hidden !important; }`).join('\n');
120
- ignoreStyleEl = await page.addStyleTag({ content: css });
144
+ catch {
145
+ // Style injection can fail on locked-down pages; capture proceeds
121
146
  }
147
+ await (0, stabilize_1.waitForFonts)(page);
122
148
  }
123
149
  // Check if scroll-and-stitch is explicitly requested (backup method)
124
150
  if (effectiveConfig.useBrowserCapture === false) {
@@ -341,6 +367,11 @@ async function snapshot(page, testInfo, name, config) {
341
367
  await ignoreStyleEl.evaluate((el) => el.remove()).catch(() => { });
342
368
  ignoreStyleEl = null;
343
369
  }
370
+ // Re-enable animations/transitions after capture
371
+ if (stabilizeStyleEl) {
372
+ await stabilizeStyleEl.evaluate((el) => el.remove()).catch(() => { });
373
+ stabilizeStyleEl = null;
374
+ }
344
375
  // 1.5. Local mode: also place the screenshot in the layout expected by
345
376
  // @testivai/witness/report (subdirectory keyed by snapshot name).
346
377
  // This is what `BaselineStore.listTemp()` and `compareAll()` enumerate.
@@ -355,7 +386,19 @@ async function snapshot(page, testInfo, name, config) {
355
386
  await fs.ensureDir(localSnapshotDir);
356
387
  await fs.copyFile(screenshotPath, path.join(localSnapshotDir, 'screenshot.png'));
357
388
  try {
358
- const domHtml = await page.evaluate(() => document.documentElement.outerHTML);
389
+ const domIgnoreSelectors = (0, ignore_selectors_1.collectIgnoreSelectors)(process.cwd(), projectConfig, effectiveConfig);
390
+ const domHtml = await page.evaluate((selectors) => {
391
+ // ignoreSelectors excludes elements from the pixel diff, so they
392
+ // are excluded from the DOM/text signal too (consistent semantic)
393
+ const clone = document.documentElement.cloneNode(true);
394
+ for (const sel of selectors) {
395
+ try {
396
+ clone.querySelectorAll(sel).forEach((el) => el.remove());
397
+ }
398
+ catch { }
399
+ }
400
+ return clone.outerHTML;
401
+ }, domIgnoreSelectors);
359
402
  if (typeof domHtml === 'string' && domHtml.length > 0) {
360
403
  await fs.writeFile(path.join(localSnapshotDir, 'dom.html'), domHtml, 'utf-8');
361
404
  }
package/dist/types.d.ts CHANGED
@@ -196,6 +196,8 @@ export interface TestivAIProjectConfig {
196
196
  structure?: StructureAnalysisConfig;
197
197
  /** Environment-specific overrides (optional) */
198
198
  environments?: EnvironmentConfig;
199
+ /** Stabilize captures (disable animations, hide caret, wait for fonts). Default: true. */
200
+ stabilize?: boolean;
199
201
  }
200
202
  /**
201
203
  * Per-test configuration overrides
@@ -228,6 +230,15 @@ export interface TestivAIConfig {
228
230
  * Example: ["[data-testivai-ignore]", ".version-badge", "#live-chat-widget"]
229
231
  */
230
232
  ignoreSelectors?: string[];
233
+ /**
234
+ * Stabilize the page before capture: disable CSS animations/transitions,
235
+ * hide the text caret, and wait for web fonts to finish loading — the top
236
+ * causes of flaky visual diffs. Default: true.
237
+ *
238
+ * Can also be set globally in `.testivai/config.json` under `stabilize`
239
+ * or in `testivai.config.ts`. Per-snapshot values win.
240
+ */
241
+ stabilize?: boolean;
231
242
  }
232
243
  /**
233
244
  * Layout/Bounding box data for an element
package/package.json CHANGED
@@ -1,16 +1,12 @@
1
1
  {
2
2
  "name": "@testivai/witness-playwright",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Playwright sensor for Testivai Visual Regression Test system",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
- "bin": {
8
- "testivai": "dist/cli/index.js"
9
- },
10
7
  "exports": {
11
8
  ".": "./dist/index.js",
12
- "./reporter": "./dist/reporter-entry.js",
13
- "./cli": "./dist/cli/index.js"
9
+ "./reporter": "./dist/reporter-entry.js"
14
10
  },
15
11
  "keywords": [
16
12
  "playwright",
@@ -46,23 +42,23 @@
46
42
  "dependencies": {
47
43
  "axios": "^1.16.1",
48
44
  "chalk": "^4.1.2",
49
- "commander": "^11.0.0",
45
+ "commander": "^14.0.3",
50
46
  "cross-fetch": "^4.0.0",
51
47
  "fs-extra": "^11.3.5",
52
48
  "sharp": "^0.34.5",
53
49
  "simple-git": "^3.21.0",
54
50
  "@testivai/common": "0.2.2",
55
- "@testivai/witness": "1.1.0"
51
+ "@testivai/witness": "1.2.0"
56
52
  },
57
53
  "devDependencies": {
58
54
  "@playwright/test": "^1.60.0",
59
55
  "@types/fs-extra": "^11.0.4",
60
56
  "@types/jest": "^30.0.0",
61
57
  "@types/node": "^20.10.0",
62
- "jest": "^30.2.0",
63
- "ts-jest": "^29.4.5",
58
+ "jest": "^30.4.2",
59
+ "ts-jest": "^29.4.11",
64
60
  "ts-node": "^10.9.0",
65
- "typescript": "^5.3.0"
61
+ "typescript": "^6.0.3"
66
62
  },
67
63
  "scripts": {
68
64
  "build": "tsc",
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/cli/index.js DELETED
@@ -1,47 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- const commander_1 = require("commander");
8
- const chalk_1 = __importDefault(require("chalk"));
9
- const init_1 = require("./init");
10
- const program = new commander_1.Command();
11
- // Display banner
12
- const showBanner = () => {
13
- console.log();
14
- console.log(chalk_1.default.cyan.bold(' TestivAI'));
15
- console.log(chalk_1.default.gray(' Catch Visual Bugs Automatically'));
16
- console.log(chalk_1.default.gray(' AI that catches real bugs, ignores the noise.'));
17
- console.log();
18
- };
19
- program
20
- .name('testivai')
21
- .description('TestivAI Playwright SDK CLI')
22
- .version(require('../../package.json').version)
23
- .hook('preAction', () => {
24
- if (!process.argv.includes('--quiet') && !process.argv.includes('-q')) {
25
- showBanner();
26
- }
27
- });
28
- // Global options
29
- program
30
- .option('-q, --quiet', 'Suppress banner output (ideal for CI)');
31
- program
32
- .command('init')
33
- .description('Initialize TestivAI configuration file')
34
- .action(async () => {
35
- try {
36
- await (0, init_1.createConfigFile)();
37
- }
38
- catch (error) {
39
- console.error('❌ Failed to initialize:', error);
40
- process.exit(1);
41
- }
42
- });
43
- program.parse(process.argv);
44
- // Show help if no command provided
45
- if (!process.argv.slice(2).length) {
46
- program.outputHelp();
47
- }
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- declare function createConfigFile(): Promise<void>;
3
- export { createConfigFile };
package/dist/cli/init.js DELETED
@@ -1,158 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.createConfigFile = createConfigFile;
38
- const fs = __importStar(require("fs-extra"));
39
- const path = __importStar(require("path"));
40
- /**
41
- * Basic CLI init script for TestivAI Playwright SDK
42
- * Creates a testivai.config.ts file with sensible defaults
43
- */
44
- const DEFAULT_CONFIG = `/**
45
- * TestivAI Configuration File
46
- *
47
- * This file configures how TestivAI analyzes visual differences in your tests.
48
- *
49
- * Layout Settings:
50
- * - sensitivity: 0-4 scale (0=strict/precise, 4=very lenient)
51
- * - tolerance: Base pixel tolerance for layout differences
52
- *
53
- * AI Settings:
54
- * - sensitivity: 0-4 scale (0=conservative, 4=aggressive)
55
- * - confidence: 0.0-1.0 scale (minimum confidence required for AI_BUG verdict)
56
- *
57
- * Custom Configuration Examples:
58
- *
59
- * // For strict testing (critical components):
60
- * export default {
61
- * layout: { sensitivity: 0, tolerance: 0.5 },
62
- * ai: { sensitivity: 0, confidence: 0.9 }
63
- * };
64
- *
65
- * // For lenient testing (dynamic content):
66
- * export default {
67
- * layout: { sensitivity: 4, tolerance: 3.0 },
68
- * ai: { sensitivity: 3, confidence: 0.6 }
69
- * };
70
- *
71
- * // Per-selector tolerances (advanced):
72
- * export default {
73
- * layout: {
74
- * sensitivity: 2,
75
- * tolerance: 1.0,
76
- * selectorTolerances: {
77
- * '.carousel': 4.0, // Carousel components shift
78
- * '.dropdown': 2.0, // Dropdowns can vary
79
- * '.tooltip': 5.0, // Tooltips are highly variable
80
- * '.submit-button': 0.5 // Critical buttons need precision
81
- * }
82
- * },
83
- * ai: { sensitivity: 2, confidence: 0.7 }
84
- * };
85
- *
86
- * // Environment-specific overrides:
87
- * export default {
88
- * layout: { sensitivity: 2, tolerance: 1.0 },
89
- * ai: { sensitivity: 2, confidence: 0.7 },
90
- * environments: {
91
- * ci: {
92
- * layout: { sensitivity: 1, tolerance: 0.5 }, // Stricter in CI
93
- * ai: { sensitivity: 1, confidence: 0.8 }
94
- * },
95
- * development: {
96
- * layout: { sensitivity: 3, tolerance: 2.0 }, // More lenient locally
97
- * ai: { sensitivity: 3, confidence: 0.6 }
98
- * }
99
- * }
100
- * };
101
- */
102
-
103
- export default {
104
- // Your TestivAI API key - get this from the dashboard after creating a project
105
- apiKey: process.env.TESTIVAI_API_KEY || '',
106
-
107
- // API endpoint - automatically points to production
108
- apiUrl: process.env.TESTIVAI_API_URL || 'https://core-api.testiv.ai',
109
-
110
- layout: {
111
- sensitivity: 2, // Balanced sensitivity (0-4 scale)
112
- tolerance: 1.0, // 1 pixel base tolerance
113
- },
114
- ai: {
115
- sensitivity: 2, // Balanced AI analysis (0-4 scale)
116
- confidence: 0.7, // 70% confidence required for AI_BUG
117
- }
118
- };
119
- `;
120
- async function createConfigFile() {
121
- const configPath = path.join(process.cwd(), 'testivai.config.ts');
122
- // Check if config already exists
123
- if (await fs.pathExists(configPath)) {
124
- console.log('❌ TestivAI config already exists at:', configPath);
125
- console.log(' Delete it first if you want to regenerate it.');
126
- process.exit(1);
127
- }
128
- try {
129
- // Create the config file
130
- await fs.writeFile(configPath, DEFAULT_CONFIG, 'utf8');
131
- console.log('✅ TestivAI configuration created successfully!');
132
- console.log('📁 Config file:', configPath);
133
- console.log('');
134
- console.log('📖 Next steps:');
135
- console.log(' 1. Set up your API key:');
136
- console.log(' TESTIVAI_API_KEY=tstvai-your-key-here');
137
- console.log('');
138
- console.log(' 2. Update your playwright.config.ts to use TestivAI reporter:');
139
- console.log(' reporter: [[\'@testivai/witness-playwright/reporter\']]');
140
- console.log('');
141
- console.log(' 3. Review and customize testivai.config.ts');
142
- console.log(' 4. Run your tests: npx playwright test');
143
- console.log('');
144
- console.log('💡 Get your API key from: https://dashboard.testiv.ai');
145
- console.log('💡 The SDK automatically connects to the production API - no URL configuration needed!');
146
- }
147
- catch (error) {
148
- console.error('❌ Failed to create configuration file:', error);
149
- process.exit(1);
150
- }
151
- }
152
- // Main execution
153
- if (require.main === module) {
154
- createConfigFile().catch(error => {
155
- console.error('❌ CLI init failed:', error);
156
- process.exit(1);
157
- });
158
- }