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 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
  */
@@ -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,EAEV,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,aAAa,EAKb,cAAc,EACd,aAAa,EACb,cAAc,EAEf,MAAM,YAAY,CAAC;AAEpB,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,cAAc,CAAwB;gBAElC,UAAU,GAAE,OAAO,CAAC,cAAc,CAAM;IASpD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB7B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B;;OAEG;YACW,OAAO;IAWrB;;OAEG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsCtD;;OAEG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IA+CtF;;OAEG;IACG,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAiCjE;;OAEG;YACW,WAAW;IAuDzB;;OAEG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA8DnD;;OAEG;IACG,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYhD;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6C9C;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2CjD;;OAEG;IACH,YAAY,IAAI,MAAM,EAAE;IAKxB;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAapC;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAqH9D;;OAEG;IACH,OAAO,CAAC,aAAa;IAarB;;OAEG;IACH,OAAO,CAAC,cAAc;IAkCtB;;OAEG;IACH,OAAO,CAAC,KAAK;IA8Bb;;OAEG;IACH,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa;IA4DpD;;OAEG;IACH,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAqBnE"}
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
- this.context = await this.browser.newContext({
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
  /**