donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
302 lines • 13.3 kB
JavaScript
;
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