@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/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 +19 -1
- package/dist/config/stabilize.js +49 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +0 -4
- package/dist/reporter.d.ts +2 -12
- package/dist/reporter.js +44 -257
- package/dist/snapshot.d.ts +2 -2
- package/dist/snapshot.js +15 -321
- package/dist/types.d.ts +10 -357
- package/dist/types.js +1 -6
- package/package.json +5 -11
- 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/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
|
|
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(
|
|
74
|
-
|
|
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,
|
|
82
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
// ──
|
|
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]
|
|
95
|
+
console.log('Testivai Reporter: [DEBUG] Generating visual report...');
|
|
219
96
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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]
|
|
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:
|
|
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
|
}
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -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.
|