@testivai/witness-playwright 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/reporter.js CHANGED
@@ -32,37 +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");
39
+ const witness_1 = require("@testivai/witness");
47
40
  class TestivAIPlaywrightReporter {
48
41
  constructor(options = {}) {
49
- this.gitInfo = null;
50
- this.browserInfo = null;
51
- this.runId = null;
52
- this.ciInfo = null;
53
42
  this.tempDir = path.join(process.cwd(), '.testivai', 'temp');
54
- this.localMode = false;
55
43
  this.captureOnly = false;
56
44
  this.shard = null;
57
45
  this.options = {
58
- apiUrl: options.apiUrl || process.env.TESTIVAI_API_URL || 'https://core-api.testiv.ai',
59
- apiKey: options.apiKey || process.env.TESTIVAI_API_KEY,
60
- compression: options.compression || {},
61
46
  debug: options.debug || process.env.TESTIVAI_DEBUG === 'true',
62
47
  captureOnly: options.captureOnly,
63
48
  };
64
- // Initialize compression helper
65
- this.compressionHelper = new common_1.CompressionHelper(this.options.compression);
66
49
  }
67
50
  /**
68
51
  * Capture-only resolution, most explicit wins:
@@ -70,65 +53,16 @@ class TestivAIPlaywrightReporter {
70
53
  * 2. TESTIVAI_CAPTURE_ONLY — for CI that can't edit playwright.config
71
54
  * 3. auto: Playwright says this is shard i/N with N > 1
72
55
  */
73
- resolveCaptureOnly(config) {
74
- if (typeof this.options.captureOnly === 'boolean')
75
- return this.options.captureOnly;
76
- const env = process.env.TESTIVAI_CAPTURE_ONLY;
77
- if (env !== undefined && env !== '')
78
- return env !== '0' && env.toLowerCase() !== 'false';
79
- return !!(this.shard && this.shard.total > 1);
56
+ resolveCaptureOnly(_config) {
57
+ return (0, witness_1.resolveCaptureOnly)(this.options.captureOnly, this.shard);
80
58
  }
81
- async onBegin(config, suite) {
82
- this.shard = config.shard ?? null;
59
+ async onBegin(config, _suite) {
60
+ // Playwright's own --shard wins; TESTIVAI_SHARD covers CI that splits work
61
+ // some other way, and is the same contract the non-JS adapters honour.
62
+ this.shard = config.shard ?? (0, witness_1.parseShardEnv)(process.env.TESTIVAI_SHARD);
83
63
  this.captureOnly = this.resolveCaptureOnly(config);
84
- // Resolve mode. Local-first is the default: with no API key we capture
85
- // and report locally rather than disabling the reporter. Cloud mode
86
- // activates only when a key is present (or config/env force it).
87
- this.localMode = (0, mode_1.resolveLocalMode)({ apiKey: this.options.apiKey });
88
- if (this.localMode) {
89
- process.env.TESTIVAI_MODE = 'local';
90
- // One quiet, friendly line — not a scary "disabling reporter" error.
91
- if (!this.options.apiKey && !process.env.TESTIVAI_MODE_ANNOUNCED) {
92
- console.log('TestivAI: no API key set — running in local mode (report → visual-report/index.html).');
93
- process.env.TESTIVAI_MODE_ANNOUNCED = '1';
94
- }
95
- }
96
- // 1. Clean temp directory
64
+ // Clean temp directory
97
65
  await fs.emptyDir(this.tempDir);
98
- // 2. Capture Git metadata
99
- try {
100
- const git = (0, simple_git_1.default)();
101
- const [branch, commit] = await Promise.all([
102
- git.revparse(['--abbrev-ref', 'HEAD']),
103
- git.revparse(['HEAD']),
104
- ]);
105
- this.gitInfo = { branch, commit };
106
- }
107
- catch (error) {
108
- console.error('Testivai Reporter: Could not get Git information.', error);
109
- this.gitInfo = { branch: 'unknown', commit: 'unknown' };
110
- }
111
- // 3. Capture Browser info from the first project
112
- const project = suite.suites[0]?.project();
113
- if (project) {
114
- this.browserInfo = {
115
- name: project.use.browserName || 'unknown',
116
- version: 'unknown', // Playwright does not easily expose browser version
117
- viewportWidth: project.use.viewport?.width || 0,
118
- viewportHeight: project.use.viewport?.height || 0,
119
- userAgent: project.use.userAgent || 'unknown',
120
- os: 'unknown',
121
- };
122
- }
123
- // 4. Get CI Run ID and CI info
124
- this.runId = (0, ci_1.getCiRunId)();
125
- this.ciInfo = (0, ci_1.getCiInfo)();
126
- if (this.runId && this.options.debug) {
127
- console.log(`Testivai Reporter: [DEBUG] Detected CI environment. Run ID: ${this.runId}`);
128
- if (this.ciInfo) {
129
- console.log(`Testivai Reporter: [DEBUG] CI Info: provider=${this.ciInfo.provider}, PR=#${this.ciInfo.prNumber || 'N/A'}`);
130
- }
131
- }
132
66
  }
133
67
  async onEnd(result) {
134
68
  // Wrap entire reporter logic in try-catch to prevent crashes
@@ -145,24 +79,10 @@ class TestivAIPlaywrightReporter {
145
79
  // Without this a crashed shard silently reduces coverage whenever
146
80
  // failOnMissing is off.
147
81
  if (this.shard) {
148
- try {
149
- const tempRoot = path.join(process.cwd(), '.testivai', 'temp');
150
- fs.ensureDirSync(tempRoot);
151
- const captures = fs
152
- .readdirSync(tempRoot, { withFileTypes: true })
153
- .filter((e) => e.isDirectory())
154
- .map((e) => e.name);
155
- fs.writeJsonSync(path.join(tempRoot, 'testivai-shard.json'), {
156
- shard: { current: this.shard.current, total: this.shard.total },
157
- captures,
158
- status: result.status,
159
- timestamp: new Date().toISOString(),
160
- });
161
- }
162
- catch {
163
- // A manifest we cannot write must not fail the test run; the merge
164
- // step degrades to "no completeness check" and says so.
165
- }
82
+ (0, witness_1.writeShardManifest)(path.join(process.cwd(), '.testivai', 'temp'), this.shard, {
83
+ complete: true,
84
+ status: result.status,
85
+ });
166
86
  }
167
87
  const where = this.shard ? `shard ${this.shard.current}/${this.shard.total}` : 'capture-only mode';
168
88
  console.log(`\n TestivAI: ${where} — captured to .testivai/temp/, comparison skipped.`);
@@ -170,182 +90,49 @@ class TestivAIPlaywrightReporter {
170
90
  console.log(' npx testivai merge-captures <dirs...> && npx testivai report --fail-on-diff');
171
91
  return;
172
92
  }
173
- // ── Local mode: generate HTML report instead of uploading ─────────────────
174
- if (this.localMode) {
175
- if (this.options.debug) {
176
- console.log('Testivai Reporter: [DEBUG] Local mode - generating visual report...');
177
- }
178
- // Dynamic import of @testivai/witness/report for local mode
179
- const { generateReport } = await Promise.resolve().then(() => __importStar(require('@testivai/witness/report')));
180
- // Load local config for report settings
181
- const localConfigPath = path.join(process.cwd(), '.testivai', 'config.json');
182
- const localConfig = fs.existsSync(localConfigPath)
183
- ? fs.readJsonSync(localConfigPath)
184
- : { threshold: 0.1, reportDir: 'visual-report', autoOpen: false };
185
- const reportData = generateReport({
186
- projectRoot: process.cwd(),
187
- reportDir: localConfig.reportDir || 'visual-report',
188
- threshold: localConfig.threshold,
189
- autoOpen: localConfig.autoOpen,
190
- });
191
- // Print summary
192
- const { summary } = reportData;
193
- const missing = reportData.missingBaselines?.length ?? 0;
194
- console.log(`\n ═══ TestivAI Visual Report ═══`);
195
- console.log(` Total: ${summary.total} | Passed: ${summary.passed} | Changed: ${summary.changed} | New: ${summary.newSnapshots}`);
196
- if (missing > 0) {
197
- console.log(` Missing baselines (no capture this run): ${missing}`);
198
- }
199
- // A reporter cannot change the process exit code — Playwright owns it,
200
- // and it reflects test results, not visual results. Saying nothing here
201
- // meant a run could print "Changed: 3" and still exit 0, which reads
202
- // like a check that passed. State it plainly and name the gate.
203
- if (summary.changed > 0 || summary.newSnapshots > 0 || missing > 0) {
204
- console.log(`\n ⚠ This did NOT fail the build — a reporter cannot set the exit code.`);
205
- console.log(` Gate it in CI: npx testivai report --fail-on-diff`);
206
- console.log(` To approve: npx testivai approve --all`);
207
- }
208
- if (this.options.debug) {
209
- console.log(`Testivai Reporter: [DEBUG] Report generated at ${path.join(process.cwd(), localConfig.reportDir || 'visual-report')}`);
210
- }
211
- return;
212
- }
213
- // ── Cloud mode: upload to TestivAI API ────────────────────────────────────
214
- if (!this.options.apiUrl) {
215
- return; // Reporter is disabled
216
- }
93
+ // ── Generate the HTML report ──────────────────────────────────────────
217
94
  if (this.options.debug) {
218
- console.log('Testivai Reporter: [DEBUG] Test run finished. Preparing to upload evidence...');
95
+ console.log('Testivai Reporter: [DEBUG] Generating visual report...');
219
96
  }
220
- const snapshotFiles = await fs.readdir(this.tempDir);
221
- // Filter out .css.json files - they're not metadata files
222
- const jsonFiles = snapshotFiles.filter(f => f.endsWith('.json') && !f.endsWith('.css.json'));
223
- if (jsonFiles.length === 0) {
224
- if (this.options.debug) {
225
- console.log('Testivai Reporter: [DEBUG] No snapshots found to upload.');
226
- }
227
- return;
228
- }
229
- if (this.options.debug) {
230
- console.log(`Testivai Reporter: [DEBUG] Found ${jsonFiles.length} snapshot(s) to process`);
231
- }
232
- // Build snapshots array matching Witness SDK format
233
- const snapshots = [];
234
- for (const jsonFile of jsonFiles) {
235
- const metadataPath = path.join(this.tempDir, jsonFile);
236
- const metadata = await fs.readJson(metadataPath);
237
- // Null safety: ensure metadata has required structure
238
- // @renamed: dom → structure, css → styles (IP protection)
239
- const structurePath = metadata.files.structure;
240
- const screenshotPath = metadata.files.screenshot;
241
- const stylesPath = metadata.files.styles;
242
- if (!metadata.files || !structurePath || !screenshotPath) {
243
- console.warn(`Testivai Reporter: Invalid metadata structure in ${jsonFile}, skipping...`);
244
- continue;
245
- }
246
- // Extract the first selector's layout data (usually 'body')
247
- const layoutKeys = Object.keys(metadata.layout || {});
248
- if (layoutKeys.length === 0) {
249
- console.warn(`Testivai Reporter: No layout data found for ${metadata.snapshotName}, skipping...`);
250
- continue;
251
- }
252
- const firstSelector = layoutKeys[0];
253
- const layoutData = metadata.layout[firstSelector];
254
- // Read screenshot and encode to base64
255
- const screenshotBuffer = await fs.readFile(screenshotPath);
256
- const screenshotBase64 = screenshotBuffer.toString('base64');
257
- // Read computed styles if available
258
- // @renamed: cssData → stylesData (IP protection)
259
- let stylesData;
260
- if (stylesPath && await fs.pathExists(stylesPath)) {
261
- try {
262
- const stylesJson = await fs.readJson(stylesPath);
263
- if (stylesJson.computed_styles && Object.keys(stylesJson.computed_styles).length > 0) {
264
- stylesData = { computed_styles: stylesJson.computed_styles };
265
- }
266
- }
267
- catch (err) {
268
- if (this.options.debug) {
269
- console.warn(`Testivai Reporter: [DEBUG] Failed to read styles file for ${metadata.snapshotName}:`, err);
270
- }
271
- }
272
- }
273
- // @renamed: dom → structure, css → styles (IP protection)
274
- const snapshotPayload = {
275
- snapshotName: metadata.snapshotName,
276
- testName: metadata.testName,
277
- timestamp: metadata.timestamp,
278
- url: metadata.url,
279
- viewport: metadata.viewport,
280
- structure: { html: await fs.readFile(structurePath, 'utf-8') },
281
- styles: stylesData,
282
- layout: {
283
- x: layoutData.x,
284
- y: layoutData.y,
285
- width: layoutData.width,
286
- height: layoutData.height,
287
- top: layoutData.y,
288
- left: layoutData.x,
289
- right: layoutData.x + layoutData.width,
290
- bottom: layoutData.y + layoutData.height
291
- },
292
- testivaiConfig: metadata.testivaiConfig,
293
- screenshotData: screenshotBase64,
294
- performanceMetrics: metadata.performanceMetrics
295
- };
296
- snapshots.push(snapshotPayload);
297
- }
298
- // Build batch payload matching Witness SDK format
299
- const batchPayload = {
300
- git: this.gitInfo,
301
- browser: this.browserInfo,
302
- snapshots,
303
- timestamp: Date.now(),
304
- runId: this.runId,
305
- ci: this.ciInfo,
306
- };
307
- // Compress and upload (same as Witness SDK)
308
- const payloadJson = JSON.stringify(batchPayload);
309
- const compressionResult = await this.compressionHelper.compress(payloadJson);
310
- if (this.options.debug && compressionResult.compressionRatio > 0) {
311
- this.compressionHelper.logCompressionResult(compressionResult);
312
- }
313
- const headers = {
314
- 'X-API-KEY': this.options.apiKey,
315
- 'Content-Type': 'application/json',
316
- ...compressionResult.headers,
317
- };
318
- // Upload to same endpoint as Witness SDK
319
- const startBatchResponse = await axios_1.default.post(`${this.options.apiUrl}/api/v1/ingest/start-batch`, compressionResult.data, { headers });
320
- const batchId = startBatchResponse.data.batch_id || startBatchResponse.data.batchId;
321
- // Show success message (brief in normal mode, detailed in debug mode)
322
- if (this.options.debug) {
323
- 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}`);
324
117
  }
325
- else {
326
- 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`);
327
126
  }
328
- // Clean up temp files (skip if DEBUG mode is enabled)
329
127
  if (this.options.debug) {
330
- 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')}`);
331
129
  }
332
- else {
333
- await fs.emptyDir(this.tempDir);
334
- }
335
- // Finalize batch
336
- await axios_1.default.post(`${this.options.apiUrl}/api/v1/ingest/finish-batch/${batchId}`, {}, {
337
- headers: { 'X-API-KEY': this.options.apiKey },
338
- });
339
130
  }
340
131
  catch (error) {
341
132
  // Log error but don't throw - let tests complete normally
342
- console.error('Testivai Reporter: ❌ Error:', error.message);
133
+ console.error('Testivai Reporter: Error:', error.message);
343
134
  if (this.options.debug) {
344
135
  console.error('Error stack:', error.stack);
345
- if (error.response) {
346
- console.error('Response status:', error.response.status);
347
- console.error('Response data:', JSON.stringify(error.response.data, null, 2));
348
- }
349
136
  }
350
137
  // Don't throw - reporter errors should not crash the test run
351
138
  }
@@ -1,5 +1,5 @@
1
- import { Page, TestInfo } from '@playwright/test';
2
- import { TestivAIConfig } from './types';
1
+ import type { Page, TestInfo } from '@playwright/test';
2
+ import type { TestivAIConfig } from './types';
3
3
  /**
4
4
  * Compute the effective snapshot name, folding in a project variant when the
5
5
  * Playwright config runs multiple projects. Without this, two projects (e.g.