@testivai/witness-playwright 1.9.0 → 2.0.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 +2 -2
- package/dist/config/ignore-selectors.d.ts +1 -1
- package/dist/config/loader.d.ts +5 -18
- package/dist/config/loader.js +18 -160
- package/dist/config/stabilize.d.ts +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +0 -4
- package/dist/reporter.d.ts +2 -12
- package/dist/reporter.js +34 -231
- package/dist/snapshot.d.ts +2 -2
- package/dist/snapshot.js +10 -321
- package/dist/types.d.ts +10 -357
- package/dist/types.js +1 -6
- package/package.json +9 -15
- package/dist/ci.d.ts +0 -30
- package/dist/ci.js +0 -117
- package/dist/mode.d.ts +0 -21
- package/dist/mode.js +0 -84
- package/dist/structureAnalyzer.d.ts +0 -12
- package/dist/structureAnalyzer.js +0 -288
package/README.md
CHANGED
|
@@ -113,6 +113,6 @@ MIT
|
|
|
113
113
|
|
|
114
114
|
## Support
|
|
115
115
|
|
|
116
|
-
- Documentation: https://github.com/
|
|
117
|
-
- Issues: https://github.com/
|
|
116
|
+
- Documentation: https://github.com/testivai/testivai-oss/tree/main/packages/playwright
|
|
117
|
+
- Issues: https://github.com/testivai/testivai-oss/issues
|
|
118
118
|
- Website: https://testiv.ai
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* These functions are pure (no Playwright dependency) so they can be
|
|
15
15
|
* exercised in plain Jest unit tests without a browser.
|
|
16
16
|
*/
|
|
17
|
-
import { TestivAIConfig, TestivAIProjectConfig, IgnoreMode, IgnoreSelectorInput } from '../types';
|
|
17
|
+
import type { TestivAIConfig, TestivAIProjectConfig, IgnoreMode, IgnoreSelectorInput } from '../types';
|
|
18
18
|
/** A fully-resolved ignore entry with an explicit mode. */
|
|
19
19
|
export interface IgnoreRule {
|
|
20
20
|
selector: string;
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TestivAIProjectConfig, TestivAIConfig } from '../types';
|
|
1
|
+
import type { TestivAIProjectConfig, TestivAIConfig } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Load TestivAI configuration from file system
|
|
4
4
|
* Supports both .ts and .js config files
|
|
@@ -7,23 +7,10 @@ import { TestivAIProjectConfig, TestivAIConfig } from '../types';
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function loadConfig(): Promise<TestivAIProjectConfig>;
|
|
9
9
|
/**
|
|
10
|
-
* Merge per-
|
|
10
|
+
* Merge per-snapshot configuration with project configuration
|
|
11
11
|
*
|
|
12
12
|
* @param projectConfig The project-level configuration
|
|
13
|
-
* @param testConfig Optional per-
|
|
14
|
-
* @returns TestivAIConfig The effective configuration for this
|
|
13
|
+
* @param testConfig Optional per-snapshot configuration overrides
|
|
14
|
+
* @returns TestivAIConfig The effective configuration for this snapshot
|
|
15
15
|
*/
|
|
16
|
-
export declare function mergeTestConfig(
|
|
17
|
-
/**
|
|
18
|
-
* Detect current environment (CI, development, production)
|
|
19
|
-
*
|
|
20
|
-
* @returns string The detected environment
|
|
21
|
-
*/
|
|
22
|
-
export declare function detectEnvironment(): string;
|
|
23
|
-
/**
|
|
24
|
-
* Apply environment-specific overrides to configuration
|
|
25
|
-
*
|
|
26
|
-
* @param config The base configuration
|
|
27
|
-
* @returns TestivAIProjectConfig Configuration with environment overrides applied
|
|
28
|
-
*/
|
|
29
|
-
export declare function applyEnvironmentOverrides(config: TestivAIProjectConfig): TestivAIProjectConfig;
|
|
16
|
+
export declare function mergeTestConfig(_projectConfig: TestivAIProjectConfig, testConfig?: TestivAIConfig): TestivAIConfig;
|
package/dist/config/loader.js
CHANGED
|
@@ -35,23 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.loadConfig = loadConfig;
|
|
37
37
|
exports.mergeTestConfig = mergeTestConfig;
|
|
38
|
-
exports.detectEnvironment = detectEnvironment;
|
|
39
|
-
exports.applyEnvironmentOverrides = applyEnvironmentOverrides;
|
|
40
38
|
const fs = __importStar(require("fs-extra"));
|
|
41
39
|
const path = __importStar(require("path"));
|
|
42
40
|
/**
|
|
43
41
|
* Default configuration when no config file is found
|
|
44
42
|
*/
|
|
45
|
-
const DEFAULT_CONFIG = {
|
|
46
|
-
layout: {
|
|
47
|
-
sensitivity: 2, // Balanced sensitivity (0-4 scale)
|
|
48
|
-
tolerance: 1.0, // 1 pixel base tolerance
|
|
49
|
-
},
|
|
50
|
-
ai: {
|
|
51
|
-
sensitivity: 2, // Balanced AI analysis (0-4 scale)
|
|
52
|
-
confidence: 0.7, // 70% confidence required for AI_BUG
|
|
53
|
-
}
|
|
54
|
-
};
|
|
43
|
+
const DEFAULT_CONFIG = {};
|
|
55
44
|
/**
|
|
56
45
|
* Load TestivAI configuration from file system
|
|
57
46
|
* Supports both .ts and .js config files
|
|
@@ -73,7 +62,7 @@ async function loadConfig() {
|
|
|
73
62
|
configPath = jsConfigPath;
|
|
74
63
|
}
|
|
75
64
|
else {
|
|
76
|
-
|
|
65
|
+
// No config file is the normal zero-config case — silently use defaults
|
|
77
66
|
return DEFAULT_CONFIG;
|
|
78
67
|
}
|
|
79
68
|
// Load configuration based on file type
|
|
@@ -88,171 +77,40 @@ async function loadConfig() {
|
|
|
88
77
|
configModule = await Promise.resolve(`${configPath}`).then(s => __importStar(require(s)));
|
|
89
78
|
}
|
|
90
79
|
const config = configModule.default || configModule;
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
if (!config) {
|
|
81
|
+
return DEFAULT_CONFIG;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ignoreSelectors: config.ignoreSelectors,
|
|
85
|
+
stabilize: config.stabilize,
|
|
86
|
+
};
|
|
93
87
|
}
|
|
94
88
|
catch (error) {
|
|
95
|
-
console.warn('
|
|
89
|
+
console.warn('Warning: failed to load testivai config, using defaults:', error);
|
|
96
90
|
return DEFAULT_CONFIG;
|
|
97
91
|
}
|
|
98
92
|
}
|
|
99
93
|
/**
|
|
100
|
-
* Merge per-
|
|
94
|
+
* Merge per-snapshot configuration with project configuration
|
|
101
95
|
*
|
|
102
96
|
* @param projectConfig The project-level configuration
|
|
103
|
-
* @param testConfig Optional per-
|
|
104
|
-
* @returns TestivAIConfig The effective configuration for this
|
|
97
|
+
* @param testConfig Optional per-snapshot configuration overrides
|
|
98
|
+
* @returns TestivAIConfig The effective configuration for this snapshot
|
|
105
99
|
*/
|
|
106
|
-
function mergeTestConfig(
|
|
100
|
+
function mergeTestConfig(_projectConfig, testConfig) {
|
|
107
101
|
if (!testConfig) {
|
|
108
|
-
return {
|
|
109
|
-
layout: projectConfig.layout,
|
|
110
|
-
ai: projectConfig.ai,
|
|
111
|
-
// @renamed: dom → structure (IP protection)
|
|
112
|
-
structure: projectConfig.structure,
|
|
113
|
-
performanceMetrics: projectConfig.performanceMetrics
|
|
114
|
-
};
|
|
102
|
+
return {};
|
|
115
103
|
}
|
|
116
104
|
return {
|
|
117
|
-
layout: {
|
|
118
|
-
...projectConfig.layout,
|
|
119
|
-
...testConfig.layout
|
|
120
|
-
},
|
|
121
|
-
ai: {
|
|
122
|
-
...projectConfig.ai,
|
|
123
|
-
...testConfig.ai
|
|
124
|
-
},
|
|
125
|
-
// @renamed: dom → structure (IP protection)
|
|
126
|
-
structure: {
|
|
127
|
-
...projectConfig.structure,
|
|
128
|
-
...testConfig.structure
|
|
129
|
-
},
|
|
130
|
-
performanceMetrics: {
|
|
131
|
-
...projectConfig.performanceMetrics,
|
|
132
|
-
...testConfig.performanceMetrics
|
|
133
|
-
},
|
|
134
|
-
selectors: testConfig.selectors,
|
|
135
105
|
useBrowserCapture: testConfig.useBrowserCapture,
|
|
136
106
|
// Per-call capture options must survive the merge: without these,
|
|
137
107
|
// witness(page, testInfo, name, { ignoreSelectors }) silently ignored
|
|
138
108
|
// the selectors (masked for a long time by the diff engine's cumulated
|
|
139
109
|
// threshold absorbing the tiny leaked pixels).
|
|
110
|
+
// Project-level ignoreSelectors and stabilize are read separately
|
|
111
|
+
// (collectIgnoreRules / resolveStabilize) so they are NOT merged here.
|
|
140
112
|
ignoreSelectors: testConfig.ignoreSelectors,
|
|
141
113
|
stabilize: testConfig.stabilize,
|
|
142
|
-
mask: testConfig.mask
|
|
114
|
+
mask: testConfig.mask,
|
|
143
115
|
};
|
|
144
116
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Detect current environment (CI, development, production)
|
|
147
|
-
*
|
|
148
|
-
* @returns string The detected environment
|
|
149
|
-
*/
|
|
150
|
-
function detectEnvironment() {
|
|
151
|
-
// Check for common CI environment variables
|
|
152
|
-
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
|
|
153
|
-
return 'ci';
|
|
154
|
-
}
|
|
155
|
-
// Check for production indicators
|
|
156
|
-
if (process.env.NODE_ENV === 'production') {
|
|
157
|
-
return 'production';
|
|
158
|
-
}
|
|
159
|
-
// Default to development
|
|
160
|
-
return 'development';
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Apply environment-specific overrides to configuration
|
|
164
|
-
*
|
|
165
|
-
* @param config The base configuration
|
|
166
|
-
* @returns TestivAIProjectConfig Configuration with environment overrides applied
|
|
167
|
-
*/
|
|
168
|
-
function applyEnvironmentOverrides(config) {
|
|
169
|
-
const environment = detectEnvironment();
|
|
170
|
-
if (!config.environments) {
|
|
171
|
-
return config;
|
|
172
|
-
}
|
|
173
|
-
// Type-safe environment override access
|
|
174
|
-
let envOverrides;
|
|
175
|
-
switch (environment) {
|
|
176
|
-
case 'ci':
|
|
177
|
-
envOverrides = config.environments.ci;
|
|
178
|
-
break;
|
|
179
|
-
case 'development':
|
|
180
|
-
envOverrides = config.environments.development;
|
|
181
|
-
break;
|
|
182
|
-
case 'production':
|
|
183
|
-
envOverrides = config.environments.production;
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
if (!envOverrides) {
|
|
187
|
-
return config;
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
layout: {
|
|
191
|
-
...config.layout,
|
|
192
|
-
...envOverrides.layout
|
|
193
|
-
},
|
|
194
|
-
ai: {
|
|
195
|
-
...config.ai,
|
|
196
|
-
...envOverrides.ai
|
|
197
|
-
},
|
|
198
|
-
environments: config.environments // Keep the original environments config
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Validate configuration values and merge with defaults
|
|
203
|
-
*
|
|
204
|
-
* @param config The configuration to validate
|
|
205
|
-
* @returns TestivAIProjectConfig Validated configuration
|
|
206
|
-
*/
|
|
207
|
-
function validateAndMergeConfig(config) {
|
|
208
|
-
// Guard against null/undefined config
|
|
209
|
-
if (!config) {
|
|
210
|
-
console.warn('⚠️ Config is null or undefined, using defaults');
|
|
211
|
-
return DEFAULT_CONFIG;
|
|
212
|
-
}
|
|
213
|
-
// Basic validation
|
|
214
|
-
const validatedConfig = {
|
|
215
|
-
// API configuration with production default
|
|
216
|
-
apiKey: config.apiKey,
|
|
217
|
-
apiUrl: config.apiUrl || 'https://core-api.testiv.ai',
|
|
218
|
-
layout: {
|
|
219
|
-
sensitivity: validateRange(config.layout?.sensitivity ?? DEFAULT_CONFIG.layout.sensitivity, 0, 4, 'layout.sensitivity', DEFAULT_CONFIG.layout.sensitivity),
|
|
220
|
-
tolerance: validateRange(config.layout?.tolerance ?? DEFAULT_CONFIG.layout.tolerance, 0, 100, 'layout.tolerance', DEFAULT_CONFIG.layout.tolerance),
|
|
221
|
-
selectorTolerances: config.layout?.selectorTolerances,
|
|
222
|
-
useRelativeTolerance: config.layout?.useRelativeTolerance,
|
|
223
|
-
relativeTolerance: config.layout?.relativeTolerance
|
|
224
|
-
},
|
|
225
|
-
ai: {
|
|
226
|
-
sensitivity: validateRange(config.ai?.sensitivity ?? DEFAULT_CONFIG.ai.sensitivity, 0, 4, 'ai.sensitivity', DEFAULT_CONFIG.ai.sensitivity),
|
|
227
|
-
confidence: validateRange(config.ai?.confidence ?? DEFAULT_CONFIG.ai.confidence, 0, 1, 'ai.confidence', DEFAULT_CONFIG.ai.confidence),
|
|
228
|
-
enableReasoning: config.ai?.enableReasoning
|
|
229
|
-
},
|
|
230
|
-
// @renamed: dom → structure (IP protection)
|
|
231
|
-
structure: config.structure, // Pass through structure analysis configuration as-is
|
|
232
|
-
performanceMetrics: config.performanceMetrics, // Pass through performance metrics configuration as-is
|
|
233
|
-
environments: config.environments
|
|
234
|
-
};
|
|
235
|
-
return applyEnvironmentOverrides(validatedConfig);
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Validate that a number is within the expected range
|
|
239
|
-
*
|
|
240
|
-
* @param value The value to validate
|
|
241
|
-
* @param min Minimum allowed value
|
|
242
|
-
* @param max Maximum allowed value
|
|
243
|
-
* @param field Field name for error messages
|
|
244
|
-
* @param defaultValue Default value to use when validation fails
|
|
245
|
-
* @returns number The validated value
|
|
246
|
-
*/
|
|
247
|
-
function validateRange(value, min, max, field, defaultValue) {
|
|
248
|
-
const numValue = Number(value);
|
|
249
|
-
if (isNaN(numValue)) {
|
|
250
|
-
console.warn(`⚠️ Invalid ${field}: ${value}, using default: ${defaultValue}`);
|
|
251
|
-
return defaultValue;
|
|
252
|
-
}
|
|
253
|
-
if (numValue < min || numValue > max) {
|
|
254
|
-
console.warn(`⚠️ ${field} must be between ${min} and ${max}, got ${numValue}, using default: ${defaultValue}`);
|
|
255
|
-
return defaultValue;
|
|
256
|
-
}
|
|
257
|
-
return numValue;
|
|
258
|
-
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 4. default true
|
|
15
15
|
*/
|
|
16
16
|
import type { Page } from '@playwright/test';
|
|
17
|
-
import { TestivAIConfig, TestivAIProjectConfig } from '../types';
|
|
17
|
+
import type { TestivAIConfig, TestivAIProjectConfig } from '../types';
|
|
18
18
|
/**
|
|
19
19
|
* CSS injected for the duration of the capture. Near-zero durations (not
|
|
20
20
|
* `none`) let every animation/transition COMPLETE instantly at its final
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { snapshot } from './snapshot';
|
|
2
|
-
import { getCiRunId } from './ci';
|
|
3
2
|
export { snapshot, snapshot as witness };
|
|
4
3
|
export declare const testivai: {
|
|
5
4
|
witness: typeof snapshot;
|
|
6
|
-
ci: typeof getCiRunId;
|
|
7
5
|
};
|
|
8
|
-
export type { TestivAIConfig, TestivAIProjectConfig,
|
|
6
|
+
export type { TestivAIConfig, TestivAIProjectConfig, IgnoreMode, IgnoreSelectorInput } from './types';
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,6 @@ exports.testivai = exports.witness = exports.snapshot = void 0;
|
|
|
4
4
|
const snapshot_1 = require("./snapshot");
|
|
5
5
|
Object.defineProperty(exports, "snapshot", { enumerable: true, get: function () { return snapshot_1.snapshot; } });
|
|
6
6
|
Object.defineProperty(exports, "witness", { enumerable: true, get: function () { return snapshot_1.snapshot; } });
|
|
7
|
-
const ci_1 = require("./ci");
|
|
8
7
|
exports.testivai = {
|
|
9
8
|
witness: snapshot_1.snapshot,
|
|
10
|
-
ci: ci_1.getCiRunId,
|
|
11
9
|
};
|
|
12
|
-
// Structure analyzer is now handled on the backend
|
|
13
|
-
// The types are kept for backwards compatibility
|
package/dist/reporter.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { Reporter, FullConfig, Suite, FullResult } from './reporter-types';
|
|
2
|
-
import { type CompressionOptions } from '@testivai/common';
|
|
1
|
+
import type { Reporter, FullConfig, Suite, FullResult } from './reporter-types';
|
|
3
2
|
interface TestivaiReporterOptions {
|
|
4
|
-
apiUrl?: string;
|
|
5
|
-
apiKey?: string;
|
|
6
|
-
compression?: CompressionOptions;
|
|
7
3
|
debug?: boolean;
|
|
8
4
|
/**
|
|
9
5
|
* Capture only — write `.testivai/temp/` and skip comparison and report
|
|
@@ -23,13 +19,7 @@ interface TestivaiReporterOptions {
|
|
|
23
19
|
}
|
|
24
20
|
export declare class TestivAIPlaywrightReporter implements Reporter {
|
|
25
21
|
private options;
|
|
26
|
-
private gitInfo;
|
|
27
|
-
private browserInfo;
|
|
28
|
-
private runId;
|
|
29
|
-
private ciInfo;
|
|
30
22
|
private tempDir;
|
|
31
|
-
private compressionHelper;
|
|
32
|
-
private localMode;
|
|
33
23
|
private captureOnly;
|
|
34
24
|
private shard;
|
|
35
25
|
constructor(options?: TestivaiReporterOptions);
|
|
@@ -40,7 +30,7 @@ export declare class TestivAIPlaywrightReporter implements Reporter {
|
|
|
40
30
|
* 3. auto: Playwright says this is shard i/N with N > 1
|
|
41
31
|
*/
|
|
42
32
|
private resolveCaptureOnly;
|
|
43
|
-
onBegin(config: FullConfig,
|
|
33
|
+
onBegin(config: FullConfig, _suite: Suite): Promise<void>;
|
|
44
34
|
onEnd(result: FullResult): Promise<void>;
|
|
45
35
|
}
|
|
46
36
|
export default TestivAIPlaywrightReporter;
|
package/dist/reporter.js
CHANGED
|
@@ -32,38 +32,20 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
36
|
exports.TestivAIPlaywrightReporter = void 0;
|
|
40
37
|
const fs = __importStar(require("fs-extra"));
|
|
41
38
|
const path = __importStar(require("path"));
|
|
42
|
-
const simple_git_1 = __importDefault(require("simple-git"));
|
|
43
|
-
const axios_1 = __importDefault(require("axios"));
|
|
44
|
-
const ci_1 = require("./ci");
|
|
45
|
-
const common_1 = require("@testivai/common");
|
|
46
|
-
const mode_1 = require("./mode");
|
|
47
39
|
const witness_1 = require("@testivai/witness");
|
|
48
40
|
class TestivAIPlaywrightReporter {
|
|
49
41
|
constructor(options = {}) {
|
|
50
|
-
this.gitInfo = null;
|
|
51
|
-
this.browserInfo = null;
|
|
52
|
-
this.runId = null;
|
|
53
|
-
this.ciInfo = null;
|
|
54
42
|
this.tempDir = path.join(process.cwd(), '.testivai', 'temp');
|
|
55
|
-
this.localMode = false;
|
|
56
43
|
this.captureOnly = false;
|
|
57
44
|
this.shard = null;
|
|
58
45
|
this.options = {
|
|
59
|
-
apiUrl: options.apiUrl || process.env.TESTIVAI_API_URL || 'https://core-api.testiv.ai',
|
|
60
|
-
apiKey: options.apiKey || process.env.TESTIVAI_API_KEY,
|
|
61
|
-
compression: options.compression || {},
|
|
62
46
|
debug: options.debug || process.env.TESTIVAI_DEBUG === 'true',
|
|
63
47
|
captureOnly: options.captureOnly,
|
|
64
48
|
};
|
|
65
|
-
// Initialize compression helper
|
|
66
|
-
this.compressionHelper = new common_1.CompressionHelper(this.options.compression);
|
|
67
49
|
}
|
|
68
50
|
/**
|
|
69
51
|
* Capture-only resolution, most explicit wins:
|
|
@@ -74,59 +56,13 @@ class TestivAIPlaywrightReporter {
|
|
|
74
56
|
resolveCaptureOnly(_config) {
|
|
75
57
|
return (0, witness_1.resolveCaptureOnly)(this.options.captureOnly, this.shard);
|
|
76
58
|
}
|
|
77
|
-
async onBegin(config,
|
|
59
|
+
async onBegin(config, _suite) {
|
|
78
60
|
// Playwright's own --shard wins; TESTIVAI_SHARD covers CI that splits work
|
|
79
61
|
// some other way, and is the same contract the non-JS adapters honour.
|
|
80
62
|
this.shard = config.shard ?? (0, witness_1.parseShardEnv)(process.env.TESTIVAI_SHARD);
|
|
81
63
|
this.captureOnly = this.resolveCaptureOnly(config);
|
|
82
|
-
//
|
|
83
|
-
// and report locally rather than disabling the reporter. Cloud mode
|
|
84
|
-
// activates only when a key is present (or config/env force it).
|
|
85
|
-
this.localMode = (0, mode_1.resolveLocalMode)({ apiKey: this.options.apiKey });
|
|
86
|
-
if (this.localMode) {
|
|
87
|
-
process.env.TESTIVAI_MODE = 'local';
|
|
88
|
-
// One quiet, friendly line — not a scary "disabling reporter" error.
|
|
89
|
-
if (!this.options.apiKey && !process.env.TESTIVAI_MODE_ANNOUNCED) {
|
|
90
|
-
console.log('TestivAI: no API key set — running in local mode (report → visual-report/index.html).');
|
|
91
|
-
process.env.TESTIVAI_MODE_ANNOUNCED = '1';
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// 1. Clean temp directory
|
|
64
|
+
// Clean temp directory
|
|
95
65
|
await fs.emptyDir(this.tempDir);
|
|
96
|
-
// 2. Capture Git metadata
|
|
97
|
-
try {
|
|
98
|
-
const git = (0, simple_git_1.default)();
|
|
99
|
-
const [branch, commit] = await Promise.all([
|
|
100
|
-
git.revparse(['--abbrev-ref', 'HEAD']),
|
|
101
|
-
git.revparse(['HEAD']),
|
|
102
|
-
]);
|
|
103
|
-
this.gitInfo = { branch, commit };
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
console.error('Testivai Reporter: Could not get Git information.', error);
|
|
107
|
-
this.gitInfo = { branch: 'unknown', commit: 'unknown' };
|
|
108
|
-
}
|
|
109
|
-
// 3. Capture Browser info from the first project
|
|
110
|
-
const project = suite.suites[0]?.project();
|
|
111
|
-
if (project) {
|
|
112
|
-
this.browserInfo = {
|
|
113
|
-
name: project.use.browserName || 'unknown',
|
|
114
|
-
version: 'unknown', // Playwright does not easily expose browser version
|
|
115
|
-
viewportWidth: project.use.viewport?.width || 0,
|
|
116
|
-
viewportHeight: project.use.viewport?.height || 0,
|
|
117
|
-
userAgent: project.use.userAgent || 'unknown',
|
|
118
|
-
os: 'unknown',
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
// 4. Get CI Run ID and CI info
|
|
122
|
-
this.runId = (0, ci_1.getCiRunId)();
|
|
123
|
-
this.ciInfo = (0, ci_1.getCiInfo)();
|
|
124
|
-
if (this.runId && this.options.debug) {
|
|
125
|
-
console.log(`Testivai Reporter: [DEBUG] Detected CI environment. Run ID: ${this.runId}`);
|
|
126
|
-
if (this.ciInfo) {
|
|
127
|
-
console.log(`Testivai Reporter: [DEBUG] CI Info: provider=${this.ciInfo.provider}, PR=#${this.ciInfo.prNumber || 'N/A'}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
66
|
}
|
|
131
67
|
async onEnd(result) {
|
|
132
68
|
// Wrap entire reporter logic in try-catch to prevent crashes
|
|
@@ -154,182 +90,49 @@ class TestivAIPlaywrightReporter {
|
|
|
154
90
|
console.log(' npx testivai merge-captures <dirs...> && npx testivai report --fail-on-diff');
|
|
155
91
|
return;
|
|
156
92
|
}
|
|
157
|
-
// ──
|
|
158
|
-
if (this.localMode) {
|
|
159
|
-
if (this.options.debug) {
|
|
160
|
-
console.log('Testivai Reporter: [DEBUG] Local mode - generating visual report...');
|
|
161
|
-
}
|
|
162
|
-
// Dynamic import of @testivai/witness/report for local mode
|
|
163
|
-
const { generateReport } = await Promise.resolve().then(() => __importStar(require('@testivai/witness/report')));
|
|
164
|
-
// Load local config for report settings
|
|
165
|
-
const localConfigPath = path.join(process.cwd(), '.testivai', 'config.json');
|
|
166
|
-
const localConfig = fs.existsSync(localConfigPath)
|
|
167
|
-
? fs.readJsonSync(localConfigPath)
|
|
168
|
-
: { threshold: 0.1, reportDir: 'visual-report', autoOpen: false };
|
|
169
|
-
const reportData = generateReport({
|
|
170
|
-
projectRoot: process.cwd(),
|
|
171
|
-
reportDir: localConfig.reportDir || 'visual-report',
|
|
172
|
-
threshold: localConfig.threshold,
|
|
173
|
-
autoOpen: localConfig.autoOpen,
|
|
174
|
-
});
|
|
175
|
-
// Print summary
|
|
176
|
-
const { summary } = reportData;
|
|
177
|
-
const missing = reportData.missingBaselines?.length ?? 0;
|
|
178
|
-
console.log(`\n ═══ TestivAI Visual Report ═══`);
|
|
179
|
-
console.log(` Total: ${summary.total} | Passed: ${summary.passed} | Changed: ${summary.changed} | New: ${summary.newSnapshots}`);
|
|
180
|
-
if (missing > 0) {
|
|
181
|
-
console.log(` Missing baselines (no capture this run): ${missing}`);
|
|
182
|
-
}
|
|
183
|
-
// A reporter cannot change the process exit code — Playwright owns it,
|
|
184
|
-
// and it reflects test results, not visual results. Saying nothing here
|
|
185
|
-
// meant a run could print "Changed: 3" and still exit 0, which reads
|
|
186
|
-
// like a check that passed. State it plainly and name the gate.
|
|
187
|
-
if (summary.changed > 0 || summary.newSnapshots > 0 || missing > 0) {
|
|
188
|
-
console.log(`\n ⚠ This did NOT fail the build — a reporter cannot set the exit code.`);
|
|
189
|
-
console.log(` Gate it in CI: npx testivai report --fail-on-diff`);
|
|
190
|
-
console.log(` To approve: npx testivai approve --all`);
|
|
191
|
-
}
|
|
192
|
-
if (this.options.debug) {
|
|
193
|
-
console.log(`Testivai Reporter: [DEBUG] Report generated at ${path.join(process.cwd(), localConfig.reportDir || 'visual-report')}`);
|
|
194
|
-
}
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
// ── Cloud mode: upload to TestivAI API ────────────────────────────────────
|
|
198
|
-
if (!this.options.apiUrl) {
|
|
199
|
-
return; // Reporter is disabled
|
|
200
|
-
}
|
|
93
|
+
// ── Generate the HTML report ──────────────────────────────────────────
|
|
201
94
|
if (this.options.debug) {
|
|
202
|
-
console.log('Testivai Reporter: [DEBUG]
|
|
95
|
+
console.log('Testivai Reporter: [DEBUG] Generating visual report...');
|
|
203
96
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const screenshotPath = metadata.files.screenshot;
|
|
225
|
-
const stylesPath = metadata.files.styles;
|
|
226
|
-
if (!metadata.files || !structurePath || !screenshotPath) {
|
|
227
|
-
console.warn(`Testivai Reporter: Invalid metadata structure in ${jsonFile}, skipping...`);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
// Extract the first selector's layout data (usually 'body')
|
|
231
|
-
const layoutKeys = Object.keys(metadata.layout || {});
|
|
232
|
-
if (layoutKeys.length === 0) {
|
|
233
|
-
console.warn(`Testivai Reporter: No layout data found for ${metadata.snapshotName}, skipping...`);
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
const firstSelector = layoutKeys[0];
|
|
237
|
-
const layoutData = metadata.layout[firstSelector];
|
|
238
|
-
// Read screenshot and encode to base64
|
|
239
|
-
const screenshotBuffer = await fs.readFile(screenshotPath);
|
|
240
|
-
const screenshotBase64 = screenshotBuffer.toString('base64');
|
|
241
|
-
// Read computed styles if available
|
|
242
|
-
// @renamed: cssData → stylesData (IP protection)
|
|
243
|
-
let stylesData;
|
|
244
|
-
if (stylesPath && await fs.pathExists(stylesPath)) {
|
|
245
|
-
try {
|
|
246
|
-
const stylesJson = await fs.readJson(stylesPath);
|
|
247
|
-
if (stylesJson.computed_styles && Object.keys(stylesJson.computed_styles).length > 0) {
|
|
248
|
-
stylesData = { computed_styles: stylesJson.computed_styles };
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
catch (err) {
|
|
252
|
-
if (this.options.debug) {
|
|
253
|
-
console.warn(`Testivai Reporter: [DEBUG] Failed to read styles file for ${metadata.snapshotName}:`, err);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// @renamed: dom → structure, css → styles (IP protection)
|
|
258
|
-
const snapshotPayload = {
|
|
259
|
-
snapshotName: metadata.snapshotName,
|
|
260
|
-
testName: metadata.testName,
|
|
261
|
-
timestamp: metadata.timestamp,
|
|
262
|
-
url: metadata.url,
|
|
263
|
-
viewport: metadata.viewport,
|
|
264
|
-
structure: { html: await fs.readFile(structurePath, 'utf-8') },
|
|
265
|
-
styles: stylesData,
|
|
266
|
-
layout: {
|
|
267
|
-
x: layoutData.x,
|
|
268
|
-
y: layoutData.y,
|
|
269
|
-
width: layoutData.width,
|
|
270
|
-
height: layoutData.height,
|
|
271
|
-
top: layoutData.y,
|
|
272
|
-
left: layoutData.x,
|
|
273
|
-
right: layoutData.x + layoutData.width,
|
|
274
|
-
bottom: layoutData.y + layoutData.height
|
|
275
|
-
},
|
|
276
|
-
testivaiConfig: metadata.testivaiConfig,
|
|
277
|
-
screenshotData: screenshotBase64,
|
|
278
|
-
performanceMetrics: metadata.performanceMetrics
|
|
279
|
-
};
|
|
280
|
-
snapshots.push(snapshotPayload);
|
|
281
|
-
}
|
|
282
|
-
// Build batch payload matching Witness SDK format
|
|
283
|
-
const batchPayload = {
|
|
284
|
-
git: this.gitInfo,
|
|
285
|
-
browser: this.browserInfo,
|
|
286
|
-
snapshots,
|
|
287
|
-
timestamp: Date.now(),
|
|
288
|
-
runId: this.runId,
|
|
289
|
-
ci: this.ciInfo,
|
|
290
|
-
};
|
|
291
|
-
// Compress and upload (same as Witness SDK)
|
|
292
|
-
const payloadJson = JSON.stringify(batchPayload);
|
|
293
|
-
const compressionResult = await this.compressionHelper.compress(payloadJson);
|
|
294
|
-
if (this.options.debug && compressionResult.compressionRatio > 0) {
|
|
295
|
-
this.compressionHelper.logCompressionResult(compressionResult);
|
|
296
|
-
}
|
|
297
|
-
const headers = {
|
|
298
|
-
'X-API-KEY': this.options.apiKey,
|
|
299
|
-
'Content-Type': 'application/json',
|
|
300
|
-
...compressionResult.headers,
|
|
301
|
-
};
|
|
302
|
-
// Upload to same endpoint as Witness SDK
|
|
303
|
-
const startBatchResponse = await axios_1.default.post(`${this.options.apiUrl}/api/v1/ingest/start-batch`, compressionResult.data, { headers });
|
|
304
|
-
const batchId = startBatchResponse.data.batch_id || startBatchResponse.data.batchId;
|
|
305
|
-
// Show success message (brief in normal mode, detailed in debug mode)
|
|
306
|
-
if (this.options.debug) {
|
|
307
|
-
console.log(`Testivai Reporter: [DEBUG] ✓ Uploaded ${snapshots.length} snapshot(s) (Batch ID: ${batchId})`);
|
|
97
|
+
// Dynamic import of @testivai/witness/report
|
|
98
|
+
const { generateReport } = await Promise.resolve().then(() => __importStar(require('@testivai/witness/report')));
|
|
99
|
+
// Load local config for report settings
|
|
100
|
+
const localConfigPath = path.join(process.cwd(), '.testivai', 'config.json');
|
|
101
|
+
const localConfig = fs.existsSync(localConfigPath)
|
|
102
|
+
? fs.readJsonSync(localConfigPath)
|
|
103
|
+
: { threshold: 0.1, reportDir: 'visual-report', autoOpen: false };
|
|
104
|
+
const reportData = generateReport({
|
|
105
|
+
projectRoot: process.cwd(),
|
|
106
|
+
reportDir: localConfig.reportDir || 'visual-report',
|
|
107
|
+
threshold: localConfig.threshold,
|
|
108
|
+
autoOpen: localConfig.autoOpen,
|
|
109
|
+
});
|
|
110
|
+
// Print summary
|
|
111
|
+
const { summary } = reportData;
|
|
112
|
+
const missing = reportData.missingBaselines?.length ?? 0;
|
|
113
|
+
console.log(`\n === TestivAI Visual Report ===`);
|
|
114
|
+
console.log(` Total: ${summary.total} | Passed: ${summary.passed} | Changed: ${summary.changed} | New: ${summary.newSnapshots}`);
|
|
115
|
+
if (missing > 0) {
|
|
116
|
+
console.log(` Missing baselines (no capture this run): ${missing}`);
|
|
308
117
|
}
|
|
309
|
-
|
|
310
|
-
|
|
118
|
+
// A reporter cannot change the process exit code — Playwright owns it,
|
|
119
|
+
// and it reflects test results, not visual results. Saying nothing here
|
|
120
|
+
// meant a run could print "Changed: 3" and still exit 0, which reads
|
|
121
|
+
// like a check that passed. State it plainly and name the gate.
|
|
122
|
+
if (summary.changed > 0 || summary.newSnapshots > 0 || missing > 0) {
|
|
123
|
+
console.log(`\n NOTE: this did NOT fail the build — a reporter cannot set the exit code.`);
|
|
124
|
+
console.log(` Gate it in CI: npx testivai report --fail-on-diff`);
|
|
125
|
+
console.log(` To approve: npx testivai approve --all`);
|
|
311
126
|
}
|
|
312
|
-
// Clean up temp files (skip if DEBUG mode is enabled)
|
|
313
127
|
if (this.options.debug) {
|
|
314
|
-
console.log(`Testivai Reporter: [DEBUG]
|
|
128
|
+
console.log(`Testivai Reporter: [DEBUG] Report generated at ${path.join(process.cwd(), localConfig.reportDir || 'visual-report')}`);
|
|
315
129
|
}
|
|
316
|
-
else {
|
|
317
|
-
await fs.emptyDir(this.tempDir);
|
|
318
|
-
}
|
|
319
|
-
// Finalize batch
|
|
320
|
-
await axios_1.default.post(`${this.options.apiUrl}/api/v1/ingest/finish-batch/${batchId}`, {}, {
|
|
321
|
-
headers: { 'X-API-KEY': this.options.apiKey },
|
|
322
|
-
});
|
|
323
130
|
}
|
|
324
131
|
catch (error) {
|
|
325
132
|
// Log error but don't throw - let tests complete normally
|
|
326
|
-
console.error('Testivai Reporter:
|
|
133
|
+
console.error('Testivai Reporter: Error:', error.message);
|
|
327
134
|
if (this.options.debug) {
|
|
328
135
|
console.error('Error stack:', error.stack);
|
|
329
|
-
if (error.response) {
|
|
330
|
-
console.error('Response status:', error.response.status);
|
|
331
|
-
console.error('Response data:', JSON.stringify(error.response.data, null, 2));
|
|
332
|
-
}
|
|
333
136
|
}
|
|
334
137
|
// Don't throw - reporter errors should not crash the test run
|
|
335
138
|
}
|