@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 +6 -3
- package/dist/config/loader.js +7 -1
- package/dist/config/stabilize.d.ts +35 -0
- package/dist/config/stabilize.js +112 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +56 -2
- package/dist/types.d.ts +11 -0
- package/package.json +7 -11
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -47
- package/dist/cli/init.d.ts +0 -3
- package/dist/cli/init.js +0 -158
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
|
|
92
|
+
// With per-snapshot overrides
|
|
93
93
|
await snapshot(page, testInfo, 'checkout-page', {
|
|
94
|
-
|
|
95
|
-
|
|
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
|
package/dist/config/loader.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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": "^
|
|
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.
|
|
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
|
|
63
|
-
"ts-jest": "^29.4.
|
|
58
|
+
"jest": "^30.4.2",
|
|
59
|
+
"ts-jest": "^29.4.11",
|
|
64
60
|
"ts-node": "^10.9.0",
|
|
65
|
-
"typescript": "^
|
|
61
|
+
"typescript": "^6.0.3"
|
|
66
62
|
},
|
|
67
63
|
"scripts": {
|
|
68
64
|
"build": "tsc",
|
package/dist/cli/index.d.ts
DELETED
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
|
-
}
|
package/dist/cli/init.d.ts
DELETED
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
|
-
}
|