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