UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

761 lines 35.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BrowserUtils = void 0; const promises_1 = __importDefault(require("node:fs/promises")); const child_process_1 = require("child_process"); const os_1 = __importDefault(require("os")); const path_1 = __importDefault(require("path")); const playwright_1 = require("playwright"); const util_1 = require("util"); const envVars_1 = require("../envVars"); const ChromeNotInstalledException_1 = require("../exceptions/ChromeNotInstalledException"); const InvalidParamValueException_1 = require("../exceptions/InvalidParamValueException"); const Logger_1 = require("./Logger"); const sleep = (0, util_1.promisify)(setTimeout); /** * Utility class for managing browser instances and contexts across different browser types and configurations. * * Provides comprehensive browser automation capabilities including: * - Device emulation with pre-configured browser/device combinations * - Remote browser instance connection via Chrome DevTools Protocol (CDP) * - BrowserBase cloud browser integration * - Complete storage state management (cookies, localStorage, sessionStorage) * - Proxy configuration and environment variable integration * - Native Chrome installation support via CDP for "Desktop Chrome" device * * Supports multiple browser engines: Chromium, Chrome, Firefox, WebKit, and iOS Safari. * * @example * ```typescript * // Create a browser context with device emulation * const config: BrowserConfig = { * using: { type: 'device', deviceName: 'iPhone 13' } * }; * const context = await BrowserUtils.create(config, './output'); * * // Use native Chrome installation (special case) * const chromeConfig: BrowserConfig = { * using: { type: 'device', deviceName: 'Desktop Chrome' } * }; * const chromeContext = await BrowserUtils.create(chromeConfig, './output'); * * // Get supported devices * const devices = BrowserUtils.getSupportedDevices(); * * // Extract storage state * const storageState = await BrowserUtils.getBrowserStorageState(context); * ``` * * @see {@link BrowserConfig} for configuration options * @see {@link BrowserStorageState} for storage state structure * @see {@link BrowserDevice} for device configuration format */ class BrowserUtils { /** * Loads browser device configurations from Playwright's built-in devices * plus the local `assets/devices.json` overrides. * * The returned map keys the devices by their name (ex: 'Desktop Firefox'). * * See `assets/devices.json` for details. * * @returns A Map containing device configurations keyed by device name * @throws {Error} When the devices configuration file cannot be loaded */ static getSupportedDevices() { return BrowserUtils.SUPPORTED_DEVICES; } /** * Creates a browser context based on the provided configuration. * Supports different browser types including device emulation, remote instances, and BrowserBase. * * Special case: When deviceName is "Desktop Chrome", launches the user's native Chrome * installation and connects via CDP instead of using Playwright's bundled browser. * * @param browserConfig - Configuration object specifying browser type and settings. * @param videoDir - If present, record video and store the artifacts in this directory. * @param storageState - Optional browser storage state to restore (cookies, localStorage, sessionStorage). * @returns A promise that resolves to a configured BrowserContext. * @throws {InvalidParamValueException} When an invalid browser type is specified. */ static async create(browserConfig, videoDir, storageState, environ = envVars_1.env.pick('BROWSERBASE_API_KEY', 'PROXY_SERVER', 'PROXY_USERNAME', 'PROXY_PASSWORD')) { const type = browserConfig.using.type; let browserContext; switch (type) { case 'device': { const deviceName = browserConfig.using.deviceName ?? BrowserUtils.DEFAULT_DEVICE_NAME; // Special case for Desktop Chrome - use native Chrome installation via CDP if (deviceName === BrowserUtils.DESKTOP_CHROME_DEVICE_NAME) { browserContext = await BrowserUtils.forNativeChrome(browserConfig.using.headless ?? false, storageState, browserConfig.using.proxy, environ); } else { browserContext = await BrowserUtils.forDevice(deviceName, browserConfig.using.headless ?? false, videoDir, storageState, browserConfig.using.proxy, environ); } break; } case 'remoteInstance': { browserContext = await BrowserUtils.forRemoteBrowser(browserConfig.using.url, videoDir, storageState); break; } case 'browserBase': { const browserBaseResult = await BrowserUtils.forBrowserBase(browserConfig.using.sessionArgs, videoDir, storageState, environ); browserContext = browserBaseResult.browserContext; // Patch browser.close because the BrowserBase session maps to the browser lifecycle. const originalBrowserClose = browserBaseResult.browser.close.bind(browserBaseResult.browser); browserBaseResult.browser.close = async () => { try { await originalBrowserClose(); } catch (_error) { // Ignore, the browser may have already been closed and that is fine. } const body = { projectId: browserBaseResult.browserBaseData.projectId, status: 'REQUEST_RELEASE', }; const password = environ.data.BROWSERBASE_API_KEY ?? ''; const options = { method: 'POST', headers: { 'X-BB-API-Key': password, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }; try { const resp = await fetch(`https://api.browserbase.com/v1/sessions/${browserBaseResult.browserBaseData.id}`, options); if (!resp.ok) { Logger_1.appLogger.warn(`Failed to release BrowserBase session '${browserBaseResult.browserBaseData.id}' due to error: ${resp.statusText}`); } } catch (error) { // Ignore, BrowserBase sessions expire automatically anyway. Logger_1.appLogger.warn(`Failed to release BrowserBase session '${browserBaseResult.browserBaseData.id}'`, error); } }; break; } default: { throw new InvalidParamValueException_1.InvalidParamValueException('type', type); } } await BrowserUtils.attachSessionStorageToBrowserContext(browserContext, storageState); return browserContext; } /** * Gets the browser storage state including cookies, localStorage, and sessionStorage. * * @param browserContext - The browser context to extract storage state from * @returns A promise that resolves to the complete browser storage state */ static async getBrowserStorageState(browserContext) { let result; try { // First get the standard storage state (cookies and localStorage) result = await browserContext.storageState({ indexedDB: true, }); } catch (error) { Logger_1.appLogger.warn('Failed to get storage state with indexedDB, falling back to cookies only', error); result = await browserContext.storageState({ indexedDB: false, }); } // Get all pages in the context const pages = browserContext.pages(); // Process each page to collect sessionStorage data for (const page of pages) { const pageUrl = page.url().trim(); try { if (pageUrl.length === 0 || pageUrl.startsWith('about:')) { // Skip pages that might have navigation errors or are about:blank. continue; } // Get the origin for the current page let pageOrigin; try { pageOrigin = new URL(pageUrl).origin; } catch { // Skip! continue; } // Find if we already have an entry for this origin let originEntry = result.origins.find((entry) => entry.origin === pageOrigin); // If not, create a new entry if (!originEntry) { originEntry = { origin: pageOrigin, localStorage: [], sessionStorage: [], }; result.origins.push(originEntry); } else if (!('sessionStorage' in originEntry)) { // If the entry exists but doesn't have sessionStorage yet, add the property originEntry.sessionStorage = []; } // Extract sessionStorage from the page const sessionStorageItems = await page.evaluate(() => { const items = []; for (let i = 0; i < sessionStorage.length; i++) { const name = sessionStorage.key(i); if (name) { items.push({ name, value: sessionStorage.getItem(name) || '', }); } } return items; }); // Add sessionStorage items to the origin entry originEntry.sessionStorage = sessionStorageItems; } catch (error) { Logger_1.appLogger.warn(`Failed to extract sessionStorage for page: ${pageUrl}`, error); continue; } } return result; } /** * Launches the user's native Chrome installation and connects to it via CDP. * This provides access to the user's real Chrome profile, extensions, and settings. * * @param headless - Whether to run Chrome in headless mode * @param storageState - Optional browser storage state to restore * @param proxy - Optional proxy configuration for the browser * @throws {Error} When Chrome cannot be launched or CDP connection fails */ static async forNativeChrome(headless, storageState, proxy, environ) { // Always use a dedicated user-data-dir to avoid handing off to an existing Chrome. const userDataDir = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'donobu-chrome-')); // Chrome will choose a free port and write it into DevToolsActivePort in userDataDir. const chromeArgs = [ `--user-data-dir=${userDataDir}`, '--remote-debugging-port=0', '--disable-field-trial-config', '--disable-background-networking', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-back-forward-cache', '--disable-breakpad', '--disable-client-side-phishing-detection', '--disable-component-extensions-with-background-pages', '--disable-component-update', '--no-default-browser-check', '--disable-default-apps', '--disable-dev-shm-usage', '--disable-features=AcceptCHFrame,AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,AutomationControlled', '--enable-features=CDPScreenshotNewSurface', '--allow-pre-commit-input', '--disable-hang-monitor', '--disable-ipc-flooding-protection', '--disable-popup-blocking', '--disable-prompt-on-repost', '--disable-renderer-backgrounding', '--force-color-profile=srgb', '--metrics-recording-only', '--no-first-run', '--password-store=basic', '--use-mock-keychain', '--no-service-autorun', '--export-tagged-pdf', '--disable-search-engine-choice-screen', '--unsafely-disable-devtools-self-xss-warnings', '--edge-skip-compat-layer-relaunch', '--enable-unsafe-swiftshader', 'about:blank', ]; if (headless) { chromeArgs.push('--headless=new'); } const expandedProxy = BrowserUtils.expandProxyConfiguration(proxy, environ); if (expandedProxy) { chromeArgs.push(`--proxy-server=${expandedProxy.server}`); if (expandedProxy.bypass) { chromeArgs.push(`--proxy-bypass-list=${expandedProxy.bypass}`); } } Logger_1.appLogger.info(`Launching Chrome with args: ${chromeArgs.join(' ')}`); const chromeProcess = await BrowserUtils.launchNativeChrome(chromeArgs); // Helper: read DevToolsActivePort written by Chrome when --remote-debugging-port=0 is used const readDevToolsPort = async (maxAttempts = 200, delayMs = 100) => { const file = path_1.default.join(userDataDir, 'DevToolsActivePort'); for (let i = 0; i < maxAttempts; i++) { try { const contents = (await promises_1.default.readFile(file, 'utf8')).trim().split('\n'); // Format: first line is the port, second is the WebSocket path. const port = Number(contents[0]); if (!Number.isNaN(port)) { return port; } } catch { /* file not ready yet */ } await sleep(delayMs); } throw new Error('DevToolsActivePort not found; Chrome did not expose CDP'); }; let browser; try { const port = await readDevToolsPort(); const cdpUrl = `http://localhost:${port}`; Logger_1.appLogger.info(`Connecting to Chrome CDP at: ${cdpUrl}`); browser = await playwright_1.chromium.connectOverCDP(cdpUrl); // IMPORTANT: Reuse the default (already-open) non-incognito context. // Calling browser.newContext() would create an incognito context -> extra window. const existingContexts = browser.contexts(); if (existingContexts.length === 0) { throw new Error('No default Chrome context found after CDP connect'); } const browserContext = existingContexts[0]; // Apply storage state (cookies) if provided. if (storageState?.cookies?.length) { try { await browserContext.addCookies(storageState.cookies); } catch (e) { Logger_1.appLogger.warn('Failed to add cookies to existing Chrome context', e); } } // (sessionStorage/localStorage restoration happens via attachSessionStorageToBrowserContext later) // Patch close: close Playwright connection, then Chrome, then remove temp profile. const originalClose = browser.close.bind(browser); browser.close = async () => { try { await originalClose(); } catch (e) { Logger_1.appLogger.warn('Error closing browser connection', e); } try { if (!chromeProcess.killed) { chromeProcess.kill('SIGTERM'); await sleep(1000); if (!chromeProcess.killed) { chromeProcess.kill('SIGKILL'); } } } catch (e) { Logger_1.appLogger.warn('Error terminating Chrome process', e); } try { await promises_1.default.rm(userDataDir, { recursive: true, force: true }); } catch (e) { Logger_1.appLogger.warn('Error removing temp userDataDir', e); } }; return browserContext; } catch (error) { try { if (browser) { await browser.close(); } } catch { } try { if (!chromeProcess.killed) { chromeProcess.kill('SIGKILL'); } } catch { } throw error; } } /** * Launches the native Chrome browser with the specified arguments. * Attempts to find Chrome in common installation locations across different platforms. * * @param args - Command line arguments to pass to Chrome * @returns The spawned Chrome process * @throws {Error} When Chrome executable cannot be found or launched */ static async launchNativeChrome(args) { const chromePaths = BrowserUtils.getChromePaths(); for (const chromePath of chromePaths) { try { Logger_1.appLogger.info(`Attempting to launch Chrome from: ${chromePath}`); const process = (0, child_process_1.spawn)(chromePath, args, { detached: false, stdio: ['ignore', 'pipe', 'pipe'], // Capture stdout and stderr }); // Set up error handling for the process process.on('error', (error) => { Logger_1.appLogger.error(`Chrome process error: ${error.message}`); }); process.stdout?.on('data', (data) => { Logger_1.appLogger.debug(`Chrome stdout: ${data.toString()}`); }); process.stderr?.on('data', (data) => { const message = data.toString(); // Don't log as error if it's just informational Chrome output if (message.includes('DevTools listening on')) { Logger_1.appLogger.info(`Chrome: ${message.trim()}`); } else { Logger_1.appLogger.debug(`Chrome stderr: ${message}`); } }); // Wait a bit to see if the process starts successfully await sleep(1000); // Increased wait time if (!process.killed && process.pid) { Logger_1.appLogger.info(`Launched Chrome from: ${chromePath} (PID: ${process.pid})`); return process; } else { Logger_1.appLogger.warn(`Chrome process died immediately for path: ${chromePath}`); } } catch (error) { Logger_1.appLogger.debug(`Failed to launch Chrome from ${chromePath}:`, error); continue; } } throw new ChromeNotInstalledException_1.ChromeNotInstalledException(); } /** * Returns an array of possible Chrome executable paths for the current platform. * * @returns Array of potential Chrome executable paths */ static getChromePaths() { const platform = process.platform; switch (platform) { case 'win32': return [ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe', ].filter(Boolean); case 'darwin': return [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', '/usr/bin/google-chrome', ]; case 'linux': return [ '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium-browser', '/usr/bin/chromium', '/snap/bin/chromium', ]; default: return ['/usr/bin/google-chrome', '/usr/bin/chromium']; } } /** * Connects to an existing Chromium browser using the Chrome DevTools Protocol (CDP) at the given URL. * * @param remoteBrowserInstanceUrl - The CDP endpoint URL of the remote browser instancevideoDir * @param storageState - Optional browser storage state to restore * @throws {InvalidParamValueException} When the remote browser URL is invalid or connection fails * @private */ static async forRemoteBrowser(remoteBrowserInstanceUrl, videoDir, storageState) { try { const browser = await playwright_1.chromium.connectOverCDP(remoteBrowserInstanceUrl); try { const contextOptions = { ...(videoDir ? { recordVideo: { dir: videoDir } } : {}), }; if (storageState) { contextOptions.storageState = storageState; } return await browser.newContext(contextOptions); } catch (error) { await browser.close(); throw error; } } catch (_) { throw new InvalidParamValueException_1.InvalidParamValueException('remoteBrowserInstanceUrl', remoteBrowserInstanceUrl); } } /** * Creates a browser and context for a specific device configuration. * If {@link storageState} is present, must be an object conforming to what is returned by * {@link BrowserContext.storageState()}. * * @param deviceName - Name of the device configuration to use * @param headless - Whether to run the browser in headless mode * @param videoDir - If present, record video and store the artifacts in this directory. * @param storageState - Optional browser storage state to restore * @param proxy - Optional proxy configuration for the browser * @throws {InvalidParamValueException} When the device name is not found in supported devices */ static async forDevice(deviceName, headless, videoDir, storageState, proxy, environ) { const { browserTypeName, browserContextOptions } = this.browserContextOptionsForDevice(deviceName, videoDir); if (storageState) { browserContextOptions.storageState = storageState; } const launchOptions = { headless, args: [ '--ignore-certificate-errors', '--disable-blink-features=AutomationControlled', ], proxy: this.expandProxyConfiguration(proxy, environ), }; return await this.newBrowser(browserTypeName, launchOptions, browserContextOptions); } /** * Builds browser context options for a specific device configuration. * * @param deviceName - Name of the device configuration to use. * @param videoDir - If present, record video and store the artifacts in this directory. * @returns An object containing the browser type name and context options. * @throws {InvalidParamValueException} When the device name is not found in supported devices. */ static browserContextOptionsForDevice(deviceName, videoDir) { const browserDevice = BrowserUtils.getSupportedDevices().get(deviceName); if (!browserDevice) { throw new InvalidParamValueException_1.InvalidParamValueException('deviceName', deviceName); } const browserContextOptions = { userAgent: browserDevice.userAgent, ...(videoDir ? { recordVideo: { dir: videoDir, size: { width: browserDevice.viewport?.width ?? 1280, height: browserDevice.viewport?.height ?? 720, }, }, } : {}), viewport: { width: browserDevice.viewport?.width ?? 1280, height: browserDevice.viewport?.height ?? 720, }, screen: { width: browserDevice.screen?.width ?? browserDevice.viewport?.width ?? 1280, height: browserDevice.screen?.height ?? browserDevice.viewport?.height ?? 720, }, deviceScaleFactor: browserDevice.deviceScaleFactor ?? 1.0, isMobile: browserDevice.isMobile ?? false, hasTouch: browserDevice.hasTouch ?? false, permissions: browserDevice.permissions, }; return { browserTypeName: browserDevice.defaultBrowserType.toLowerCase(), browserContextOptions: browserContextOptions, }; } /** * Returns the Playwright browser engine ('chromium', 'firefox', or 'webkit') * required to run the given device name. * * 'chrome' and 'ios' (used internally by Playwright devices) are normalised * to 'chromium' and 'webkit' respectively, so callers always receive one of * the three canonical engine names. * * @throws {InvalidParamValueException} When deviceName is not in the supported device list. */ static getBrowserTypeForDeviceName(deviceName) { const { browserTypeName } = BrowserUtils.browserContextOptionsForDevice(deviceName); if (browserTypeName === 'chrome') { return 'chromium'; } if (browserTypeName === 'ios') { return 'webkit'; } return browserTypeName; } /** * Expands proxy configuration by merging provided proxy settings with environment variables. * Environment variables serve as fallbacks for missing proxy configuration values. * * @param proxy - Optional proxy configuration object * @returns Expanded proxy configuration or undefined if no proxy is configured */ static expandProxyConfiguration(proxy, environ) { const envProxyServer = environ.data.PROXY_SERVER; const envProxyUsername = environ.data.PROXY_USERNAME; const envProxyPassword = environ.data.PROXY_PASSWORD; if (!proxy) { return envProxyServer !== undefined ? { server: envProxyServer, username: envProxyUsername, password: envProxyPassword, } : undefined; } return { server: proxy.server, bypass: proxy.bypass, username: proxy.username ?? envProxyUsername, password: proxy.password ?? envProxyPassword, }; } /** * Creates a new browser instance of the specified type with given launch and context options. * Handles cleanup by closing the browser if context creation fails. * * @param browserTypeName - The type of browser to launch * @param launchOptions - Options for launching the browser * @param browserContextOptions - Options for creating the browser context * @throws {InvalidParamValueException} When an unsupported browser type is specified */ static async newBrowser(browserTypeName, launchOptions, browserContextOptions) { let browser; switch (browserTypeName) { case 'firefox': browser = await playwright_1.firefox.launch(launchOptions); break; case 'chromium': browser = await playwright_1.chromium.launch(launchOptions); break; case 'chrome': browser = await playwright_1.chromium.launch({ ...launchOptions, channel: 'chrome', }); break; case 'webkit': case 'ios': browser = await playwright_1.webkit.launch(launchOptions); break; default: throw new InvalidParamValueException_1.InvalidParamValueException('browserType', browserTypeName); } try { return await browser.newContext(browserContextOptions); } catch (error) { await browser.close(); throw error; } } /** * Creates a BrowserBase session. Using this method requires the * BROWSERBASE_API_KEY environment variable to be set. * * The returned browserBaseData object conforms to the response of the session * creation API endpoint. See... * https://docs.browserbase.com/reference/api/create-a-session#response-id * * @param sessionArgs - Arguments for creating the BrowserBase session. * @param videoDir - If present, record video and store the artifacts in this directory. * @param storageState - Optional browser storage state to restore. * @returns A promise that resolves to an object containing the browser, context, and session data. * @throws {InvalidParamValueException} When BrowserBase API key is missing or session creation fails. */ static async forBrowserBase(sessionArgs, videoDir, storageState, environ) { const browserBaseData = await BrowserUtils.establishBrowserBaseSession(sessionArgs, environ); const browser = await playwright_1.chromium.connectOverCDP(browserBaseData.connectUrl); const contextOptions = { ...(videoDir ? { recordVideo: { dir: videoDir } } : {}), }; if (storageState) { contextOptions.storageState = storageState; } return { browser: browser, browserContext: await browser.newContext(contextOptions), browserBaseData: browserBaseData, }; } /** * Establishes a BrowserBase session. The returned structure matches the * response structure from the BrowserBase session API. See... * https://docs.browserbase.com/reference/api/create-a-session#response-id * * @param sessionArgs - Arguments for creating the BrowserBase session * @returns A promise that resolves to the BrowserBase session data * @throws {InvalidParamValueException} When the API key is missing or the API returns an error */ static async establishBrowserBaseSession(sessionArgs, environ) { const password = environ.data.BROWSERBASE_API_KEY; if (!password) { throw new InvalidParamValueException_1.InvalidParamValueException(environ.keys.BROWSERBASE_API_KEY, null); } const options = { method: 'POST', headers: { 'X-BB-API-Key': password, 'Content-Type': 'application/json' }, body: JSON.stringify(sessionArgs), }; const browserBaseData = await fetch('https://api.browserbase.com/v1/sessions', options).then((response) => response.json()); if (browserBaseData.error) { throw new InvalidParamValueException_1.InvalidParamValueException(environ.keys.BROWSERBASE_API_KEY, '*** REDACTED ***', `${browserBaseData.error}: ${browserBaseData.message}`); } return browserBaseData; } /** * Attaches sessionStorage data to a browser context by adding an initialization script. * The script will restore sessionStorage items for each origin when pages are loaded. * * @param browserContext - The browser context to attach sessionStorage to * @param storageState - Optional browser storage state containing sessionStorage data */ static async attachSessionStorageToBrowserContext(browserContext, storageState) { // Add init script to restore sessionStorage if storage state is provided if (storageState?.origins) { // Transform the storage state to map origins to their sessionStorage const sessionStorageByOrigin = {}; for (const origin of storageState.origins) { if (origin.sessionStorage && origin.sessionStorage.length > 0) { // Create a key-value map for this origin's sessionStorage const sessionStorageMap = {}; for (const item of origin.sessionStorage) { sessionStorageMap[item.name] = item.value; } sessionStorageByOrigin[origin.origin] = sessionStorageMap; } } // Add the init script to restore sessionStorage based on the page's origin await browserContext.addInitScript((storageData) => { // Get current origin const currentOrigin = window.location.origin; // Check if we have sessionStorage data for this origin if (storageData[currentOrigin]) { // Restore the sessionStorage items for (const [key, value] of Object.entries(storageData[currentOrigin])) { window.sessionStorage.setItem(key, value); } console.log(`Restored ${Object.keys(storageData[currentOrigin]).length} sessionStorage items for ${currentOrigin}`); } }, sessionStorageByOrigin); } } } exports.BrowserUtils = BrowserUtils; BrowserUtils.SUPPORTED_DEVICES = new Map([ ...Object.entries(playwright_1.devices), // We map 'Desktop Chrome' to these Chromium variants because we treat // the normal 'Desktop Chrome' device name to detect if someone wants to // use their normal Chrome installation, not the special Playwright version. [ 'Desktop Chromium', { ...playwright_1.devices['Desktop Chrome'], deviceScaleFactor: 2, }, ], [ 'Desktop Chromium with Media', { ...playwright_1.devices['Desktop Chrome'], permissions: ['geolocation', 'camera', 'microphone'], deviceScaleFactor: 2, }, ], ]); BrowserUtils.DEFAULT_DEVICE_NAME = 'Desktop Chromium'; BrowserUtils.DESKTOP_CHROME_DEVICE_NAME = 'Desktop Chrome'; //# sourceMappingURL=BrowserUtils.js.map