@testivai/witness-playwright 1.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.
Files changed (61) hide show
  1. package/__tests__/.gitkeep +0 -0
  2. package/__tests__/config-integration.spec.ts +102 -0
  3. package/__tests__/snapshot.spec.d.ts +1 -0
  4. package/__tests__/snapshot.spec.js +81 -0
  5. package/__tests__/snapshot.spec.ts +58 -0
  6. package/__tests__/unit/ci.spec.d.ts +1 -0
  7. package/__tests__/unit/ci.spec.js +35 -0
  8. package/__tests__/unit/ci.spec.ts +40 -0
  9. package/__tests__/unit/reporter.spec.d.ts +1 -0
  10. package/__tests__/unit/reporter.spec.js +37 -0
  11. package/__tests__/unit/reporter.spec.ts +43 -0
  12. package/__tests__/unit/structureAnalyzer.spec.js +212 -0
  13. package/__tests__/unit/types.spec.ts +179 -0
  14. package/dist/__tests__/unit/ci.spec.d.ts +1 -0
  15. package/dist/__tests__/unit/ci.spec.js +226 -0
  16. package/dist/__tests__/unit/compression.spec.d.ts +4 -0
  17. package/dist/__tests__/unit/compression.spec.js +46 -0
  18. package/dist/ci.d.ts +30 -0
  19. package/dist/ci.js +117 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.js +47 -0
  22. package/dist/cli/init.d.ts +3 -0
  23. package/dist/cli/init.js +158 -0
  24. package/dist/config/loader.d.ts +29 -0
  25. package/dist/config/loader.js +251 -0
  26. package/dist/domAnalyzer.d.ts +10 -0
  27. package/dist/domAnalyzer.js +285 -0
  28. package/dist/index.d.ts +7 -0
  29. package/dist/index.js +11 -0
  30. package/dist/reporter-entry.d.ts +2 -0
  31. package/dist/reporter-entry.js +5 -0
  32. package/dist/reporter-types.d.ts +2 -0
  33. package/dist/reporter-types.js +2 -0
  34. package/dist/reporter.d.ts +21 -0
  35. package/dist/reporter.js +249 -0
  36. package/dist/snapshot.d.ts +12 -0
  37. package/dist/snapshot.js +601 -0
  38. package/dist/structureAnalyzer.d.ts +12 -0
  39. package/dist/structureAnalyzer.js +288 -0
  40. package/dist/types.d.ts +368 -0
  41. package/dist/types.js +10 -0
  42. package/examples/structure-analysis-example.spec.ts +118 -0
  43. package/examples/structure-analysis.config.ts +159 -0
  44. package/jest.config.js +8 -0
  45. package/package.json +51 -0
  46. package/playwright.config.ts +11 -0
  47. package/src/__tests__/unit/ci.spec.ts +257 -0
  48. package/src/__tests__/unit/compression.spec.ts +52 -0
  49. package/src/ci.ts +140 -0
  50. package/src/cli/index.ts +49 -0
  51. package/src/cli/init.ts +131 -0
  52. package/src/config/loader.ts +238 -0
  53. package/src/index.ts +14 -0
  54. package/src/reporter-entry.ts +6 -0
  55. package/src/reporter-types.ts +5 -0
  56. package/src/reporter.ts +251 -0
  57. package/src/snapshot.ts +632 -0
  58. package/src/structureAnalyzer.ts +338 -0
  59. package/src/types.ts +388 -0
  60. package/tsconfig.jest.json +7 -0
  61. package/tsconfig.json +20 -0
