@testivai/witness-playwright 1.2.1 → 1.3.1

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
@@ -132,7 +132,13 @@ function mergeTestConfig(projectConfig, testConfig) {
132
132
  ...testConfig.performanceMetrics
133
133
  },
134
134
  selectors: testConfig.selectors,
135
- useBrowserCapture: testConfig.useBrowserCapture
135
+ useBrowserCapture: testConfig.useBrowserCapture,
136
+ // Per-call capture options must survive the merge: without these,
137
+ // witness(page, testInfo, name, { ignoreSelectors }) silently ignored
138
+ // the selectors (masked for a long time by the diff engine's cumulated
139
+ // threshold absorbing the tiny leaked pixels).
140
+ ignoreSelectors: testConfig.ignoreSelectors,
141
+ stabilize: testConfig.stabilize
136
142
  };
137
143
  }
138
144
  /**
@@ -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,6 +36,7 @@ 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"));
@@ -43,6 +44,7 @@ const url_1 = require("url");
43
44
  const sharp_1 = __importDefault(require("sharp"));
44
45
  const loader_1 = require("./config/loader");
45
46
  const ignore_selectors_1 = require("./config/ignore-selectors");
47
+ const stabilize_1 = require("./config/stabilize");
46
48
  /**
47
49
  * Generates a safe filename from a URL.
48
50
  * @param pageUrl The URL of the page.
@@ -63,6 +65,27 @@ function getSnapshotNameFromUrl(pageUrl) {
63
65
  return 'snapshot';
64
66
  }
65
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
+ }
66
89
  /**
67
90
  * Captures a snapshot of the page, including a screenshot, DOM, and layout data.
68
91
  * The evidence is stored in a temporary directory for the reporter to process later.
@@ -89,7 +112,7 @@ async function snapshot(page, testInfo, name, config) {
89
112
  }
90
113
  const outputDir = path.join(process.cwd(), '.testivai', 'temp');
91
114
  await fs.ensureDir(outputDir);
92
- const snapshotName = name || getSnapshotNameFromUrl(page.url());
115
+ const snapshotName = effectiveSnapshotName(name || getSnapshotNameFromUrl(page.url()), testInfo);
93
116
  const timestamp = Date.now();
94
117
  const safeName = snapshotName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
95
118
  const baseFilename = `${timestamp}_${safeName}`;
@@ -109,6 +132,20 @@ async function snapshot(page, testInfo, name, config) {
109
132
  ignoreStyleEl = await page.addStyleTag({ content: css });
110
133
  }
111
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)) {
141
+ try {
142
+ stabilizeStyleEl = await page.addStyleTag({ content: stabilize_1.STABILIZE_CSS });
143
+ }
144
+ catch {
145
+ // Style injection can fail on locked-down pages; capture proceeds
146
+ }
147
+ await (0, stabilize_1.waitForFonts)(page);
148
+ }
112
149
  // Check if scroll-and-stitch is explicitly requested (backup method)
113
150
  if (effectiveConfig.useBrowserCapture === false) {
114
151
  // Use scroll-and-stitch approach (backup method)
@@ -330,6 +367,11 @@ async function snapshot(page, testInfo, name, config) {
330
367
  await ignoreStyleEl.evaluate((el) => el.remove()).catch(() => { });
331
368
  ignoreStyleEl = null;
332
369
  }
370
+ // Re-enable animations/transitions after capture
371
+ if (stabilizeStyleEl) {
372
+ await stabilizeStyleEl.evaluate((el) => el.remove()).catch(() => { });
373
+ stabilizeStyleEl = null;
374
+ }
333
375
  // 1.5. Local mode: also place the screenshot in the layout expected by
334
376
  // @testivai/witness/report (subdirectory keyed by snapshot name).
335
377
  // This is what `BaselineStore.listTemp()` and `compareAll()` enumerate.
@@ -344,7 +386,19 @@ async function snapshot(page, testInfo, name, config) {
344
386
  await fs.ensureDir(localSnapshotDir);
345
387
  await fs.copyFile(screenshotPath, path.join(localSnapshotDir, 'screenshot.png'));
346
388
  try {
347
- 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);
348
402
  if (typeof domHtml === 'string' && domHtml.length > 0) {
349
403
  await fs.writeFile(path.join(localSnapshotDir, 'dom.html'), domHtml, 'utf-8');
350
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.1",
3
+ "version": "1.3.1",
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
- }