@testivai/witness-playwright 0.1.7 → 0.1.9
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/snapshot.js +143 -83
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/snapshot.ts +78 -9
- package/src/types.ts +2 -0
package/dist/snapshot.js
CHANGED
|
@@ -75,21 +75,80 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
75
75
|
// Load project configuration and merge with test-specific overrides
|
|
76
76
|
const projectConfig = await (0, loader_1.loadConfig)();
|
|
77
77
|
const effectiveConfig = (0, loader_1.mergeTestConfig)(projectConfig, config);
|
|
78
|
+
// Debug: Log config
|
|
79
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
80
|
+
console.log('[TestivAI] Config:', {
|
|
81
|
+
projectConfig,
|
|
82
|
+
testConfig: config,
|
|
83
|
+
effectiveConfig
|
|
84
|
+
});
|
|
85
|
+
}
|
|
78
86
|
const outputDir = path.join(process.cwd(), '.testivai', 'temp');
|
|
79
87
|
await fs.ensureDir(outputDir);
|
|
80
88
|
const snapshotName = name || getSnapshotNameFromUrl(page.url());
|
|
81
89
|
const timestamp = Date.now();
|
|
82
90
|
const safeName = snapshotName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
83
91
|
const baseFilename = `${timestamp}_${safeName}`;
|
|
84
|
-
// 1. Capture full-page screenshot
|
|
85
|
-
// This is the same technique used by GoFullPage Chrome extension
|
|
92
|
+
// 1. Capture full-page screenshot
|
|
86
93
|
const screenshotPath = path.join(outputDir, `${baseFilename}.png`);
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
// Check if CDP approach is enabled
|
|
95
|
+
if (effectiveConfig.useCDP) {
|
|
96
|
+
// Use Chrome DevTools Protocol for full-page capture
|
|
97
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
98
|
+
console.log('[TestivAI] Using CDP approach for full-page screenshot');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
// Create a CDP session
|
|
102
|
+
const client = await page.context().newCDPSession(page);
|
|
103
|
+
// Enable Page domain
|
|
104
|
+
await client.send('Page.enable');
|
|
105
|
+
// Get layout metrics to determine full page size
|
|
106
|
+
const layoutMetrics = await client.send('Page.getLayoutMetrics');
|
|
107
|
+
// Calculate full page dimensions
|
|
108
|
+
const pageWidth = Math.ceil(layoutMetrics.contentSize.width);
|
|
109
|
+
const pageHeight = Math.ceil(layoutMetrics.contentSize.height);
|
|
110
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
111
|
+
console.log('[TestivAI] CDP Layout metrics:', {
|
|
112
|
+
pageWidth,
|
|
113
|
+
pageHeight,
|
|
114
|
+
viewportWidth: layoutMetrics.layoutViewport.clientWidth,
|
|
115
|
+
viewportHeight: layoutMetrics.layoutViewport.clientHeight
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Capture screenshot with captureBeyondViewport: true
|
|
119
|
+
const screenshot = await client.send('Page.captureScreenshot', {
|
|
120
|
+
format: 'png',
|
|
121
|
+
captureBeyondViewport: true,
|
|
122
|
+
clip: {
|
|
123
|
+
x: 0,
|
|
124
|
+
y: 0,
|
|
125
|
+
width: pageWidth,
|
|
126
|
+
height: pageHeight,
|
|
127
|
+
scale: 1
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// Save the screenshot
|
|
131
|
+
await fs.writeFile(screenshotPath, Buffer.from(screenshot.data, 'base64'));
|
|
132
|
+
// Close CDP session
|
|
133
|
+
await client.detach();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('[TestivAI] CDP screenshot failed:', error.message);
|
|
137
|
+
// Fallback to regular screenshot
|
|
138
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Use scroll-and-stitch approach (default)
|
|
143
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
144
|
+
console.log('[TestivAI] Using scroll-and-stitch approach for full-page screenshot');
|
|
145
|
+
}
|
|
146
|
+
// Get viewport dimensions
|
|
147
|
+
const viewport = page.viewportSize();
|
|
148
|
+
const viewportWidth = viewport?.width || 1280;
|
|
149
|
+
const viewportHeight = viewport?.height || 720;
|
|
150
|
+
// Find the main scrollable container and get its dimensions
|
|
151
|
+
const scrollableInfo = await page.evaluate(`
|
|
93
152
|
(function() {
|
|
94
153
|
var mainScrollable = null;
|
|
95
154
|
var maxScrollHeight = 0;
|
|
@@ -134,91 +193,92 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
134
193
|
};
|
|
135
194
|
})()
|
|
136
195
|
`);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
// Calculate number of screenshots needed
|
|
197
|
+
const totalHeight = scrollableInfo.scrollHeight;
|
|
198
|
+
const captureHeight = scrollableInfo.clientHeight;
|
|
199
|
+
const numCaptures = Math.ceil(totalHeight / captureHeight);
|
|
200
|
+
// Debug logging (only when TESTIVAI_DEBUG is enabled)
|
|
201
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
202
|
+
console.log(`[TestivAI] Scroll-and-stitch info:`, {
|
|
203
|
+
hasScrollable: scrollableInfo.hasScrollable,
|
|
204
|
+
scrollableId: scrollableInfo.scrollableId,
|
|
205
|
+
totalHeight,
|
|
206
|
+
captureHeight,
|
|
207
|
+
numCaptures,
|
|
208
|
+
viewportWidth,
|
|
209
|
+
viewportHeight
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// If only one capture needed, just take a regular screenshot
|
|
213
|
+
if (numCaptures <= 1) {
|
|
214
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Scroll-and-stitch approach
|
|
218
|
+
const screenshots = [];
|
|
219
|
+
for (let i = 0; i < numCaptures; i++) {
|
|
220
|
+
const scrollPosition = i * captureHeight;
|
|
221
|
+
// Scroll to position
|
|
222
|
+
if (scrollableInfo.hasScrollable && scrollableInfo.scrollableId) {
|
|
223
|
+
await page.evaluate(`
|
|
165
224
|
document.getElementById('${scrollableInfo.scrollableId}').scrollTop = ${scrollPosition};
|
|
166
225
|
`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
await page.evaluate(`window.scrollTo(0, ${scrollPosition})`);
|
|
229
|
+
}
|
|
230
|
+
// Wait for scroll and any lazy-loaded content
|
|
231
|
+
await page.waitForTimeout(100);
|
|
232
|
+
// Capture this viewport
|
|
233
|
+
const screenshotBuffer = await page.screenshot({ fullPage: false });
|
|
234
|
+
screenshots.push(screenshotBuffer);
|
|
167
235
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// For the last screenshot, we need to crop from the bottom
|
|
185
|
-
if (isLast && lastCaptureHeight < captureHeight) {
|
|
236
|
+
// Stitch screenshots together using sharp
|
|
237
|
+
// Calculate the actual height of the last capture (may be partial)
|
|
238
|
+
const lastCaptureHeight = totalHeight - (captureHeight * (numCaptures - 1));
|
|
239
|
+
// Create composite image
|
|
240
|
+
const compositeInputs = screenshots.map((buffer, index) => {
|
|
241
|
+
const isLast = index === screenshots.length - 1;
|
|
242
|
+
const yOffset = index * captureHeight;
|
|
243
|
+
// For the last screenshot, we need to crop from the bottom
|
|
244
|
+
if (isLast && lastCaptureHeight < captureHeight) {
|
|
245
|
+
return {
|
|
246
|
+
input: buffer,
|
|
247
|
+
top: yOffset,
|
|
248
|
+
left: 0,
|
|
249
|
+
// We'll handle the cropping separately
|
|
250
|
+
};
|
|
251
|
+
}
|
|
186
252
|
return {
|
|
187
253
|
input: buffer,
|
|
188
254
|
top: yOffset,
|
|
189
255
|
left: 0,
|
|
190
|
-
// We'll handle the cropping separately
|
|
191
256
|
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
.png()
|
|
212
|
-
.toBuffer();
|
|
213
|
-
await fs.writeFile(screenshotPath, stitchedImage);
|
|
214
|
-
// Restore original scroll position
|
|
215
|
-
if (scrollableInfo.hasScrollable && scrollableInfo.scrollableId) {
|
|
216
|
-
await page.evaluate(`
|
|
257
|
+
});
|
|
258
|
+
// Create the final stitched image
|
|
259
|
+
const finalImage = (0, sharp_1.default)({
|
|
260
|
+
create: {
|
|
261
|
+
width: viewportWidth,
|
|
262
|
+
height: totalHeight,
|
|
263
|
+
channels: 4,
|
|
264
|
+
background: { r: 255, g: 255, b: 255, alpha: 1 }
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// Composite all screenshots
|
|
268
|
+
const stitchedImage = await finalImage
|
|
269
|
+
.composite(compositeInputs)
|
|
270
|
+
.png()
|
|
271
|
+
.toBuffer();
|
|
272
|
+
await fs.writeFile(screenshotPath, stitchedImage);
|
|
273
|
+
// Restore original scroll position
|
|
274
|
+
if (scrollableInfo.hasScrollable && scrollableInfo.scrollableId) {
|
|
275
|
+
await page.evaluate(`
|
|
217
276
|
document.getElementById('${scrollableInfo.scrollableId}').scrollTop = ${scrollableInfo.scrollTop};
|
|
218
277
|
`);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
await page.evaluate(`window.scrollTo(0, ${scrollableInfo.scrollTop})`);
|
|
281
|
+
}
|
|
222
282
|
}
|
|
223
283
|
}
|
|
224
284
|
// 2. Dump full-page DOM
|
package/dist/types.d.ts
CHANGED
|
@@ -133,6 +133,8 @@ export interface TestivAIConfig {
|
|
|
133
133
|
performance?: Partial<PerformanceConfig>;
|
|
134
134
|
/** Element selectors to capture (existing option) */
|
|
135
135
|
selectors?: string[];
|
|
136
|
+
/** Use Chrome DevTools Protocol for full-page capture (default: false - uses scroll-and-stitch) */
|
|
137
|
+
useCDP?: boolean;
|
|
136
138
|
}
|
|
137
139
|
/**
|
|
138
140
|
* Layout/Bounding box data for an element
|
package/package.json
CHANGED
package/src/snapshot.ts
CHANGED
|
@@ -46,6 +46,15 @@ export async function snapshot(
|
|
|
46
46
|
const projectConfig = await loadConfig();
|
|
47
47
|
const effectiveConfig = mergeTestConfig(projectConfig, config);
|
|
48
48
|
|
|
49
|
+
// Debug: Log config
|
|
50
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
51
|
+
console.log('[TestivAI] Config:', {
|
|
52
|
+
projectConfig,
|
|
53
|
+
testConfig: config,
|
|
54
|
+
effectiveConfig
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
const outputDir = path.join(process.cwd(), '.testivai', 'temp');
|
|
50
59
|
await fs.ensureDir(outputDir);
|
|
51
60
|
|
|
@@ -54,17 +63,76 @@ export async function snapshot(
|
|
|
54
63
|
const safeName = snapshotName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
55
64
|
const baseFilename = `${timestamp}_${safeName}`;
|
|
56
65
|
|
|
57
|
-
// 1. Capture full-page screenshot
|
|
58
|
-
// This is the same technique used by GoFullPage Chrome extension
|
|
66
|
+
// 1. Capture full-page screenshot
|
|
59
67
|
const screenshotPath = path.join(outputDir, `${baseFilename}.png`);
|
|
60
68
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
// Check if CDP approach is enabled
|
|
70
|
+
if (effectiveConfig.useCDP) {
|
|
71
|
+
// Use Chrome DevTools Protocol for full-page capture
|
|
72
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
73
|
+
console.log('[TestivAI] Using CDP approach for full-page screenshot');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Create a CDP session
|
|
78
|
+
const client = await page.context().newCDPSession(page);
|
|
79
|
+
|
|
80
|
+
// Enable Page domain
|
|
81
|
+
await client.send('Page.enable');
|
|
82
|
+
|
|
83
|
+
// Get layout metrics to determine full page size
|
|
84
|
+
const layoutMetrics = await client.send('Page.getLayoutMetrics');
|
|
85
|
+
|
|
86
|
+
// Calculate full page dimensions
|
|
87
|
+
const pageWidth = Math.ceil(layoutMetrics.contentSize.width);
|
|
88
|
+
const pageHeight = Math.ceil(layoutMetrics.contentSize.height);
|
|
89
|
+
|
|
90
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
91
|
+
console.log('[TestivAI] CDP Layout metrics:', {
|
|
92
|
+
pageWidth,
|
|
93
|
+
pageHeight,
|
|
94
|
+
viewportWidth: layoutMetrics.layoutViewport.clientWidth,
|
|
95
|
+
viewportHeight: layoutMetrics.layoutViewport.clientHeight
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Capture screenshot with captureBeyondViewport: true
|
|
100
|
+
const screenshot = await client.send('Page.captureScreenshot', {
|
|
101
|
+
format: 'png',
|
|
102
|
+
captureBeyondViewport: true,
|
|
103
|
+
clip: {
|
|
104
|
+
x: 0,
|
|
105
|
+
y: 0,
|
|
106
|
+
width: pageWidth,
|
|
107
|
+
height: pageHeight,
|
|
108
|
+
scale: 1
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Save the screenshot
|
|
113
|
+
await fs.writeFile(screenshotPath, Buffer.from(screenshot.data, 'base64'));
|
|
114
|
+
|
|
115
|
+
// Close CDP session
|
|
116
|
+
await client.detach();
|
|
117
|
+
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
console.error('[TestivAI] CDP screenshot failed:', error.message);
|
|
120
|
+
// Fallback to regular screenshot
|
|
121
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Use scroll-and-stitch approach (default)
|
|
125
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
126
|
+
console.log('[TestivAI] Using scroll-and-stitch approach for full-page screenshot');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get viewport dimensions
|
|
130
|
+
const viewport = page.viewportSize();
|
|
131
|
+
const viewportWidth = viewport?.width || 1280;
|
|
132
|
+
const viewportHeight = viewport?.height || 720;
|
|
133
|
+
|
|
134
|
+
// Find the main scrollable container and get its dimensions
|
|
135
|
+
const scrollableInfo = await page.evaluate(`
|
|
68
136
|
(function() {
|
|
69
137
|
var mainScrollable = null;
|
|
70
138
|
var maxScrollHeight = 0;
|
|
@@ -213,6 +281,7 @@ export async function snapshot(
|
|
|
213
281
|
} else {
|
|
214
282
|
await page.evaluate(`window.scrollTo(0, ${scrollableInfo.scrollTop})`);
|
|
215
283
|
}
|
|
284
|
+
}
|
|
216
285
|
}
|
|
217
286
|
|
|
218
287
|
// 2. Dump full-page DOM
|
package/src/types.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface TestivAIConfig {
|
|
|
141
141
|
performance?: Partial<PerformanceConfig>;
|
|
142
142
|
/** Element selectors to capture (existing option) */
|
|
143
143
|
selectors?: string[];
|
|
144
|
+
/** Use Chrome DevTools Protocol for full-page capture (default: false - uses scroll-and-stitch) */
|
|
145
|
+
useCDP?: boolean;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
/**
|