@@ -0,0 +1,251 @@
1
+ import { Reporter, FullConfig, Suite, FullResult } from './reporter-types';
2
+ import * as fs from 'fs-extra';
3
+ import * as path from 'path';
4
+ import simpleGit, { SimpleGit } from 'simple-git';
5
+ import axios from 'axios';
6
+ import { BatchPayload, BrowserInfo, GitInfo, SnapshotPayload } from './types';
7
+ import { getCiRunId, getCiInfo, CiInfo } from './ci';
8
+ import { CompressionHelper, type CompressionOptions } from '@testivai/common';
9
+
10
+ interface TestivaiReporterOptions {
11
+ apiUrl?: string;
12
+ apiKey?: string;
13
+ compression?: CompressionOptions;
14
+ debug?: boolean;
15
+ }
16
+
17
+ export class TestivAIPlaywrightReporter implements Reporter {
18
+ private options: TestivaiReporterOptions;
19
+ private gitInfo: GitInfo | null = null;
20
+ private browserInfo: BrowserInfo | null = null;
21
+ private runId: string | null = null;
22
+ private ciInfo: CiInfo | null = null;
23
+ private tempDir = path.join(process.cwd(), '.testivai', 'temp');
24
+ private compressionHelper: CompressionHelper;
25
+
26
+ constructor(options: TestivaiReporterOptions = {}) {
27
+ this.options = {
28
+ apiUrl: options.apiUrl || process.env.TESTIVAI_API_URL || 'https://core-api.testiv.ai',
29
+ apiKey: options.apiKey || process.env.TESTIVAI_API_KEY,
30
+ compression: options.compression || {},
31
+ debug: options.debug || process.env.TESTIVAI_DEBUG === 'true',
32
+ };
33
+
34
+ // Initialize compression helper
35
+ this.compressionHelper = new CompressionHelper(this.options.compression);
36
+ }
37
+
38
+ async onBegin(config: FullConfig, suite: Suite): Promise<void> {
39
+ if (!this.options.apiKey) {
40
+ console.error('Testivai Reporter: API Key is not configured. Disabling reporter.');
41
+ console.error('Set TESTIVAI_API_KEY environment variable or pass apiKey in reporter options.');
42
+ this.options.apiUrl = undefined; // Disable reporter
43
+ return;
44
+ }
45
+
46
+ // 1. Clean temp directory
47
+ await fs.emptyDir(this.tempDir);
48
+
49
+ // 2. Capture Git metadata
50
+ try {
51
+ const git: SimpleGit = simpleGit();
52
+ const [branch, commit] = await Promise.all([
53
+ git.revparse(['--abbrev-ref', 'HEAD']),
54
+ git.revparse(['HEAD']),
55
+ ]);
56
+ this.gitInfo = { branch, commit };
57
+ } catch (error) {
58
+ console.error('Testivai Reporter: Could not get Git information.', error);
59
+ this.gitInfo = { branch: 'unknown', commit: 'unknown' };
60
+ }
61
+
62
+ // 3. Capture Browser info from the first project
63
+ const project = suite.suites[0]?.project();
64
+ if (project) {
65
+ this.browserInfo = {
66
+ name: project.use.browserName || 'unknown',
67
+ version: 'unknown', // Playwright does not easily expose browser version
68
+ viewportWidth: project.use.viewport?.width || 0,
69
+ viewportHeight: project.use.viewport?.height || 0,
70
+ userAgent: project.use.userAgent || 'unknown',
71
+ os: 'unknown',
72
+ };
73
+ }
74
+
75
+ // 4. Get CI Run ID and CI info
76
+ this.runId = getCiRunId();
77
+ this.ciInfo = getCiInfo();
78
+ if (this.runId && this.options.debug) {
79
+ console.log(`Testivai Reporter: [DEBUG] Detected CI environment. Run ID: ${this.runId}`);
80
+ if (this.ciInfo) {
81
+ console.log(`Testivai Reporter: [DEBUG] CI Info: provider=${this.ciInfo.provider}, PR=#${this.ciInfo.prNumber || 'N/A'}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ async onEnd(result: FullResult): Promise<void> {
87
+ // Wrap entire reporter logic in try-catch to prevent crashes
88
+ try {
89
+ if (!this.options.apiUrl) {
90
+ return; // Reporter is disabled
91
+ }
92
+
93
+ if (this.options.debug) {
94
+ console.log('Testivai Reporter: [DEBUG] Test run finished. Preparing to upload evidence...');
95
+ }
96
+ const snapshotFiles = await fs.readdir(this.tempDir);
97
+ // Filter out .css.json files - they're not metadata files
98
+ const jsonFiles = snapshotFiles.filter(f => f.endsWith('.json') && !f.endsWith('.css.json'));
99
+
100
+ if (jsonFiles.length === 0) {
101
+ if (this.options.debug) {
102
+ console.log('Testivai Reporter: [DEBUG] No snapshots found to upload.');
103
+ }
104
+ return;
105
+ }
106
+
107
+ if (this.options.debug) {
108
+ console.log(`Testivai Reporter: [DEBUG] Found ${jsonFiles.length} snapshot(s) to process`);
109
+ }
110
+
111
+ // Build snapshots array matching CDP SDK format
112
+ const snapshots: SnapshotPayload[] = [];
113
+
114
+ for (const jsonFile of jsonFiles) {
115
+ const metadataPath = path.join(this.tempDir, jsonFile);
116
+ const metadata = await fs.readJson(metadataPath);
117
+
118
+ // Null safety: ensure metadata has required structure
119
+ // @renamed: dom → structure, css → styles (IP protection)
120
+ const structurePath = metadata.files.structure;
121
+ const screenshotPath = metadata.files.screenshot;
122
+ const stylesPath = metadata.files.styles;
123
+
124
+ if (!metadata.files || !structurePath || !screenshotPath) {
125
+ console.warn(`Testivai Reporter: Invalid metadata structure in ${jsonFile}, skipping...`);
126
+ continue;
127
+ }
128
+
129
+ // Extract the first selector's layout data (usually 'body')
130
+ const layoutKeys = Object.keys(metadata.layout || {});
131
+ if (layoutKeys.length === 0) {
132
+ console.warn(`Testivai Reporter: No layout data found for ${metadata.snapshotName}, skipping...`);
133
+ continue;
134
+ }
135
+ const firstSelector = layoutKeys[0];
136
+ const layoutData = metadata.layout[firstSelector];
137
+
138
+ // Read screenshot and encode to base64
139
+ const screenshotBuffer = await fs.readFile(screenshotPath);
140
+ const screenshotBase64 = screenshotBuffer.toString('base64');
141
+
142
+ // Read computed styles if available
143
+ // @renamed: cssData → stylesData (IP protection)
144
+ let stylesData: { computed_styles: Record<string, Record<string, string>> } | undefined;
145
+ if (stylesPath && await fs.pathExists(stylesPath)) {
146
+ try {
147
+ const stylesJson = await fs.readJson(stylesPath);
148
+ if (stylesJson.computed_styles && Object.keys(stylesJson.computed_styles).length > 0) {
149
+ stylesData = { computed_styles: stylesJson.computed_styles };
150
+ }
151
+ } catch (err) {
152
+ if (this.options.debug) {
153
+ console.warn(`Testivai Reporter: [DEBUG] Failed to read styles file for ${metadata.snapshotName}:`, err);
154
+ }
155
+ }
156
+ }
157
+
158
+ // @renamed: dom → structure, css → styles (IP protection)
159
+ const snapshotPayload: SnapshotPayload = {
160
+ snapshotName: metadata.snapshotName,
161
+ testName: metadata.testName,
162
+ timestamp: metadata.timestamp,
163
+ url: metadata.url,
164
+ viewport: metadata.viewport,
165
+ structure: { html: await fs.readFile(structurePath, 'utf-8') },
166
+ styles: stylesData,
167
+ layout: {
168
+ x: layoutData.x,
169
+ y: layoutData.y,
170
+ width: layoutData.width,
171
+ height: layoutData.height,
172
+ top: layoutData.y,
173
+ left: layoutData.x,
174
+ right: layoutData.x + layoutData.width,
175
+ bottom: layoutData.y + layoutData.height
176
+ },
177
+ testivaiConfig: metadata.testivaiConfig,
178
+ screenshotData: screenshotBase64
179
+ };
180
+ snapshots.push(snapshotPayload);
181
+ }
182
+
183
+ // Build batch payload matching CDP SDK format
184
+ const batchPayload: Omit<BatchPayload, 'batchId'> = {
185
+ git: this.gitInfo!,
186
+ browser: this.browserInfo!,
187
+ snapshots,
188
+ timestamp: Date.now(),
189
+ runId: this.runId,
190
+ ci: this.ciInfo,
191
+ };
192
+
193
+ // Compress and upload (same as CDP SDK)
194
+ const payloadJson = JSON.stringify(batchPayload);
195
+ const compressionResult = await this.compressionHelper.compress(payloadJson);
196
+
197
+ if (this.options.debug && compressionResult.compressionRatio > 0) {
198
+ this.compressionHelper.logCompressionResult(compressionResult);
199
+ }
200
+
201
+ const headers: Record<string, string> = {
202
+ 'X-API-KEY': this.options.apiKey!,
203
+ 'Content-Type': 'application/json',
204
+ ...compressionResult.headers,
205
+ };
206
+
207
+ // Upload to same endpoint as CDP SDK
208
+ const startBatchResponse = await axios.post(
209
+ `${this.options.apiUrl}/api/v1/ingest/start-batch`,
210
+ compressionResult.data,
211
+ { headers }
212
+ );
213
+
214
+ const batchId = startBatchResponse.data.batch_id || startBatchResponse.data.batchId;
215
+
216
+ // Show success message (brief in normal mode, detailed in debug mode)
217
+ if (this.options.debug) {
218
+ console.log(`Testivai Reporter: [DEBUG] ✓ Uploaded ${snapshots.length} snapshot(s) (Batch ID: ${batchId})`);
219
+ } else {
220
+ console.log(`Testivai Reporter: ✓ Uploaded ${snapshots.length} snapshot(s)`);
221
+ }
222
+
223
+ // Clean up temp files (skip if DEBUG mode is enabled)
224
+ if (this.options.debug) {
225
+ console.log(`Testivai Reporter: [DEBUG] Keeping temporary evidence files in: ${this.tempDir}`);
226
+ } else {
227
+ await fs.emptyDir(this.tempDir);
228
+ }
229
+
230
+ // Finalize batch
231
+ await axios.post(`${this.options.apiUrl}/api/v1/ingest/finish-batch/${batchId}`, {}, {
232
+ headers: { 'X-API-KEY': this.options.apiKey },
233
+ });
234
+
235
+ } catch (error: any) {
236
+ // Log error but don't throw - let tests complete normally
237
+ console.error('Testivai Reporter: ❌ Error:', error.message);
238
+ if (this.options.debug) {
239
+ console.error('Error stack:', error.stack);
240
+ if (error.response) {
241
+ console.error('Response status:', error.response.status);
242
+ console.error('Response data:', JSON.stringify(error.response.data, null, 2));
243
+ }
244
+ }
245
+ // Don't throw - reporter errors should not crash the test run
246
+ }
247
+ }
248
+
249
+ }
250
+
251
+ export default TestivAIPlaywrightReporter;