cbrowser 2.3.0 → 2.4.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/dist/browser.d.ts +97 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +488 -2
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +389 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +31 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +42 -6
- package/dist/config.js.map +1 -1
- package/dist/types.d.ts +181 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +87 -0
- package/dist/types.js.map +1 -1
- package/package.json +9 -2
package/dist/browser.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* AI-powered browser automation with constitutional safety.
|
|
5
5
|
*/
|
|
6
6
|
import { type CBrowserConfig } from "./config.js";
|
|
7
|
-
import type { NavigationResult, ClickResult, ExtractResult, JourneyResult, CleanupOptions, CleanupResult, JourneyOptions } from "./types.js";
|
|
7
|
+
import type { SavedSession, NavigationResult, ClickResult, ExtractResult, JourneyResult, CleanupOptions, CleanupResult, JourneyOptions, NetworkMock, NetworkRequest, PerformanceMetrics, PerformanceAuditResult } from "./types.js";
|
|
8
8
|
export declare class CBrowser {
|
|
9
9
|
private config;
|
|
10
10
|
private paths;
|
|
@@ -12,6 +12,10 @@ export declare class CBrowser {
|
|
|
12
12
|
private context;
|
|
13
13
|
private page;
|
|
14
14
|
private currentPersona;
|
|
15
|
+
private networkRequests;
|
|
16
|
+
private networkResponses;
|
|
17
|
+
private harEntries;
|
|
18
|
+
private isRecordingHar;
|
|
15
19
|
constructor(userConfig?: Partial<CBrowserConfig>);
|
|
16
20
|
/**
|
|
17
21
|
* Launch the browser.
|
|
@@ -25,6 +29,98 @@ export declare class CBrowser {
|
|
|
25
29
|
* Get the current page, launching if needed.
|
|
26
30
|
*/
|
|
27
31
|
private getPage;
|
|
32
|
+
/**
|
|
33
|
+
* Set up network mocks for API interception.
|
|
34
|
+
*/
|
|
35
|
+
private setupNetworkMocks;
|
|
36
|
+
/**
|
|
37
|
+
* Add a network mock at runtime.
|
|
38
|
+
*/
|
|
39
|
+
addNetworkMock(mock: NetworkMock): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Clear all network mocks.
|
|
42
|
+
*/
|
|
43
|
+
clearNetworkMocks(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Set up network request/response tracking for HAR.
|
|
46
|
+
*/
|
|
47
|
+
private setupNetworkTracking;
|
|
48
|
+
/**
|
|
49
|
+
* Start recording HAR.
|
|
50
|
+
*/
|
|
51
|
+
startHarRecording(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Stop recording and export HAR.
|
|
54
|
+
*/
|
|
55
|
+
exportHar(outputPath?: string): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Get all captured network requests.
|
|
58
|
+
*/
|
|
59
|
+
getNetworkRequests(): NetworkRequest[];
|
|
60
|
+
/**
|
|
61
|
+
* Clear network request history.
|
|
62
|
+
*/
|
|
63
|
+
clearNetworkHistory(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Collect Core Web Vitals and performance metrics.
|
|
66
|
+
*/
|
|
67
|
+
getPerformanceMetrics(): Promise<PerformanceMetrics>;
|
|
68
|
+
/**
|
|
69
|
+
* Run a performance audit against a budget.
|
|
70
|
+
*/
|
|
71
|
+
auditPerformance(url?: string): Promise<PerformanceAuditResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Get all cookies for the current context.
|
|
74
|
+
*/
|
|
75
|
+
getCookies(urls?: string[]): Promise<SavedSession["cookies"]>;
|
|
76
|
+
/**
|
|
77
|
+
* Set cookies.
|
|
78
|
+
*/
|
|
79
|
+
setCookies(cookies: SavedSession["cookies"]): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Clear all cookies.
|
|
82
|
+
*/
|
|
83
|
+
clearCookies(): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Delete specific cookies by name.
|
|
86
|
+
*/
|
|
87
|
+
deleteCookie(name: string, domain?: string): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Get the path to the video file (after browser closes).
|
|
90
|
+
*/
|
|
91
|
+
getVideoPath(): Promise<string | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Save the video with a custom filename.
|
|
94
|
+
*/
|
|
95
|
+
saveVideo(outputPath: string): Promise<string | null>;
|
|
96
|
+
/**
|
|
97
|
+
* Set device emulation (requires browser restart).
|
|
98
|
+
*/
|
|
99
|
+
setDevice(deviceName: string): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* List available device presets.
|
|
102
|
+
*/
|
|
103
|
+
static listDevices(): string[];
|
|
104
|
+
/**
|
|
105
|
+
* Set geolocation (requires browser restart or use setGeolocationRuntime).
|
|
106
|
+
*/
|
|
107
|
+
setGeolocation(location: string | {
|
|
108
|
+
latitude: number;
|
|
109
|
+
longitude: number;
|
|
110
|
+
accuracy?: number;
|
|
111
|
+
}): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Set geolocation at runtime without restarting.
|
|
114
|
+
*/
|
|
115
|
+
setGeolocationRuntime(location: string | {
|
|
116
|
+
latitude: number;
|
|
117
|
+
longitude: number;
|
|
118
|
+
accuracy?: number;
|
|
119
|
+
}): Promise<boolean>;
|
|
120
|
+
/**
|
|
121
|
+
* List available location presets.
|
|
122
|
+
*/
|
|
123
|
+
static listLocations(): string[];
|
|
28
124
|
/**
|
|
29
125
|
* Navigate to a URL.
|
|
30
126
|
*/
|
package/dist/browser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,KAAK,cAAc,EAAgE,MAAM,aAAa,CAAC;AAEhH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
package/dist/browser.js
CHANGED
|
@@ -11,6 +11,7 @@ const fs_1 = require("fs");
|
|
|
11
11
|
const path_1 = require("path");
|
|
12
12
|
const config_js_1 = require("./config.js");
|
|
13
13
|
const personas_js_1 = require("./personas.js");
|
|
14
|
+
const types_js_1 = require("./types.js");
|
|
14
15
|
class CBrowser {
|
|
15
16
|
config;
|
|
16
17
|
paths;
|
|
@@ -18,6 +19,10 @@ class CBrowser {
|
|
|
18
19
|
context = null;
|
|
19
20
|
page = null;
|
|
20
21
|
currentPersona = null;
|
|
22
|
+
networkRequests = [];
|
|
23
|
+
networkResponses = new Map();
|
|
24
|
+
harEntries = [];
|
|
25
|
+
isRecordingHar = false;
|
|
21
26
|
constructor(userConfig = {}) {
|
|
22
27
|
this.config = (0, config_js_1.mergeConfig)(userConfig);
|
|
23
28
|
this.paths = (0, config_js_1.ensureDirectories)((0, config_js_1.getPaths)(this.config.dataDir));
|
|
@@ -40,13 +45,77 @@ class CBrowser {
|
|
|
40
45
|
this.browser = await browserType.launch({
|
|
41
46
|
headless: this.config.headless,
|
|
42
47
|
});
|
|
43
|
-
|
|
48
|
+
// Build context options
|
|
49
|
+
const contextOptions = {
|
|
44
50
|
viewport: {
|
|
45
51
|
width: this.config.viewportWidth,
|
|
46
52
|
height: this.config.viewportHeight,
|
|
47
53
|
},
|
|
48
|
-
}
|
|
54
|
+
};
|
|
55
|
+
// Apply device emulation if configured
|
|
56
|
+
if (this.config.device && types_js_1.DEVICE_PRESETS[this.config.device]) {
|
|
57
|
+
const device = types_js_1.DEVICE_PRESETS[this.config.device];
|
|
58
|
+
contextOptions.viewport = device.viewport;
|
|
59
|
+
contextOptions.userAgent = device.userAgent;
|
|
60
|
+
contextOptions.deviceScaleFactor = device.deviceScaleFactor;
|
|
61
|
+
contextOptions.isMobile = device.isMobile;
|
|
62
|
+
contextOptions.hasTouch = device.hasTouch;
|
|
63
|
+
}
|
|
64
|
+
else if (this.config.deviceDescriptor) {
|
|
65
|
+
const device = this.config.deviceDescriptor;
|
|
66
|
+
contextOptions.viewport = device.viewport;
|
|
67
|
+
contextOptions.userAgent = device.userAgent;
|
|
68
|
+
contextOptions.deviceScaleFactor = device.deviceScaleFactor;
|
|
69
|
+
contextOptions.isMobile = device.isMobile;
|
|
70
|
+
contextOptions.hasTouch = device.hasTouch;
|
|
71
|
+
}
|
|
72
|
+
// Apply custom user agent if set (overrides device)
|
|
73
|
+
if (this.config.userAgent) {
|
|
74
|
+
contextOptions.userAgent = this.config.userAgent;
|
|
75
|
+
}
|
|
76
|
+
// Apply geolocation if configured
|
|
77
|
+
if (this.config.geolocation) {
|
|
78
|
+
contextOptions.geolocation = {
|
|
79
|
+
latitude: this.config.geolocation.latitude,
|
|
80
|
+
longitude: this.config.geolocation.longitude,
|
|
81
|
+
accuracy: this.config.geolocation.accuracy,
|
|
82
|
+
};
|
|
83
|
+
contextOptions.permissions = ["geolocation"];
|
|
84
|
+
}
|
|
85
|
+
// Apply locale if configured
|
|
86
|
+
if (this.config.locale) {
|
|
87
|
+
contextOptions.locale = this.config.locale;
|
|
88
|
+
}
|
|
89
|
+
// Apply timezone if configured
|
|
90
|
+
if (this.config.timezone) {
|
|
91
|
+
contextOptions.timezoneId = this.config.timezone;
|
|
92
|
+
}
|
|
93
|
+
// Apply color scheme if configured
|
|
94
|
+
if (this.config.colorScheme) {
|
|
95
|
+
contextOptions.colorScheme = this.config.colorScheme;
|
|
96
|
+
}
|
|
97
|
+
// Enable video recording if configured
|
|
98
|
+
if (this.config.recordVideo) {
|
|
99
|
+
const videoDir = this.config.videoDir || (0, path_1.join)(this.paths.dataDir, "videos");
|
|
100
|
+
if (!(0, fs_1.existsSync)(videoDir)) {
|
|
101
|
+
(0, fs_1.mkdirSync)(videoDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
contextOptions.recordVideo = {
|
|
104
|
+
dir: videoDir,
|
|
105
|
+
size: {
|
|
106
|
+
width: contextOptions.viewport?.width || 1280,
|
|
107
|
+
height: contextOptions.viewport?.height || 800,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
49
112
|
this.page = await this.context.newPage();
|
|
113
|
+
// Apply network mocks if configured
|
|
114
|
+
if (this.config.networkMocks && this.config.networkMocks.length > 0) {
|
|
115
|
+
await this.setupNetworkMocks(this.config.networkMocks);
|
|
116
|
+
}
|
|
117
|
+
// Set up network request/response tracking for HAR
|
|
118
|
+
this.setupNetworkTracking();
|
|
50
119
|
}
|
|
51
120
|
/**
|
|
52
121
|
* Close the browser.
|
|
@@ -69,6 +138,423 @@ class CBrowser {
|
|
|
69
138
|
return this.page;
|
|
70
139
|
}
|
|
71
140
|
// =========================================================================
|
|
141
|
+
// Network Mocking
|
|
142
|
+
// =========================================================================
|
|
143
|
+
/**
|
|
144
|
+
* Set up network mocks for API interception.
|
|
145
|
+
*/
|
|
146
|
+
async setupNetworkMocks(mocks) {
|
|
147
|
+
const page = this.page;
|
|
148
|
+
for (const mock of mocks) {
|
|
149
|
+
const pattern = typeof mock.urlPattern === "string"
|
|
150
|
+
? new RegExp(mock.urlPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
151
|
+
: mock.urlPattern;
|
|
152
|
+
await page.route(pattern, async (route) => {
|
|
153
|
+
const request = route.request();
|
|
154
|
+
// Check method match if specified
|
|
155
|
+
if (mock.method && request.method() !== mock.method.toUpperCase()) {
|
|
156
|
+
await route.continue();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Handle abort
|
|
160
|
+
if (mock.abort) {
|
|
161
|
+
await route.abort();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Apply delay if specified
|
|
165
|
+
if (mock.delay) {
|
|
166
|
+
await new Promise((r) => setTimeout(r, mock.delay));
|
|
167
|
+
}
|
|
168
|
+
// Fulfill with mock response
|
|
169
|
+
const body = typeof mock.body === "object"
|
|
170
|
+
? JSON.stringify(mock.body)
|
|
171
|
+
: mock.body || "";
|
|
172
|
+
await route.fulfill({
|
|
173
|
+
status: mock.status || 200,
|
|
174
|
+
headers: mock.headers || { "Content-Type": "application/json" },
|
|
175
|
+
body,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Add a network mock at runtime.
|
|
182
|
+
*/
|
|
183
|
+
async addNetworkMock(mock) {
|
|
184
|
+
const page = await this.getPage();
|
|
185
|
+
await this.setupNetworkMocks([mock]);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Clear all network mocks.
|
|
189
|
+
*/
|
|
190
|
+
async clearNetworkMocks() {
|
|
191
|
+
const page = await this.getPage();
|
|
192
|
+
await page.unrouteAll();
|
|
193
|
+
}
|
|
194
|
+
// =========================================================================
|
|
195
|
+
// Network Tracking & HAR Export
|
|
196
|
+
// =========================================================================
|
|
197
|
+
/**
|
|
198
|
+
* Set up network request/response tracking for HAR.
|
|
199
|
+
*/
|
|
200
|
+
setupNetworkTracking() {
|
|
201
|
+
if (!this.page)
|
|
202
|
+
return;
|
|
203
|
+
this.page.on("request", (request) => {
|
|
204
|
+
const networkRequest = {
|
|
205
|
+
url: request.url(),
|
|
206
|
+
method: request.method(),
|
|
207
|
+
headers: request.headers(),
|
|
208
|
+
postData: request.postData() || undefined,
|
|
209
|
+
resourceType: request.resourceType(),
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
211
|
+
};
|
|
212
|
+
this.networkRequests.push(networkRequest);
|
|
213
|
+
if (this.isRecordingHar) {
|
|
214
|
+
// Start HAR entry
|
|
215
|
+
const harEntry = {
|
|
216
|
+
startedDateTime: new Date().toISOString(),
|
|
217
|
+
request: {
|
|
218
|
+
method: request.method(),
|
|
219
|
+
url: request.url(),
|
|
220
|
+
httpVersion: "HTTP/1.1",
|
|
221
|
+
headers: Object.entries(request.headers()).map(([name, value]) => ({ name, value })),
|
|
222
|
+
queryString: [],
|
|
223
|
+
headersSize: -1,
|
|
224
|
+
bodySize: request.postData()?.length || 0,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
this.networkResponses.set(request.url() + request.method(), harEntry);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
this.page.on("response", async (response) => {
|
|
231
|
+
const key = response.url() + response.request().method();
|
|
232
|
+
const networkResponse = {
|
|
233
|
+
url: response.url(),
|
|
234
|
+
status: response.status(),
|
|
235
|
+
statusText: response.statusText(),
|
|
236
|
+
headers: response.headers(),
|
|
237
|
+
};
|
|
238
|
+
if (this.isRecordingHar && this.networkResponses.has(key)) {
|
|
239
|
+
const partial = this.networkResponses.get(key);
|
|
240
|
+
const startTime = new Date(partial.startedDateTime).getTime();
|
|
241
|
+
const endTime = Date.now();
|
|
242
|
+
const harEntry = {
|
|
243
|
+
...partial,
|
|
244
|
+
time: endTime - startTime,
|
|
245
|
+
response: {
|
|
246
|
+
status: response.status(),
|
|
247
|
+
statusText: response.statusText(),
|
|
248
|
+
httpVersion: "HTTP/1.1",
|
|
249
|
+
headers: Object.entries(response.headers()).map(([name, value]) => ({ name, value })),
|
|
250
|
+
content: {
|
|
251
|
+
size: parseInt(response.headers()["content-length"] || "0"),
|
|
252
|
+
mimeType: response.headers()["content-type"] || "application/octet-stream",
|
|
253
|
+
},
|
|
254
|
+
redirectURL: response.headers()["location"] || "",
|
|
255
|
+
headersSize: -1,
|
|
256
|
+
bodySize: parseInt(response.headers()["content-length"] || "-1"),
|
|
257
|
+
},
|
|
258
|
+
cache: {},
|
|
259
|
+
timings: {
|
|
260
|
+
blocked: 0,
|
|
261
|
+
dns: -1,
|
|
262
|
+
connect: -1,
|
|
263
|
+
send: 0,
|
|
264
|
+
wait: endTime - startTime,
|
|
265
|
+
receive: 0,
|
|
266
|
+
ssl: -1,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
this.harEntries.push(harEntry);
|
|
270
|
+
this.networkResponses.delete(key);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Start recording HAR.
|
|
276
|
+
*/
|
|
277
|
+
startHarRecording() {
|
|
278
|
+
this.isRecordingHar = true;
|
|
279
|
+
this.harEntries = [];
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Stop recording and export HAR.
|
|
283
|
+
*/
|
|
284
|
+
async exportHar(outputPath) {
|
|
285
|
+
this.isRecordingHar = false;
|
|
286
|
+
const harLog = {
|
|
287
|
+
version: "1.2",
|
|
288
|
+
creator: { name: "CBrowser", version: "2.4.0" },
|
|
289
|
+
entries: this.harEntries,
|
|
290
|
+
};
|
|
291
|
+
const har = { log: harLog };
|
|
292
|
+
const harDir = (0, path_1.join)(this.paths.dataDir, "har");
|
|
293
|
+
if (!(0, fs_1.existsSync)(harDir)) {
|
|
294
|
+
(0, fs_1.mkdirSync)(harDir, { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
const filename = outputPath || (0, path_1.join)(harDir, `har-${Date.now()}.har`);
|
|
297
|
+
(0, fs_1.writeFileSync)(filename, JSON.stringify(har, null, 2));
|
|
298
|
+
return filename;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get all captured network requests.
|
|
302
|
+
*/
|
|
303
|
+
getNetworkRequests() {
|
|
304
|
+
return [...this.networkRequests];
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Clear network request history.
|
|
308
|
+
*/
|
|
309
|
+
clearNetworkHistory() {
|
|
310
|
+
this.networkRequests = [];
|
|
311
|
+
this.harEntries = [];
|
|
312
|
+
}
|
|
313
|
+
// =========================================================================
|
|
314
|
+
// Performance Metrics
|
|
315
|
+
// =========================================================================
|
|
316
|
+
/**
|
|
317
|
+
* Collect Core Web Vitals and performance metrics.
|
|
318
|
+
*/
|
|
319
|
+
async getPerformanceMetrics() {
|
|
320
|
+
const page = await this.getPage();
|
|
321
|
+
const metrics = await page.evaluate(() => {
|
|
322
|
+
const result = {};
|
|
323
|
+
// Navigation timing
|
|
324
|
+
const navTiming = performance.getEntriesByType("navigation")[0];
|
|
325
|
+
if (navTiming) {
|
|
326
|
+
result.ttfb = navTiming.responseStart - navTiming.requestStart;
|
|
327
|
+
result.domContentLoaded = navTiming.domContentLoadedEventEnd - navTiming.startTime;
|
|
328
|
+
result.load = navTiming.loadEventEnd - navTiming.startTime;
|
|
329
|
+
}
|
|
330
|
+
// Paint timing
|
|
331
|
+
const paintEntries = performance.getEntriesByType("paint");
|
|
332
|
+
for (const entry of paintEntries) {
|
|
333
|
+
if (entry.name === "first-contentful-paint") {
|
|
334
|
+
result.fcp = entry.startTime;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// LCP from PerformanceObserver (if available)
|
|
338
|
+
const lcpEntries = performance.getEntriesByType("largest-contentful-paint");
|
|
339
|
+
if (lcpEntries.length > 0) {
|
|
340
|
+
result.lcp = lcpEntries[lcpEntries.length - 1].startTime;
|
|
341
|
+
}
|
|
342
|
+
// CLS from layout-shift entries
|
|
343
|
+
const clsEntries = performance.getEntriesByType("layout-shift");
|
|
344
|
+
let clsScore = 0;
|
|
345
|
+
for (const entry of clsEntries) {
|
|
346
|
+
if (!entry.hadRecentInput) {
|
|
347
|
+
clsScore += entry.value || 0;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
result.cls = clsScore;
|
|
351
|
+
// Resource counts
|
|
352
|
+
const resources = performance.getEntriesByType("resource");
|
|
353
|
+
result.resourceCount = resources.length;
|
|
354
|
+
result.transferSize = resources.reduce((sum, r) => sum + (r.transferSize || 0), 0);
|
|
355
|
+
return result;
|
|
356
|
+
});
|
|
357
|
+
// Rate the metrics
|
|
358
|
+
const lcpRating = metrics.lcp
|
|
359
|
+
? metrics.lcp <= 2500 ? "good" : metrics.lcp <= 4000 ? "needs-improvement" : "poor"
|
|
360
|
+
: undefined;
|
|
361
|
+
const clsRating = metrics.cls !== undefined
|
|
362
|
+
? metrics.cls <= 0.1 ? "good" : metrics.cls <= 0.25 ? "needs-improvement" : "poor"
|
|
363
|
+
: undefined;
|
|
364
|
+
return {
|
|
365
|
+
lcp: metrics.lcp,
|
|
366
|
+
cls: metrics.cls,
|
|
367
|
+
fcp: metrics.fcp,
|
|
368
|
+
ttfb: metrics.ttfb,
|
|
369
|
+
domContentLoaded: metrics.domContentLoaded,
|
|
370
|
+
load: metrics.load,
|
|
371
|
+
resourceCount: metrics.resourceCount,
|
|
372
|
+
transferSize: metrics.transferSize,
|
|
373
|
+
lcpRating: lcpRating,
|
|
374
|
+
clsRating: clsRating,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Run a performance audit against a budget.
|
|
379
|
+
*/
|
|
380
|
+
async auditPerformance(url) {
|
|
381
|
+
if (url) {
|
|
382
|
+
await this.navigate(url);
|
|
383
|
+
}
|
|
384
|
+
const page = await this.getPage();
|
|
385
|
+
const metrics = await this.getPerformanceMetrics();
|
|
386
|
+
const budget = this.config.performanceBudget;
|
|
387
|
+
const violations = [];
|
|
388
|
+
let passed = true;
|
|
389
|
+
if (budget) {
|
|
390
|
+
if (budget.lcp && metrics.lcp && metrics.lcp > budget.lcp) {
|
|
391
|
+
violations.push(`LCP ${metrics.lcp}ms exceeds budget ${budget.lcp}ms`);
|
|
392
|
+
passed = false;
|
|
393
|
+
}
|
|
394
|
+
if (budget.fcp && metrics.fcp && metrics.fcp > budget.fcp) {
|
|
395
|
+
violations.push(`FCP ${metrics.fcp}ms exceeds budget ${budget.fcp}ms`);
|
|
396
|
+
passed = false;
|
|
397
|
+
}
|
|
398
|
+
if (budget.cls && metrics.cls && metrics.cls > budget.cls) {
|
|
399
|
+
violations.push(`CLS ${metrics.cls} exceeds budget ${budget.cls}`);
|
|
400
|
+
passed = false;
|
|
401
|
+
}
|
|
402
|
+
if (budget.ttfb && metrics.ttfb && metrics.ttfb > budget.ttfb) {
|
|
403
|
+
violations.push(`TTFB ${metrics.ttfb}ms exceeds budget ${budget.ttfb}ms`);
|
|
404
|
+
passed = false;
|
|
405
|
+
}
|
|
406
|
+
if (budget.transferSize && metrics.transferSize && metrics.transferSize > budget.transferSize) {
|
|
407
|
+
violations.push(`Transfer size ${metrics.transferSize}B exceeds budget ${budget.transferSize}B`);
|
|
408
|
+
passed = false;
|
|
409
|
+
}
|
|
410
|
+
if (budget.resourceCount && metrics.resourceCount && metrics.resourceCount > budget.resourceCount) {
|
|
411
|
+
violations.push(`Resource count ${metrics.resourceCount} exceeds budget ${budget.resourceCount}`);
|
|
412
|
+
passed = false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
url: page.url(),
|
|
417
|
+
timestamp: new Date().toISOString(),
|
|
418
|
+
metrics,
|
|
419
|
+
budget,
|
|
420
|
+
passed,
|
|
421
|
+
violations,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
// =========================================================================
|
|
425
|
+
// Cookie Management
|
|
426
|
+
// =========================================================================
|
|
427
|
+
/**
|
|
428
|
+
* Get all cookies for the current context.
|
|
429
|
+
*/
|
|
430
|
+
async getCookies(urls) {
|
|
431
|
+
if (!this.context) {
|
|
432
|
+
await this.launch();
|
|
433
|
+
}
|
|
434
|
+
return await this.context.cookies(urls);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Set cookies.
|
|
438
|
+
*/
|
|
439
|
+
async setCookies(cookies) {
|
|
440
|
+
if (!this.context) {
|
|
441
|
+
await this.launch();
|
|
442
|
+
}
|
|
443
|
+
await this.context.addCookies(cookies);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Clear all cookies.
|
|
447
|
+
*/
|
|
448
|
+
async clearCookies() {
|
|
449
|
+
if (!this.context)
|
|
450
|
+
return;
|
|
451
|
+
await this.context.clearCookies();
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Delete specific cookies by name.
|
|
455
|
+
*/
|
|
456
|
+
async deleteCookie(name, domain) {
|
|
457
|
+
const cookies = await this.getCookies();
|
|
458
|
+
const filtered = cookies.filter((c) => {
|
|
459
|
+
if (c.name !== name)
|
|
460
|
+
return true;
|
|
461
|
+
if (domain && c.domain !== domain)
|
|
462
|
+
return true;
|
|
463
|
+
return false;
|
|
464
|
+
});
|
|
465
|
+
await this.clearCookies();
|
|
466
|
+
if (filtered.length > 0) {
|
|
467
|
+
await this.setCookies(filtered);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// =========================================================================
|
|
471
|
+
// Video Recording
|
|
472
|
+
// =========================================================================
|
|
473
|
+
/**
|
|
474
|
+
* Get the path to the video file (after browser closes).
|
|
475
|
+
*/
|
|
476
|
+
async getVideoPath() {
|
|
477
|
+
if (!this.page)
|
|
478
|
+
return null;
|
|
479
|
+
const video = this.page.video();
|
|
480
|
+
if (!video)
|
|
481
|
+
return null;
|
|
482
|
+
return await video.path();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Save the video with a custom filename.
|
|
486
|
+
*/
|
|
487
|
+
async saveVideo(outputPath) {
|
|
488
|
+
if (!this.page)
|
|
489
|
+
return null;
|
|
490
|
+
const video = this.page.video();
|
|
491
|
+
if (!video)
|
|
492
|
+
return null;
|
|
493
|
+
await video.saveAs(outputPath);
|
|
494
|
+
return outputPath;
|
|
495
|
+
}
|
|
496
|
+
// =========================================================================
|
|
497
|
+
// Device Emulation
|
|
498
|
+
// =========================================================================
|
|
499
|
+
/**
|
|
500
|
+
* Set device emulation (requires browser restart).
|
|
501
|
+
*/
|
|
502
|
+
setDevice(deviceName) {
|
|
503
|
+
if (types_js_1.DEVICE_PRESETS[deviceName]) {
|
|
504
|
+
this.config.device = deviceName;
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* List available device presets.
|
|
511
|
+
*/
|
|
512
|
+
static listDevices() {
|
|
513
|
+
return Object.keys(types_js_1.DEVICE_PRESETS);
|
|
514
|
+
}
|
|
515
|
+
// =========================================================================
|
|
516
|
+
// Geolocation
|
|
517
|
+
// =========================================================================
|
|
518
|
+
/**
|
|
519
|
+
* Set geolocation (requires browser restart or use setGeolocationRuntime).
|
|
520
|
+
*/
|
|
521
|
+
setGeolocation(location) {
|
|
522
|
+
if (typeof location === "string") {
|
|
523
|
+
if (types_js_1.LOCATION_PRESETS[location]) {
|
|
524
|
+
this.config.geolocation = types_js_1.LOCATION_PRESETS[location];
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
this.config.geolocation = location;
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Set geolocation at runtime without restarting.
|
|
534
|
+
*/
|
|
535
|
+
async setGeolocationRuntime(location) {
|
|
536
|
+
if (!this.context)
|
|
537
|
+
return false;
|
|
538
|
+
let geo;
|
|
539
|
+
if (typeof location === "string") {
|
|
540
|
+
if (!types_js_1.LOCATION_PRESETS[location])
|
|
541
|
+
return false;
|
|
542
|
+
geo = types_js_1.LOCATION_PRESETS[location];
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
geo = location;
|
|
546
|
+
}
|
|
547
|
+
await this.context.setGeolocation(geo);
|
|
548
|
+
await this.context.grantPermissions(["geolocation"]);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* List available location presets.
|
|
553
|
+
*/
|
|
554
|
+
static listLocations() {
|
|
555
|
+
return Object.keys(types_js_1.LOCATION_PRESETS);
|
|
556
|
+
}
|
|
557
|
+
// =========================================================================
|
|
72
558
|
// Navigation
|
|
73
559
|
// =========================================================================
|
|
74
560
|
/**
|