UNPKG

donobu

Version:

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

302 lines 13.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PlaywrightUtils = void 0; // Disable font readiness checks for Playwright screenshots. Must be set before // any screenshot call, and must live here (not in a side-effect init module) so // that library consumers who import PlaywrightUtils always get the fix. process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY = '1'; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const playwright_core_1 = require("playwright-core"); const dialog_prompt_tracker_1 = require("../browser-side-scripts/dialog-prompt-tracker"); const donobu_namespace_1 = require("../browser-side-scripts/donobu-namespace"); const interactive_elements_tracker_1 = require("../browser-side-scripts/interactive-elements-tracker"); const smart_selector_generator_1 = require("../browser-side-scripts/smart-selector-generator"); const envVars_1 = require("../envVars"); const PageClosedException_1 = require("../exceptions/PageClosedException"); const Logger_1 = require("./Logger"); const MiscUtils_1 = require("./MiscUtils"); /** * Miscellaneous utility functions for working with the Playwright SDK. If you are looking to * instantiate a Playwright instance, see PlaywrightSetup instead. */ class PlaywrightUtils { static get BLANK_JPEG() { PlaywrightUtils._blankJpeg ??= (0, fs_1.readFileSync)(MiscUtils_1.MiscUtils.getResourceFilePath('no-screenshot.jpeg')); return PlaywrightUtils._blankJpeg; } static get BLANK_PNG() { PlaywrightUtils._blankPng ??= (0, fs_1.readFileSync)(MiscUtils_1.MiscUtils.getResourceFilePath('no-screenshot.png')); return PlaywrightUtils._blankPng; } /** * Takes a screenshot of the given page, returning the raw byte array. * * Playwright's native `page.screenshot()` hangs when the renderer is * blocked (synchronous JS, mid-navigation, font loading, etc.). To avoid * this, two capture strategies race in parallel against a shared timeout: * * 1. **Playwright native** — highest quality, wins when the renderer is * responsive. * 2. **CDP `Page.captureScreenshot`** — captures from the browser's * compositor layer, which remains available even when the renderer is * blocked. Chromium-only; silently drops out on Firefox/WebKit. * * If neither strategy resolves within `timeoutMs`, a static fallback image * ("No screenshot available.") is returned from the assets directory. This * makes the method effectively infallible — it always returns a `Buffer` * unless the page is closed, in which case {@link PageClosedException} is * thrown. */ static async takeViewportScreenshot(page, options = { timeout: envVars_1.env.data.SCREENSHOT_TIMEOUT_MS, type: 'jpeg', }) { if (page.isClosed()) { throw new PageClosedException_1.PageClosedException(); } const timeoutMs = options.timeout ?? envVars_1.env.data.SCREENSHOT_TIMEOUT_MS; // Helper: swallow errors so a failing tier never wins the race — it just // becomes a promise that never settles. const neverOnError = (promise) => promise.catch((error) => { if (PlaywrightUtils.isPageClosedError(error)) { throw error; } return new Promise(() => { }); }); // Tier 1: Playwright native screenshot. const tier1 = neverOnError(page.screenshot(options)); // Tier 2: CDP compositor capture (Chromium only). const cdpFormat = options.type === 'png' ? 'png' : 'jpeg'; const tier2 = neverOnError((async () => { const cdp = await page.context().newCDPSession(page); try { const cdpOptions = { format: cdpFormat }; if (cdpFormat === 'jpeg') { cdpOptions.quality = 80; } const { data } = await cdp.send('Page.captureScreenshot', cdpOptions); return Buffer.from(data, 'base64'); } finally { await cdp.detach().catch(() => { }); } })()); // Race the real tiers against a shared timeout. try { return await Promise.race([ tier1, tier2, new Promise((_, reject) => { setTimeout(() => reject(new Error(`Screenshot timed out after ${timeoutMs}ms`)), timeoutMs); }), ]); } catch (error) { if (PlaywrightUtils.isPageClosedError(error)) { throw new PageClosedException_1.PageClosedException(); } Logger_1.appLogger.warn(`All screenshot strategies failed, returning placeholder: ${error.message}`); return options.type === 'png' ? PlaywrightUtils.BLANK_PNG : PlaywrightUtils.BLANK_JPEG; } } /** * Generate valid selectors for the given element. The generated selectors are * in a priority order based on their ability to identify the given element. * For example, an unambiguous selector that matches the element exactly are * first in the list, and weaker selectors that match multiple elements are * last. * * NOTE: Using Locator['evaluate'] as the type for 'evaluate' as it is * conveniently compatible with Element['evaluate'], and we want to be * able to generate selectors when passed either a Locator or Element. */ static async generateSelectors(element) { return element.evaluate((elem) => { return window.__donobu.generateSmartSelectors(elem); }); } /** * Sets up the given browser context so that it can be minimally used by the * rest of Donobu. This involves registering critical page initialization * scripts. */ static async setupBasicBrowserContext(browserContext) { await browserContext.addInitScript(donobu_namespace_1.installDonobuNamespace); await browserContext.addInitScript(interactive_elements_tracker_1.installInteractiveElementsTracker); await browserContext.addInitScript(dialog_prompt_tracker_1.installDialogPromptTracker); await browserContext.addInitScript(smart_selector_generator_1.installSmartSelectorGenerator); } /** * Returned true IFF the given error is a Playwright error regarding page closing, * of if the given error is an instance of {@link PageClosedException}. */ static isPageClosedError(error) { if (error instanceof PageClosedException_1.PageClosedException) { return true; } else { const exceptionMessage = error?.message?.toLowerCase(); if (!exceptionMessage) { return false; } else { return (exceptionMessage.includes('detached') || exceptionMessage.includes('context was destroyed') || exceptionMessage.includes('browser has been closed') || exceptionMessage.includes('no longer existing')); } } } static async ensurePlaywrightInstallation() { return PlaywrightUtils.runPlaywrightCli(['install', '--with-deps']); } static async ensureChromiumInstallation() { return PlaywrightUtils.runPlaywrightCli([ 'install', '--with-deps', 'chromium', ]); } static async ensureFirefoxInstallation() { return PlaywrightUtils.runPlaywrightCli([ 'install', '--with-deps', 'firefox', ]); } static async ensureWebkitInstallation() { return PlaywrightUtils.runPlaywrightCli([ 'install', '--with-deps', 'webkit', ]); } static async isBrowserInstalled(browserType) { try { const browsers = { chromium: playwright_core_1.chromium, firefox: playwright_core_1.firefox, webkit: playwright_core_1.webkit }; const executablePath = browsers[browserType].executablePath(); return (0, fs_1.existsSync)(executablePath); } catch { return false; } } static async ensureBrowserReady(browserType) { if (await PlaywrightUtils.isBrowserInstalled(browserType)) { return; } let promise = PlaywrightUtils._browserInstallPromises.get(browserType); if (!promise) { const installFn = browserType === 'chromium' ? PlaywrightUtils.ensureChromiumInstallation : browserType === 'firefox' ? PlaywrightUtils.ensureFirefoxInstallation : PlaywrightUtils.ensureWebkitInstallation; promise = installFn().finally(() => { PlaywrightUtils._browserInstallPromises.delete(browserType); }); PlaywrightUtils._browserInstallPromises.set(browserType, promise); } await promise; } static async runPlaywrightCli(args) { try { // First, resolve the package root const packagePath = require.resolve('playwright-core/package.json'); // Then construct the path to cli.js const cliPath = path_1.default.join(path_1.default.dirname(packagePath), 'cli.js'); Logger_1.appLogger.debug(`Found Playwright CLI at: ${cliPath}`); Logger_1.appLogger.info(`Running Playwright CLI with args: ${args.join(' ')}`); const env = { ...process.env, PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS: '1', PLAYWRIGHT_SKIP_BROWSER_GC: '1', PLAYWRIGHT_SKIP_BROWSER_VALIDATION: '1', FORCE_COLOR: '0', NODE_NO_WARNINGS: '1', ELECTRON_RUN_AS_NODE: '1', }; const childProcess = (0, child_process_1.spawn)(process.execPath, [cliPath, ...args], { stdio: 'inherit', env: env, windowsHide: true, }); return new Promise((resolve, reject) => { childProcess.on('exit', (code) => { if (code === 0) { Logger_1.appLogger.debug('Playwright CLI completed successfully'); resolve(); } else { reject(new Error(`Playwright CLI failed with code ${code}`)); } }); childProcess.on('error', (error) => { reject(new Error(`Failed to execute Playwright CLI: ${error.message}`)); }); }); } catch (error) { throw new Error(`Failed to initialize Playwright CLI: ${error instanceof Error ? error.message : String(error)}`); } } /** * Attempts to wait until the currently focused page is stable. If the page * never stabilizes, it just returns after timing out. If any error occurs, * it is logged and ignored. If page is null, this function has no effect. */ static async waitForPageStability(page) { if (page) { try { await Promise.all([ page.waitForLoadState('load', { timeout: envVars_1.env.data.DONOBU_MAX_STABILITY_WAIT_MS, }), page.waitForTimeout(envVars_1.env.data.DONOBU_MIN_STABILITY_WAIT_MS), ]); } catch (error) { if (!PlaywrightUtils.isPageClosedError(error)) { // Pass, just move on and hope for the best, we waited long enough. (0, Logger_1.logErrorWithoutStack)(`${page.url()} is taking its dear time to reach a steady state, moving on...`, error, 'warn'); } } } } /** * Returns true IFF if the given selector is an xpath-based selector. */ static isXpathSelector(selector) { const s = selector.trim(); // Detect XPath: // - starts with // or .// // - OR starts with "(" followed by optional whitespace and then // or .// const isXPath = s.startsWith('xpath=') || s.startsWith('//') || s.startsWith('.//') || /^\(\s*(\.?\/\/)/.test(s); return isXPath; } static normalizeSelector(selector) { const s = selector.trim(); // If it's already using a Playwright engine, leave it. // e.g., "xpath=...", "css=...", "text=...", "id=..." if (/^[a-z-]+=/i.test(s)) { return s; } else { return PlaywrightUtils.isXpathSelector(s) ? `xpath=${s}` : s; } } } exports.PlaywrightUtils = PlaywrightUtils; // Per-browser in-flight install promises — deduplicate concurrent requests. PlaywrightUtils._browserInstallPromises = new Map(); //# sourceMappingURL=PlaywrightUtils.js.map