@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 CHANGED
@@ -113,6 +113,6 @@ MIT
113
113
 
114
114
  ## Support
115
115
 
116
- - Documentation: https://github.com/mcbuddy/testivai-oss/tree/main/packages/playwright
117
- - Issues: https://github.com/mcbuddy/testivai-oss/issues
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;
@@ -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-test configuration with project configuration
10
+ * Merge per-snapshot configuration with project configuration
11
11
  *
12
12
  * @param projectConfig The project-level configuration
13
- * @param testConfig Optional per-test configuration overrides
14
- * @returns TestivAIConfig The effective configuration for this test
13
+ * @param testConfig Optional per-snapshot configuration overrides
14
+ * @returns TestivAIConfig The effective configuration for this snapshot
15
15
  */
16
- export declare function mergeTestConfig(projectConfig: TestivAIProjectConfig, testConfig?: TestivAIConfig): TestivAIConfig;
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;
@@ -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
- console.log('⚠️ No testivai.config.ts or testivai.config.js found, using defaults');
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
- // Validate and merge with defaults
92
- return validateAndMergeConfig(config);
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('⚠️ Failed to load testivai config, using defaults:', error);
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-test configuration with project configuration
94
+ * Merge per-snapshot configuration with project configuration
101
95
  *
102
96
  * @param projectConfig The project-level configuration
103
- * @param testConfig Optional per-test configuration overrides
104
- * @returns TestivAIConfig The effective configuration for this test
97
+ * @param testConfig Optional per-snapshot configuration overrides
98
+ * @returns TestivAIConfig The effective configuration for this snapshot
105
99
  */
106
- function mergeTestConfig(projectConfig, testConfig) {
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, StructureAnalysisConfig, StructureAnalysis } from './types';
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
@@ -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, suite: Suite): Promise<void>;
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, suite) {
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
- // Resolve mode. Local-first is the default: with no API key we capture
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
- // ── Local mode: generate HTML report instead of uploading ─────────────────
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] Test run finished. Preparing to upload evidence...');
95
+ console.log('Testivai Reporter: [DEBUG] Generating visual report...');
203
96
  }
204
- const snapshotFiles = await fs.readdir(this.tempDir);
205
- // Filter out .css.json files - they're not metadata files
206
- const jsonFiles = snapshotFiles.filter(f => f.endsWith('.json') && !f.endsWith('.css.json'));
207
- if (jsonFiles.length === 0) {
208
- if (this.options.debug) {
209
- console.log('Testivai Reporter: [DEBUG] No snapshots found to upload.');
210
- }
211
- return;
212
- }
213
- if (this.options.debug) {
214
- console.log(`Testivai Reporter: [DEBUG] Found ${jsonFiles.length} snapshot(s) to process`);
215
- }
216
- // Build snapshots array matching Witness SDK format
217
- const snapshots = [];
218
- for (const jsonFile of jsonFiles) {
219
- const metadataPath = path.join(this.tempDir, jsonFile);
220
- const metadata = await fs.readJson(metadataPath);
221
- // Null safety: ensure metadata has required structure
222
- // @renamed: dom → structure, css → styles (IP protection)
223
- const structurePath = metadata.files.structure;
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
- else {
310
- console.log(`Testivai Reporter: ✓ Uploaded ${snapshots.length} snapshot(s)`);
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] Keeping temporary evidence files in: ${this.tempDir}`);
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: ❌ Error:', error.message);
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
  }