@testivai/witness-playwright 0.1.2 → 0.1.4
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 +70 -2
- package/dist/cli/init.js +0 -0
- package/dist/reporter.js +6 -11
- package/dist/snapshot.js +99 -2
- package/dist/types.d.ts +66 -0
- package/package.json +2 -1
- package/progress.md +88 -3
- package/src/reporter.ts +7 -16
- package/src/snapshot.ts +109 -3
- package/src/types.ts +69 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @testivai/witness-playwright
|
|
2
2
|
|
|
3
|
-
**Status**: ✅ Production Ready | **Last Updated**: January
|
|
3
|
+
**Status**: ✅ Production Ready | **Last Updated**: January 10, 2026
|
|
4
4
|
|
|
5
5
|
**Project:** @testivai/witness-playwright (MVP - 1 Month Plan)
|
|
6
6
|
|
|
@@ -73,6 +73,74 @@ test('my example test', async ({ page }, testInfo) => {
|
|
|
73
73
|
npm install @testivai/witness-playwright
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
## Performance Metrics
|
|
77
|
+
|
|
78
|
+
**NEW**: Automatically capture page performance metrics and Core Web Vitals!
|
|
79
|
+
|
|
80
|
+
### Basic Performance Capture (Enabled by Default)
|
|
81
|
+
|
|
82
|
+
Every snapshot automatically captures:
|
|
83
|
+
- **Core Web Vitals**: LCP, FCP, CLS
|
|
84
|
+
- **Page Load Metrics**: DOM Content Loaded, Load Complete
|
|
85
|
+
- **Navigation Timing**: Navigation start and timing data
|
|
86
|
+
|
|
87
|
+
No configuration needed - it just works!
|
|
88
|
+
|
|
89
|
+
### Optional Lighthouse Integration
|
|
90
|
+
|
|
91
|
+
For comprehensive performance audits, enable Lighthouse:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// testivai.config.ts
|
|
95
|
+
export default {
|
|
96
|
+
performance: {
|
|
97
|
+
captureTimings: true, // Basic timing (default: ON)
|
|
98
|
+
enableLighthouse: false, // Lighthouse audit (default: OFF)
|
|
99
|
+
lighthouseThresholds: {
|
|
100
|
+
performance: 80,
|
|
101
|
+
accessibility: 90,
|
|
102
|
+
bestPractices: 80,
|
|
103
|
+
seo: 80
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Per-Test Configuration
|
|
110
|
+
|
|
111
|
+
Enable Lighthouse for specific tests:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
test('performance critical page', async ({ page }, testInfo) => {
|
|
115
|
+
await page.goto('https://example.com');
|
|
116
|
+
|
|
117
|
+
await testivai.witness(page, testInfo, 'homepage', {
|
|
118
|
+
performance: {
|
|
119
|
+
enableLighthouse: true,
|
|
120
|
+
lighthouseThresholds: {
|
|
121
|
+
performance: 90
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### What Gets Captured
|
|
129
|
+
|
|
130
|
+
**Basic Timing (Always Captured)**:
|
|
131
|
+
- First Contentful Paint (FCP)
|
|
132
|
+
- Largest Contentful Paint (LCP)
|
|
133
|
+
- Cumulative Layout Shift (CLS)
|
|
134
|
+
- DOM Content Loaded
|
|
135
|
+
- Load Complete
|
|
136
|
+
|
|
137
|
+
**Lighthouse (When Enabled)**:
|
|
138
|
+
- Performance Score (0-100)
|
|
139
|
+
- Accessibility Score (0-100)
|
|
140
|
+
- Best Practices Score (0-100)
|
|
141
|
+
- SEO Score (0-100)
|
|
142
|
+
- Detailed Core Web Vitals
|
|
143
|
+
|
|
76
144
|
## 📊 Progress
|
|
77
145
|
|
|
78
|
-
See [
|
|
146
|
+
See [progress.md](./progress.md) for detailed development progress.
|
package/dist/cli/init.js
CHANGED
|
File without changes
|
package/dist/reporter.js
CHANGED
|
@@ -106,7 +106,6 @@ class TestivAIPlaywrightReporter {
|
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
const snapshots = [];
|
|
109
|
-
const filesToUpload = [];
|
|
110
109
|
for (const jsonFile of jsonFiles) {
|
|
111
110
|
const metadataPath = path.join(this.tempDir, jsonFile);
|
|
112
111
|
const metadata = await fs.readJson(metadataPath);
|
|
@@ -120,6 +119,9 @@ class TestivAIPlaywrightReporter {
|
|
|
120
119
|
}
|
|
121
120
|
const firstSelector = layoutKeys[0];
|
|
122
121
|
const layoutData = metadata.layout[firstSelector];
|
|
122
|
+
// Read screenshot and encode to base64
|
|
123
|
+
const screenshotBuffer = await fs.readFile(screenshotPath);
|
|
124
|
+
const screenshotBase64 = screenshotBuffer.toString('base64');
|
|
123
125
|
const snapshotPayload = {
|
|
124
126
|
...metadata,
|
|
125
127
|
dom: { html: await fs.readFile(domPath, 'utf-8') },
|
|
@@ -129,10 +131,10 @@ class TestivAIPlaywrightReporter {
|
|
|
129
131
|
width: layoutData.width,
|
|
130
132
|
height: layoutData.height
|
|
131
133
|
},
|
|
132
|
-
testivaiConfig: metadata.testivaiConfig
|
|
134
|
+
testivaiConfig: metadata.testivaiConfig,
|
|
135
|
+
screenshotData: screenshotBase64 // Include base64-encoded screenshot
|
|
133
136
|
};
|
|
134
137
|
snapshots.push(snapshotPayload);
|
|
135
|
-
filesToUpload.push({ filePath: screenshotPath, contentType: 'image/png' });
|
|
136
138
|
}
|
|
137
139
|
const batchPayload = {
|
|
138
140
|
git: this.gitInfo,
|
|
@@ -141,20 +143,13 @@ class TestivAIPlaywrightReporter {
|
|
|
141
143
|
timestamp: Date.now(),
|
|
142
144
|
runId: this.runId,
|
|
143
145
|
};
|
|
144
|
-
// Start batch
|
|
146
|
+
// Start batch - screenshots are now included in the payload
|
|
145
147
|
const startBatchResponse = await axios_1.default.post(`${this.options.apiUrl}/api/v1/ingest/start-batch`, batchPayload, {
|
|
146
148
|
headers: { 'X-API-KEY': this.options.apiKey },
|
|
147
149
|
});
|
|
148
150
|
console.log('Testivai Reporter: API Response:', JSON.stringify(startBatchResponse.data, null, 2));
|
|
149
151
|
// Handle both snake_case and camelCase response formats
|
|
150
152
|
const batchId = startBatchResponse.data.batch_id || startBatchResponse.data.batchId;
|
|
151
|
-
const uploadInstructions = startBatchResponse.data.upload_instructions || startBatchResponse.data.uploadInstructions;
|
|
152
|
-
// Upload files
|
|
153
|
-
const uploadPromises = filesToUpload.map((file, index) => {
|
|
154
|
-
const instruction = uploadInstructions[index];
|
|
155
|
-
return fs.readFile(file.filePath).then(buffer => axios_1.default.put(instruction.url, buffer, { headers: { 'Content-Type': file.contentType } }));
|
|
156
|
-
});
|
|
157
|
-
await Promise.all(uploadPromises);
|
|
158
153
|
// Finalize batch
|
|
159
154
|
await axios_1.default.post(`${this.options.apiUrl}/api/v1/ingest/finish-batch/${batchId}`, {}, {
|
|
160
155
|
headers: { 'X-API-KEY': this.options.apiKey },
|
package/dist/snapshot.js
CHANGED
|
@@ -79,6 +79,16 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
79
79
|
const baseFilename = `${timestamp}_${safeName}`;
|
|
80
80
|
// 1. Capture full-page screenshot
|
|
81
81
|
const screenshotPath = path.join(outputDir, `${baseFilename}.png`);
|
|
82
|
+
// Ensure lazy-loaded content is loaded by scrolling to bottom and back
|
|
83
|
+
try {
|
|
84
|
+
await page.evaluate('window.scrollTo(0, Math.max(document.body.scrollHeight, document.documentElement.scrollHeight))');
|
|
85
|
+
await page.waitForTimeout(100);
|
|
86
|
+
await page.evaluate('window.scrollTo(0, 0)');
|
|
87
|
+
await page.waitForTimeout(100);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Ignore scroll errors
|
|
91
|
+
}
|
|
82
92
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
83
93
|
// 2. Dump full-page DOM
|
|
84
94
|
const domPath = path.join(outputDir, `${baseFilename}.html`);
|
|
@@ -100,7 +110,90 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
100
110
|
};
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
|
-
// 4.
|
|
113
|
+
// 4. Capture performance metrics (if enabled)
|
|
114
|
+
let performanceTimings;
|
|
115
|
+
let lighthouseResults;
|
|
116
|
+
const captureTimings = effectiveConfig.performance?.captureTimings ?? true; // Default: enabled
|
|
117
|
+
const enableLighthouse = effectiveConfig.performance?.enableLighthouse ?? false; // Default: disabled
|
|
118
|
+
// Capture basic timing metrics
|
|
119
|
+
if (captureTimings) {
|
|
120
|
+
try {
|
|
121
|
+
performanceTimings = await page.evaluate(() => {
|
|
122
|
+
// @ts-ignore - window is available in browser context
|
|
123
|
+
const perfData = window.performance;
|
|
124
|
+
const navigation = perfData.timing;
|
|
125
|
+
const paintEntries = perfData.getEntriesByType('paint');
|
|
126
|
+
const lcpEntries = perfData.getEntriesByType('largest-contentful-paint');
|
|
127
|
+
// Get First Contentful Paint
|
|
128
|
+
const fcpEntry = paintEntries.find((entry) => entry.name === 'first-contentful-paint');
|
|
129
|
+
// Get Largest Contentful Paint
|
|
130
|
+
const lcpEntry = lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
131
|
+
// Calculate Cumulative Layout Shift
|
|
132
|
+
let cls = 0;
|
|
133
|
+
const layoutShiftEntries = perfData.getEntriesByType('layout-shift');
|
|
134
|
+
layoutShiftEntries.forEach((entry) => {
|
|
135
|
+
if (!entry.hadRecentInput) {
|
|
136
|
+
cls += entry.value;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
navigationStart: navigation.navigationStart,
|
|
141
|
+
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.navigationStart,
|
|
142
|
+
loadComplete: navigation.loadEventEnd - navigation.navigationStart,
|
|
143
|
+
firstContentfulPaint: fcpEntry ? fcpEntry.startTime : undefined,
|
|
144
|
+
largestContentfulPaint: lcpEntry ? lcpEntry.startTime : undefined,
|
|
145
|
+
cumulativeLayoutShift: cls,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.warn('Failed to capture performance metrics:', err);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Capture Lighthouse audit (if enabled)
|
|
154
|
+
if (enableLighthouse) {
|
|
155
|
+
try {
|
|
156
|
+
const { playAudit } = await Promise.resolve().then(() => __importStar(require('playwright-lighthouse')));
|
|
157
|
+
const thresholds = effectiveConfig.performance?.lighthouseThresholds || {};
|
|
158
|
+
const auditResult = await playAudit({
|
|
159
|
+
page,
|
|
160
|
+
port: 9222, // Required by playwright-lighthouse
|
|
161
|
+
thresholds: {
|
|
162
|
+
performance: thresholds.performance || 0,
|
|
163
|
+
accessibility: thresholds.accessibility || 0,
|
|
164
|
+
'best-practices': thresholds.bestPractices || 0,
|
|
165
|
+
seo: thresholds.seo || 0,
|
|
166
|
+
},
|
|
167
|
+
reports: {
|
|
168
|
+
formats: {
|
|
169
|
+
json: false, // Don't save report files
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
// Extract key metrics from Lighthouse result
|
|
174
|
+
if (auditResult) {
|
|
175
|
+
const lhr = auditResult.lhr;
|
|
176
|
+
if (lhr) {
|
|
177
|
+
lighthouseResults = {
|
|
178
|
+
performance: lhr.categories?.performance?.score ? Math.round(lhr.categories.performance.score * 100) : undefined,
|
|
179
|
+
accessibility: lhr.categories?.accessibility?.score ? Math.round(lhr.categories.accessibility.score * 100) : undefined,
|
|
180
|
+
bestPractices: lhr.categories?.['best-practices']?.score ? Math.round(lhr.categories['best-practices'].score * 100) : undefined,
|
|
181
|
+
seo: lhr.categories?.seo?.score ? Math.round(lhr.categories.seo.score * 100) : undefined,
|
|
182
|
+
coreWebVitals: {
|
|
183
|
+
lcp: lhr.audits?.['largest-contentful-paint']?.numericValue,
|
|
184
|
+
fid: lhr.audits?.['max-potential-fid']?.numericValue,
|
|
185
|
+
cls: lhr.audits?.['cumulative-layout-shift']?.numericValue,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
console.warn('Failed to run Lighthouse audit:', err);
|
|
193
|
+
console.warn('Make sure playwright-lighthouse is installed: npm install playwright-lighthouse');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// 5. Save metadata with configuration and performance data
|
|
104
197
|
const metadataPath = path.join(outputDir, `${baseFilename}.json`);
|
|
105
198
|
const metadata = {
|
|
106
199
|
snapshotName,
|
|
@@ -117,6 +210,10 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
117
210
|
},
|
|
118
211
|
layout,
|
|
119
212
|
// Store the effective configuration for the reporter
|
|
120
|
-
testivaiConfig: effectiveConfig
|
|
213
|
+
testivaiConfig: effectiveConfig,
|
|
214
|
+
// Store performance metrics if captured
|
|
215
|
+
performanceTimings,
|
|
216
|
+
// Store Lighthouse results if captured
|
|
217
|
+
lighthouseResults
|
|
121
218
|
});
|
|
122
219
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -32,6 +32,62 @@ export interface AIConfig {
|
|
|
32
32
|
/** Include AI reasoning in results (optional) */
|
|
33
33
|
enableReasoning?: boolean;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Performance metrics configuration
|
|
37
|
+
*/
|
|
38
|
+
export interface PerformanceConfig {
|
|
39
|
+
/** Enable basic timing capture (default: true) */
|
|
40
|
+
captureTimings?: boolean;
|
|
41
|
+
/** Enable Lighthouse performance audit (default: false) */
|
|
42
|
+
enableLighthouse?: boolean;
|
|
43
|
+
/** Lighthouse performance thresholds (optional) */
|
|
44
|
+
lighthouseThresholds?: {
|
|
45
|
+
performance?: number;
|
|
46
|
+
accessibility?: number;
|
|
47
|
+
bestPractices?: number;
|
|
48
|
+
seo?: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Performance timing metrics
|
|
53
|
+
*/
|
|
54
|
+
export interface PerformanceTimings {
|
|
55
|
+
/** Navigation start time */
|
|
56
|
+
navigationStart?: number;
|
|
57
|
+
/** DOM content loaded time */
|
|
58
|
+
domContentLoaded?: number;
|
|
59
|
+
/** Page load complete time */
|
|
60
|
+
loadComplete?: number;
|
|
61
|
+
/** First contentful paint */
|
|
62
|
+
firstContentfulPaint?: number;
|
|
63
|
+
/** Largest contentful paint */
|
|
64
|
+
largestContentfulPaint?: number;
|
|
65
|
+
/** Time to interactive */
|
|
66
|
+
timeToInteractive?: number;
|
|
67
|
+
/** Total blocking time */
|
|
68
|
+
totalBlockingTime?: number;
|
|
69
|
+
/** Cumulative layout shift */
|
|
70
|
+
cumulativeLayoutShift?: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Lighthouse performance results
|
|
74
|
+
*/
|
|
75
|
+
export interface LighthouseResults {
|
|
76
|
+
/** Performance score (0-100) */
|
|
77
|
+
performance?: number;
|
|
78
|
+
/** Accessibility score (0-100) */
|
|
79
|
+
accessibility?: number;
|
|
80
|
+
/** Best practices score (0-100) */
|
|
81
|
+
bestPractices?: number;
|
|
82
|
+
/** SEO score (0-100) */
|
|
83
|
+
seo?: number;
|
|
84
|
+
/** Core Web Vitals */
|
|
85
|
+
coreWebVitals?: {
|
|
86
|
+
lcp?: number;
|
|
87
|
+
fid?: number;
|
|
88
|
+
cls?: number;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
35
91
|
/**
|
|
36
92
|
* Environment-specific configuration overrides
|
|
37
93
|
*/
|
|
@@ -60,6 +116,8 @@ export interface TestivAIProjectConfig {
|
|
|
60
116
|
layout: LayoutConfig;
|
|
61
117
|
/** AI analysis settings */
|
|
62
118
|
ai: AIConfig;
|
|
119
|
+
/** Performance metrics settings (optional) */
|
|
120
|
+
performance?: PerformanceConfig;
|
|
63
121
|
/** Environment-specific overrides (optional) */
|
|
64
122
|
environments?: EnvironmentConfig;
|
|
65
123
|
}
|
|
@@ -71,6 +129,8 @@ export interface TestivAIConfig {
|
|
|
71
129
|
layout?: Partial<LayoutConfig>;
|
|
72
130
|
/** AI settings (optional - overrides project defaults) */
|
|
73
131
|
ai?: Partial<AIConfig>;
|
|
132
|
+
/** Performance settings (optional - overrides project defaults) */
|
|
133
|
+
performance?: Partial<PerformanceConfig>;
|
|
74
134
|
/** Element selectors to capture (existing option) */
|
|
75
135
|
selectors?: string[];
|
|
76
136
|
}
|
|
@@ -127,6 +187,12 @@ export interface SnapshotPayload {
|
|
|
127
187
|
};
|
|
128
188
|
/** TestivAI configuration for this snapshot */
|
|
129
189
|
testivaiConfig?: TestivAIConfig;
|
|
190
|
+
/** Base64-encoded screenshot data (PNG) */
|
|
191
|
+
screenshotData?: string;
|
|
192
|
+
/** Performance timing metrics (optional) */
|
|
193
|
+
performanceTimings?: PerformanceTimings;
|
|
194
|
+
/** Lighthouse results (optional) */
|
|
195
|
+
lighthouseResults?: LighthouseResults;
|
|
130
196
|
}
|
|
131
197
|
/**
|
|
132
198
|
* Git information for batch context
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-playwright",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Playwright sensor for Testivai Visual Regression Test system",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"axios": "^1.6.0",
|
|
33
33
|
"cross-fetch": "^4.0.0",
|
|
34
34
|
"fs-extra": "^11.2.0",
|
|
35
|
+
"playwright-lighthouse": "^4.0.0",
|
|
35
36
|
"simple-git": "^3.21.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
package/progress.md
CHANGED
|
@@ -661,11 +661,96 @@ The Playwright SDK provides two main components that are production-ready:
|
|
|
661
661
|
|
|
662
662
|
---
|
|
663
663
|
|
|
664
|
-
|
|
664
|
+
## Performance Metrics Integration (January 10, 2026)
|
|
665
|
+
|
|
666
|
+
### Performance Monitoring ✅ COMPLETE
|
|
667
|
+
|
|
668
|
+
**Goal**: Automatically capture page performance metrics and Core Web Vitals with optional Lighthouse integration.
|
|
669
|
+
|
|
670
|
+
#### Changes Implemented
|
|
671
|
+
|
|
672
|
+
1. **Basic Performance Capture** ✅
|
|
673
|
+
- Automatically captures performance timing metrics on every snapshot
|
|
674
|
+
- Enabled by default (`captureTimings: true`)
|
|
675
|
+
- Zero configuration needed for basic metrics
|
|
676
|
+
- Metrics captured:
|
|
677
|
+
- Navigation Start
|
|
678
|
+
- DOM Content Loaded
|
|
679
|
+
- Load Complete
|
|
680
|
+
- First Contentful Paint (FCP)
|
|
681
|
+
- Largest Contentful Paint (LCP)
|
|
682
|
+
- Cumulative Layout Shift (CLS)
|
|
683
|
+
|
|
684
|
+
2. **Lighthouse Integration** ✅
|
|
685
|
+
- Added `playwright-lighthouse@4.0.0` dependency
|
|
686
|
+
- Optional full Lighthouse audit (OFF by default)
|
|
687
|
+
- Captures all 4 Lighthouse categories:
|
|
688
|
+
- Performance Score (0-100)
|
|
689
|
+
- Accessibility Score (0-100)
|
|
690
|
+
- Best Practices Score (0-100)
|
|
691
|
+
- SEO Score (0-100)
|
|
692
|
+
- Extracts Core Web Vitals from Lighthouse
|
|
693
|
+
|
|
694
|
+
3. **Configuration** ✅
|
|
695
|
+
```typescript
|
|
696
|
+
// In testivai.config.ts
|
|
697
|
+
export default {
|
|
698
|
+
performance: {
|
|
699
|
+
captureTimings: true, // Basic timing (default: ON)
|
|
700
|
+
enableLighthouse: false, // Lighthouse audit (default: OFF)
|
|
701
|
+
lighthouseThresholds: {
|
|
702
|
+
performance: 80,
|
|
703
|
+
accessibility: 90,
|
|
704
|
+
bestPractices: 80,
|
|
705
|
+
seo: 80
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Per-test override
|
|
711
|
+
await testivai.witness(page, testInfo, 'test-name', {
|
|
712
|
+
performance: {
|
|
713
|
+
enableLighthouse: true
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
4. **Data Storage** ✅
|
|
719
|
+
- Performance data stored in `testivai_config.performanceTimings`
|
|
720
|
+
- Lighthouse results stored in `testivai_config.lighthouseResults`
|
|
721
|
+
- Flows through existing ingestion pipeline
|
|
722
|
+
- No database schema changes needed
|
|
723
|
+
|
|
724
|
+
5. **Dashboard Display** ✅
|
|
725
|
+
- Core Web Vitals displayed with color coding:
|
|
726
|
+
- Green: Good (LCP < 2.5s, FCP < 1.8s, CLS < 0.1)
|
|
727
|
+
- Yellow: Needs Improvement
|
|
728
|
+
- Red: Poor
|
|
729
|
+
- Page Load metrics table
|
|
730
|
+
- Beautiful card-based UI
|
|
731
|
+
|
|
732
|
+
#### Files Modified
|
|
733
|
+
- `src/types.ts` - Added PerformanceConfig, PerformanceTimings, LighthouseResults
|
|
734
|
+
- `src/snapshot.ts` - Implemented performance capture and Lighthouse integration
|
|
735
|
+
- `package.json` - Added playwright-lighthouse dependency
|
|
736
|
+
- `apps/dashboard/src/services/api.ts` - Added testivai_config to TestRun interface
|
|
737
|
+
- `apps/dashboard/src/pages/TestRunDetailPage.tsx` - Performance metrics display
|
|
738
|
+
|
|
739
|
+
#### Benefits
|
|
740
|
+
- **Automatic Tracking**: Every test run captures performance metrics
|
|
741
|
+
- **Core Web Vitals**: Track LCP, FCP, CLS automatically
|
|
742
|
+
- **Optional Lighthouse**: Full audit when needed (OFF by default)
|
|
743
|
+
- **Performance Regression Detection**: Compare metrics across test runs
|
|
744
|
+
- **Zero Configuration**: Works out of the box with sensible defaults
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
**Last Updated**: January 10, 2026
|
|
665
749
|
**Status**: 🎉 PUBLISHED TO NPM ✅ - Publicly available
|
|
666
|
-
**NPM Package**: @testivai/witness-playwright@0.1.
|
|
667
|
-
**Core Features**: Evidence capture
|
|
750
|
+
**NPM Package**: @testivai/witness-playwright@0.1.2
|
|
751
|
+
**Core Features**: Evidence capture, batch upload, and performance monitoring fully functional
|
|
668
752
|
**Configuration**: ✅ COMPLETE - End-to-end flow working
|
|
753
|
+
**Performance Metrics**: ✅ COMPLETE - Basic timing + optional Lighthouse
|
|
669
754
|
**API Key Format**: tstvai-{secure-random-string}
|
|
670
755
|
**Known Issues**: Minor UX improvements (retry logic, progress reporting)
|
|
671
756
|
**Blocker**: None - All critical features implemented
|
package/src/reporter.ts
CHANGED
|
@@ -86,7 +86,6 @@ export class TestivAIPlaywrightReporter implements Reporter {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const snapshots: SnapshotPayload[] = [];
|
|
89
|
-
const filesToUpload: { filePath: string, contentType: string }[] = [];
|
|
90
89
|
|
|
91
90
|
for (const jsonFile of jsonFiles) {
|
|
92
91
|
const metadataPath = path.join(this.tempDir, jsonFile);
|
|
@@ -104,6 +103,10 @@ export class TestivAIPlaywrightReporter implements Reporter {
|
|
|
104
103
|
const firstSelector = layoutKeys[0];
|
|
105
104
|
const layoutData = metadata.layout[firstSelector];
|
|
106
105
|
|
|
106
|
+
// Read screenshot and encode to base64
|
|
107
|
+
const screenshotBuffer = await fs.readFile(screenshotPath);
|
|
108
|
+
const screenshotBase64 = screenshotBuffer.toString('base64');
|
|
109
|
+
|
|
107
110
|
const snapshotPayload: SnapshotPayload = {
|
|
108
111
|
...metadata,
|
|
109
112
|
dom: { html: await fs.readFile(domPath, 'utf-8') },
|
|
@@ -113,11 +116,10 @@ export class TestivAIPlaywrightReporter implements Reporter {
|
|
|
113
116
|
width: layoutData.width,
|
|
114
117
|
height: layoutData.height
|
|
115
118
|
},
|
|
116
|
-
testivaiConfig: metadata.testivaiConfig
|
|
119
|
+
testivaiConfig: metadata.testivaiConfig,
|
|
120
|
+
screenshotData: screenshotBase64 // Include base64-encoded screenshot
|
|
117
121
|
};
|
|
118
122
|
snapshots.push(snapshotPayload);
|
|
119
|
-
|
|
120
|
-
filesToUpload.push({ filePath: screenshotPath, contentType: 'image/png' });
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
const batchPayload: Omit<BatchPayload, 'batchId'> = {
|
|
@@ -128,7 +130,7 @@ export class TestivAIPlaywrightReporter implements Reporter {
|
|
|
128
130
|
runId: this.runId,
|
|
129
131
|
};
|
|
130
132
|
|
|
131
|
-
// Start batch
|
|
133
|
+
// Start batch - screenshots are now included in the payload
|
|
132
134
|
const startBatchResponse = await axios.post(`${this.options.apiUrl}/api/v1/ingest/start-batch`, batchPayload, {
|
|
133
135
|
headers: { 'X-API-KEY': this.options.apiKey },
|
|
134
136
|
});
|
|
@@ -137,17 +139,6 @@ export class TestivAIPlaywrightReporter implements Reporter {
|
|
|
137
139
|
|
|
138
140
|
// Handle both snake_case and camelCase response formats
|
|
139
141
|
const batchId = startBatchResponse.data.batch_id || startBatchResponse.data.batchId;
|
|
140
|
-
const uploadInstructions = startBatchResponse.data.upload_instructions || startBatchResponse.data.uploadInstructions;
|
|
141
|
-
|
|
142
|
-
// Upload files
|
|
143
|
-
const uploadPromises = filesToUpload.map((file, index) => {
|
|
144
|
-
const instruction = uploadInstructions[index];
|
|
145
|
-
return fs.readFile(file.filePath).then(buffer =>
|
|
146
|
-
axios.put(instruction.url, buffer, { headers: { 'Content-Type': file.contentType } })
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
await Promise.all(uploadPromises);
|
|
151
142
|
|
|
152
143
|
// Finalize batch
|
|
153
144
|
await axios.post(`${this.options.apiUrl}/api/v1/ingest/finish-batch/${batchId}`, {}, {
|
package/src/snapshot.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Page, TestInfo } from '@playwright/test';
|
|
|
2
2
|
import * as fs from 'fs-extra';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { URL } from 'url';
|
|
5
|
-
import { SnapshotPayload, LayoutData, TestivAIConfig } from './types';
|
|
5
|
+
import { SnapshotPayload, LayoutData, TestivAIConfig, PerformanceTimings, LighthouseResults } from './types';
|
|
6
6
|
import { loadConfig, mergeTestConfig } from './config/loader';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -55,6 +55,17 @@ export async function snapshot(
|
|
|
55
55
|
|
|
56
56
|
// 1. Capture full-page screenshot
|
|
57
57
|
const screenshotPath = path.join(outputDir, `${baseFilename}.png`);
|
|
58
|
+
|
|
59
|
+
// Ensure lazy-loaded content is loaded by scrolling to bottom and back
|
|
60
|
+
try {
|
|
61
|
+
await page.evaluate('window.scrollTo(0, Math.max(document.body.scrollHeight, document.documentElement.scrollHeight))');
|
|
62
|
+
await page.waitForTimeout(100);
|
|
63
|
+
await page.evaluate('window.scrollTo(0, 0)');
|
|
64
|
+
await page.waitForTimeout(100);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// Ignore scroll errors
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
59
70
|
|
|
60
71
|
// 2. Dump full-page DOM
|
|
@@ -80,7 +91,98 @@ export async function snapshot(
|
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
// 4.
|
|
94
|
+
// 4. Capture performance metrics (if enabled)
|
|
95
|
+
let performanceTimings: PerformanceTimings | undefined;
|
|
96
|
+
let lighthouseResults: LighthouseResults | undefined;
|
|
97
|
+
|
|
98
|
+
const captureTimings = effectiveConfig.performance?.captureTimings ?? true; // Default: enabled
|
|
99
|
+
const enableLighthouse = effectiveConfig.performance?.enableLighthouse ?? false; // Default: disabled
|
|
100
|
+
|
|
101
|
+
// Capture basic timing metrics
|
|
102
|
+
if (captureTimings) {
|
|
103
|
+
try {
|
|
104
|
+
performanceTimings = await page.evaluate(() => {
|
|
105
|
+
// @ts-ignore - window is available in browser context
|
|
106
|
+
const perfData = window.performance;
|
|
107
|
+
const navigation = perfData.timing;
|
|
108
|
+
const paintEntries = perfData.getEntriesByType('paint');
|
|
109
|
+
const lcpEntries = perfData.getEntriesByType('largest-contentful-paint');
|
|
110
|
+
|
|
111
|
+
// Get First Contentful Paint
|
|
112
|
+
const fcpEntry = paintEntries.find((entry: any) => entry.name === 'first-contentful-paint');
|
|
113
|
+
|
|
114
|
+
// Get Largest Contentful Paint
|
|
115
|
+
const lcpEntry = lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
116
|
+
|
|
117
|
+
// Calculate Cumulative Layout Shift
|
|
118
|
+
let cls = 0;
|
|
119
|
+
const layoutShiftEntries = perfData.getEntriesByType('layout-shift') as any[];
|
|
120
|
+
layoutShiftEntries.forEach((entry: any) => {
|
|
121
|
+
if (!entry.hadRecentInput) {
|
|
122
|
+
cls += entry.value;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
navigationStart: navigation.navigationStart,
|
|
128
|
+
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.navigationStart,
|
|
129
|
+
loadComplete: navigation.loadEventEnd - navigation.navigationStart,
|
|
130
|
+
firstContentfulPaint: fcpEntry ? fcpEntry.startTime : undefined,
|
|
131
|
+
largestContentfulPaint: lcpEntry ? (lcpEntry as any).startTime : undefined,
|
|
132
|
+
cumulativeLayoutShift: cls,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn('Failed to capture performance metrics:', err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Capture Lighthouse audit (if enabled)
|
|
141
|
+
if (enableLighthouse) {
|
|
142
|
+
try {
|
|
143
|
+
const { playAudit } = await import('playwright-lighthouse');
|
|
144
|
+
const thresholds = effectiveConfig.performance?.lighthouseThresholds || {};
|
|
145
|
+
|
|
146
|
+
const auditResult = await playAudit({
|
|
147
|
+
page,
|
|
148
|
+
port: 9222, // Required by playwright-lighthouse
|
|
149
|
+
thresholds: {
|
|
150
|
+
performance: thresholds.performance || 0,
|
|
151
|
+
accessibility: thresholds.accessibility || 0,
|
|
152
|
+
'best-practices': thresholds.bestPractices || 0,
|
|
153
|
+
seo: thresholds.seo || 0,
|
|
154
|
+
},
|
|
155
|
+
reports: {
|
|
156
|
+
formats: {
|
|
157
|
+
json: false, // Don't save report files
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Extract key metrics from Lighthouse result
|
|
163
|
+
if (auditResult) {
|
|
164
|
+
const lhr = (auditResult as any).lhr;
|
|
165
|
+
if (lhr) {
|
|
166
|
+
lighthouseResults = {
|
|
167
|
+
performance: lhr.categories?.performance?.score ? Math.round(lhr.categories.performance.score * 100) : undefined,
|
|
168
|
+
accessibility: lhr.categories?.accessibility?.score ? Math.round(lhr.categories.accessibility.score * 100) : undefined,
|
|
169
|
+
bestPractices: lhr.categories?.['best-practices']?.score ? Math.round(lhr.categories['best-practices'].score * 100) : undefined,
|
|
170
|
+
seo: lhr.categories?.seo?.score ? Math.round(lhr.categories.seo.score * 100) : undefined,
|
|
171
|
+
coreWebVitals: {
|
|
172
|
+
lcp: lhr.audits?.['largest-contentful-paint']?.numericValue,
|
|
173
|
+
fid: lhr.audits?.['max-potential-fid']?.numericValue,
|
|
174
|
+
cls: lhr.audits?.['cumulative-layout-shift']?.numericValue,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.warn('Failed to run Lighthouse audit:', err);
|
|
181
|
+
console.warn('Make sure playwright-lighthouse is installed: npm install playwright-lighthouse');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 5. Save metadata with configuration and performance data
|
|
84
186
|
const metadataPath = path.join(outputDir, `${baseFilename}.json`);
|
|
85
187
|
const metadata: Partial<SnapshotPayload> = {
|
|
86
188
|
snapshotName,
|
|
@@ -98,6 +200,10 @@ export async function snapshot(
|
|
|
98
200
|
},
|
|
99
201
|
layout,
|
|
100
202
|
// Store the effective configuration for the reporter
|
|
101
|
-
testivaiConfig: effectiveConfig
|
|
203
|
+
testivaiConfig: effectiveConfig,
|
|
204
|
+
// Store performance metrics if captured
|
|
205
|
+
performanceTimings,
|
|
206
|
+
// Store Lighthouse results if captured
|
|
207
|
+
lighthouseResults
|
|
102
208
|
});
|
|
103
209
|
}
|
package/src/types.ts
CHANGED
|
@@ -35,6 +35,65 @@ export interface AIConfig {
|
|
|
35
35
|
enableReasoning?: boolean;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Performance metrics configuration
|
|
40
|
+
*/
|
|
41
|
+
export interface PerformanceConfig {
|
|
42
|
+
/** Enable basic timing capture (default: true) */
|
|
43
|
+
captureTimings?: boolean;
|
|
44
|
+
/** Enable Lighthouse performance audit (default: false) */
|
|
45
|
+
enableLighthouse?: boolean;
|
|
46
|
+
/** Lighthouse performance thresholds (optional) */
|
|
47
|
+
lighthouseThresholds?: {
|
|
48
|
+
performance?: number;
|
|
49
|
+
accessibility?: number;
|
|
50
|
+
bestPractices?: number;
|
|
51
|
+
seo?: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Performance timing metrics
|
|
57
|
+
*/
|
|
58
|
+
export interface PerformanceTimings {
|
|
59
|
+
/** Navigation start time */
|
|
60
|
+
navigationStart?: number;
|
|
61
|
+
/** DOM content loaded time */
|
|
62
|
+
domContentLoaded?: number;
|
|
63
|
+
/** Page load complete time */
|
|
64
|
+
loadComplete?: number;
|
|
65
|
+
/** First contentful paint */
|
|
66
|
+
firstContentfulPaint?: number;
|
|
67
|
+
/** Largest contentful paint */
|
|
68
|
+
largestContentfulPaint?: number;
|
|
69
|
+
/** Time to interactive */
|
|
70
|
+
timeToInteractive?: number;
|
|
71
|
+
/** Total blocking time */
|
|
72
|
+
totalBlockingTime?: number;
|
|
73
|
+
/** Cumulative layout shift */
|
|
74
|
+
cumulativeLayoutShift?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Lighthouse performance results
|
|
79
|
+
*/
|
|
80
|
+
export interface LighthouseResults {
|
|
81
|
+
/** Performance score (0-100) */
|
|
82
|
+
performance?: number;
|
|
83
|
+
/** Accessibility score (0-100) */
|
|
84
|
+
accessibility?: number;
|
|
85
|
+
/** Best practices score (0-100) */
|
|
86
|
+
bestPractices?: number;
|
|
87
|
+
/** SEO score (0-100) */
|
|
88
|
+
seo?: number;
|
|
89
|
+
/** Core Web Vitals */
|
|
90
|
+
coreWebVitals?: {
|
|
91
|
+
lcp?: number;
|
|
92
|
+
fid?: number;
|
|
93
|
+
cls?: number;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
38
97
|
/**
|
|
39
98
|
* Environment-specific configuration overrides
|
|
40
99
|
*/
|
|
@@ -64,6 +123,8 @@ export interface TestivAIProjectConfig {
|
|
|
64
123
|
layout: LayoutConfig;
|
|
65
124
|
/** AI analysis settings */
|
|
66
125
|
ai: AIConfig;
|
|
126
|
+
/** Performance metrics settings (optional) */
|
|
127
|
+
performance?: PerformanceConfig;
|
|
67
128
|
/** Environment-specific overrides (optional) */
|
|
68
129
|
environments?: EnvironmentConfig;
|
|
69
130
|
}
|
|
@@ -76,6 +137,8 @@ export interface TestivAIConfig {
|
|
|
76
137
|
layout?: Partial<LayoutConfig>;
|
|
77
138
|
/** AI settings (optional - overrides project defaults) */
|
|
78
139
|
ai?: Partial<AIConfig>;
|
|
140
|
+
/** Performance settings (optional - overrides project defaults) */
|
|
141
|
+
performance?: Partial<PerformanceConfig>;
|
|
79
142
|
/** Element selectors to capture (existing option) */
|
|
80
143
|
selectors?: string[];
|
|
81
144
|
}
|
|
@@ -135,6 +198,12 @@ export interface SnapshotPayload {
|
|
|
135
198
|
};
|
|
136
199
|
/** TestivAI configuration for this snapshot */
|
|
137
200
|
testivaiConfig?: TestivAIConfig;
|
|
201
|
+
/** Base64-encoded screenshot data (PNG) */
|
|
202
|
+
screenshotData?: string;
|
|
203
|
+
/** Performance timing metrics (optional) */
|
|
204
|
+
performanceTimings?: PerformanceTimings;
|
|
205
|
+
/** Lighthouse results (optional) */
|
|
206
|
+
lighthouseResults?: LighthouseResults;
|
|
138
207
|
}
|
|
139
208
|
|
|
140
209
|
/**
|