A high-level API to automate web browsers and comprehensive framework for web testing
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Mobile browser emulation, device-specific testing, Android automation, and Electron app automation for comprehensive cross-platform testing.
Pre-configured device descriptors for mobile and desktop browser testing.
/**
* Collection of device descriptors for browser emulation
*/
const devices: Devices;
interface Devices {
/** Device descriptors indexed by name */
[key: string]: DeviceDescriptor;
}
interface DeviceDescriptor {
/** User agent string */
userAgent: string;
/** Screen dimensions */
viewport: ViewportSize;
/** Device pixel ratio */
deviceScaleFactor: number;
/** Mobile device flag */
isMobile: boolean;
/** Touch support flag */
hasTouch: boolean;
/** Default browser type */
defaultBrowserType?: 'chromium' | 'firefox' | 'webkit';
}
interface ViewportSize {
width: number;
height: number;
}Usage Examples:
import { devices, chromium } from 'playwright';
// Launch with device emulation
const browser = await chromium.launch();
const context = await browser.newContext({
...devices['iPhone 12'],
});
const page = await context.newPage();
// Common device presets
const iphone = devices['iPhone 12'];
const ipad = devices['iPad Pro'];
const pixel = devices['Pixel 5'];
const galaxy = devices['Galaxy S9+'];
const desktopChrome = devices['Desktop Chrome'];
const desktopFirefox = devices['Desktop Firefox'];
console.log('iPhone 12 viewport:', iphone.viewport);
console.log('Is mobile:', iphone.isMobile);
console.log('Has touch:', iphone.hasTouch);
// Custom device configuration
const customDevice = {
userAgent: 'Custom Device User Agent',
viewport: { width: 375, height: 812 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true
};
const customContext = await browser.newContext(customDevice);Connect to and automate Android devices and emulators.
/**
* Android automation interface
*/
const _android: Android;
interface Android {
/** List connected Android devices */
devices(): Promise<AndroidDevice[]>;
/** Set default timeout */
setDefaultTimeout(timeout: number): void;
/** Connect to device via ADB */
connect(wsEndpoint: string, options?: AndroidConnectOptions): Promise<AndroidDevice>;
}
interface AndroidDevice {
/** Device model */
model(): string;
/** Device serial number */
serial(): string;
/** Launch application */
launchBrowser(options?: AndroidLaunchOptions): Promise<BrowserContext>;
/** Close device connection */
close(): Promise<void>;
/** Install APK */
installApk(apkPath: string): Promise<void>;
/** Get device shell */
shell(command: string): Promise<Buffer>;
/** Push file to device */
push(local: string, remote: string): Promise<void>;
/** Pull file from device */
pull(remote: string, local: string): Promise<void>;
/** Take screenshot */
screenshot(): Promise<Buffer>;
/** Get web views */
webViews(): Promise<AndroidWebView[]>;
/** Get web view by selector */
webView(selector: AndroidSelector): Promise<AndroidWebView>;
/** Tap coordinates */
tap(selector: AndroidSelector): Promise<void>;
/** Fill text */
fill(selector: AndroidSelector, text: string): Promise<void>;
/** Press key */
press(key: AndroidKey): Promise<void>;
/** Scroll */
scroll(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number): Promise<void>;
/** Wait for element */
wait(selector: AndroidSelector, options?: AndroidWaitOptions): Promise<AndroidElementInfo>;
/** Get element info */
info(selector: AndroidSelector): Promise<AndroidElementInfo>;
/** Input interface */
input: AndroidInput;
}
interface AndroidLaunchOptions {
/** Browser package name */
pkg?: string;
/** Browser activity */
activity?: string;
/** Wait for network idle */
waitForNetworkIdle?: boolean;
}
interface AndroidConnectOptions {
/** Connection timeout */
timeout?: number;
}
interface AndroidWaitOptions {
/** Wait timeout */
timeout?: number;
/** Element state to wait for */
state?: 'gone' | 'present';
}Usage Examples:
import { _android as android } from 'playwright';
// Connect to Android device
const devices = await android.devices();
const device = devices[0];
console.log('Device model:', device.model());
console.log('Serial:', device.serial());
// Launch browser on device
const context = await device.launchBrowser();
const page = await context.newPage();
await page.goto('https://example.com');
// Native Android automation
await device.tap({ text: 'Settings' });
await device.fill({ resource: 'com.android.settings:id/search' }, 'wifi');
await device.press('KEYCODE_ENTER');
// Take screenshot
const screenshot = await device.screenshot();
require('fs').writeFileSync('android-screenshot.png', screenshot);
// Work with WebViews
const webViews = await device.webViews();
if (webViews.length > 0) {
const webView = webViews[0];
const page = await webView.page();
await page.click('button');
}
await device.close();Handle Android-specific input methods and UI interactions.
interface AndroidInput {
/** Tap coordinates */
tap(point: { x: number; y: number }): Promise<void>;
/** Swipe gesture */
swipe(from: { x: number; y: number }, to: { x: number; y: number }, steps: number): Promise<void>;
/** Drag gesture */
drag(from: { x: number; y: number }, to: { x: number; y: number }, steps: number): Promise<void>;
/** Press key */
press(key: AndroidKey): Promise<void>;
/** Type text */
type(text: string): Promise<void>;
}
interface AndroidWebView {
/** Get package name */
pkg(): string;
/** Get page instance */
page(): Promise<Page>;
}
interface AndroidSocket {
/** Write data to socket */
write(data: Buffer | string): Promise<void>;
/** Close socket */
close(): Promise<void>;
}
type AndroidElementInfo = {
/** Element bounds */
bounds: { x: number; y: number; width: number; height: number };
/** Element class */
clazz: string;
/** Element description */
desc: string;
/** Package name */
pkg: string;
/** Resource ID */
res: string;
/** Element text */
text: string;
/** Element checkable */
checkable: boolean;
/** Element checked */
checked: boolean;
/** Element clickable */
clickable: boolean;
/** Element enabled */
enabled: boolean;
/** Element focusable */
focusable: boolean;
/** Element focused */
focused: boolean;
/** Element long clickable */
longClickable: boolean;
/** Element scrollable */
scrollable: boolean;
/** Element selected */
selected: boolean;
};
type AndroidSelector = {
/** Select by checkable state */
checkable?: boolean;
/** Select by checked state */
checked?: boolean;
/** Select by class name */
clazz?: string | RegExp;
/** Select by clickable state */
clickable?: boolean;
/** Select by depth */
depth?: number;
/** Select by description */
desc?: string | RegExp;
/** Select by enabled state */
enabled?: boolean;
/** Select by focusable state */
focusable?: boolean;
/** Select by focused state */
focused?: boolean;
/** Select by long clickable state */
longClickable?: boolean;
/** Select by package name */
pkg?: string | RegExp;
/** Select by resource ID */
res?: string | RegExp;
/** Select by scrollable state */
scrollable?: boolean;
/** Select by selected state */
selected?: boolean;
/** Select by text content */
text?: string | RegExp;
};
type AndroidKey =
| 'KEYCODE_UNKNOWN' | 'KEYCODE_SOFT_LEFT' | 'KEYCODE_SOFT_RIGHT' | 'KEYCODE_HOME'
| 'KEYCODE_BACK' | 'KEYCODE_CALL' | 'KEYCODE_ENDCALL' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
| 'KEYCODE_STAR' | 'KEYCODE_POUND' | 'KEYCODE_DPAD_UP' | 'KEYCODE_DPAD_DOWN' | 'KEYCODE_DPAD_LEFT' | 'KEYCODE_DPAD_RIGHT'
| 'KEYCODE_DPAD_CENTER' | 'KEYCODE_VOLUME_UP' | 'KEYCODE_VOLUME_DOWN' | 'KEYCODE_POWER' | 'KEYCODE_CAMERA'
| 'KEYCODE_CLEAR' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z'
| 'KEYCODE_COMMA' | 'KEYCODE_PERIOD' | 'KEYCODE_ALT_LEFT' | 'KEYCODE_ALT_RIGHT' | 'KEYCODE_SHIFT_LEFT' | 'KEYCODE_SHIFT_RIGHT'
| 'KEYCODE_TAB' | 'KEYCODE_SPACE' | 'KEYCODE_SYM' | 'KEYCODE_EXPLORER' | 'KEYCODE_ENVELOPE' | 'KEYCODE_ENTER'
| 'KEYCODE_DEL' | 'KEYCODE_GRAVE' | 'KEYCODE_MINUS' | 'KEYCODE_EQUALS' | 'KEYCODE_LEFT_BRACKET' | 'KEYCODE_RIGHT_BRACKET'
| 'KEYCODE_BACKSLASH' | 'KEYCODE_SEMICOLON' | 'KEYCODE_APOSTROPHE' | 'KEYCODE_SLASH' | 'KEYCODE_AT' | 'KEYCODE_NUM'
| 'KEYCODE_HEADSETHOOK' | 'KEYCODE_FOCUS' | 'KEYCODE_PLUS' | 'KEYCODE_MENU' | 'KEYCODE_NOTIFICATION' | 'KEYCODE_SEARCH'
| 'KEYCODE_MEDIA_PLAY_PAUSE' | 'KEYCODE_MEDIA_STOP' | 'KEYCODE_MEDIA_NEXT' | 'KEYCODE_MEDIA_PREVIOUS' | 'KEYCODE_MEDIA_REWIND'
| 'KEYCODE_MEDIA_FAST_FORWARD' | 'KEYCODE_MUTE';Usage Examples:
// Android input interactions
await device.input.tap({ x: 100, y: 200 });
await device.input.swipe(
{ x: 100, y: 500 },
{ x: 100, y: 200 },
10
);
// Press hardware keys
await device.input.press('KEYCODE_HOME');
await device.input.press('KEYCODE_BACK');
await device.input.press('KEYCODE_VOLUME_UP');
// Type text
await device.input.type('Hello Android');
// Complex selector
const element = await device.wait({
pkg: /com\.android\.settings/,
text: /Wi.*Fi/,
clickable: true
});
console.log('Found element:', element.text);
console.log('Element bounds:', element.bounds);
// WebView automation
const webViews = await device.webViews();
for (const webView of webViews) {
console.log('WebView package:', webView.pkg());
const page = await webView.page();
const title = await page.title();
console.log('WebView title:', title);
}Automate Electron desktop applications.
/**
* Electron automation interface
*/
const _electron: Electron;
interface Electron {
/** Launch Electron application */
launch(options: ElectronLaunchOptions): Promise<ElectronApplication>;
}
interface ElectronApplication {
/** Get BrowserWindow instance */
browserWindow(page: Page): Promise<JSHandle>;
/** Close application */
close(): Promise<void>;
/** Get application context */
context(): BrowserContext;
/** Evaluate in main process */
evaluate<R, Arg>(pageFunction: PageFunction<Arg, R>, arg: Arg): Promise<R>;
evaluate<R>(pageFunction: PageFunction<void, R>, arg?: any): Promise<R>;
/** First window */
firstWindow(options?: ElectronApplicationFirstWindowOptions): Promise<Page>;
/** Get all windows */
windows(): Page[];
/** Wait for event */
waitForEvent(event: string, optionsOrPredicate?: WaitForEventOptions): Promise<any>;
/** Get process */
process(): ChildProcess;
}
interface ElectronLaunchOptions {
/** Path to Electron app */
executablePath?: string;
/** Application arguments */
args?: string[];
/** Working directory */
cwd?: string;
/** Environment variables */
env?: { [key: string]: string | number | boolean };
/** Connection timeout */
timeout?: number;
/** Accept downloads */
acceptDownloads?: boolean;
/** Bypass CSP */
bypassCSP?: boolean;
/** Color scheme */
colorScheme?: 'light' | 'dark' | 'no-preference';
/** Extra HTTP headers */
extraHTTPHeaders?: { [key: string]: string };
/** Geolocation */
geolocation?: Geolocation;
/** HTTP credentials */
httpCredentials?: HTTPCredentials;
/** Ignore HTTPS errors */
ignoreHTTPSErrors?: boolean;
/** Locale */
locale?: string;
/** Offline mode */
offline?: boolean;
/** Permissions */
permissions?: string[];
/** Proxy */
proxy?: ProxySettings;
/** Record HAR */
recordHar?: { omitContent?: boolean; path: string };
/** Record video */
recordVideo?: { dir: string; size?: ViewportSize };
/** Reduced motion */
reducedMotion?: 'reduce' | 'no-preference';
/** Service workers */
serviceWorkers?: 'allow' | 'block';
/** Storage state */
storageState?: string | { cookies: Cookie[]; origins: any[] };
/** Strict selectors */
strictSelectors?: boolean;
/** Timezone */
timezoneId?: string;
/** User agent */
userAgent?: string;
/** Viewport */
viewport?: ViewportSize | null;
}
interface ElectronApplicationFirstWindowOptions {
/** Wait timeout */
timeout?: number;
}Usage Examples:
import { _electron as electron } from 'playwright';
// Launch Electron app
const electronApp = await electron.launch({
args: ['./my-electron-app']
});
// Get first window
const window = await electronApp.firstWindow();
await window.waitForLoadState();
// Interact with main process
const version = await electronApp.evaluate(async ({ app }) => {
return app.getVersion();
});
console.log('App version:', version);
// Interact with renderer process (window)
await window.click('#menu-file');
await window.click('#menu-open');
// Get BrowserWindow handle
const browserWindow = await electronApp.browserWindow(window);
const isMaximized = await browserWindow.evaluate(win => win.isMaximized());
console.log('Window maximized:', isMaximized);
// Handle multiple windows
const allWindows = electronApp.windows();
console.log('Window count:', allWindows.length);
// Wait for new window
electronApp.on('window', async (page) => {
console.log('New window opened:', await page.title());
});
await electronApp.close();Common patterns for mobile and device testing.
interface BrowserContext {
/** Add geolocation override */
setGeolocation(geolocation: Geolocation | null): Promise<void>;
/** Set offline mode */
setOffline(offline: boolean): Promise<void>;
/** Override permissions */
grantPermissions(permissions: string[], options?: BrowserContextGrantPermissionsOptions): Promise<void>;
/** Clear permissions */
clearPermissions(): Promise<void>;
}
interface Page {
/** Emulate media features */
emulateMedia(options?: PageEmulateMediaOptions): Promise<void>;
/** Set viewport size */
setViewportSize(viewportSize: ViewportSize): Promise<void>;
}
interface PageEmulateMediaOptions {
/** Media type */
media?: 'screen' | 'print' | null;
/** Color scheme */
colorScheme?: 'light' | 'dark' | 'no-preference' | null;
/** Reduced motion */
reducedMotion?: 'reduce' | 'no-preference' | null;
/** Forced colors */
forcedColors?: 'active' | 'none' | null;
}
interface BrowserContextGrantPermissionsOptions {
/** Origin for permissions */
origin?: string;
}Usage Examples:
// Test responsive design
const context = await browser.newContext({
...devices['iPhone 12'],
viewport: { width: 375, height: 812 }
});
const page = await context.newPage();
await page.goto('https://example.com');
// Test different orientations
await page.setViewportSize({ width: 812, height: 375 }); // Landscape
await page.screenshot({ path: 'landscape.png' });
await page.setViewportSize({ width: 375, height: 812 }); // Portrait
await page.screenshot({ path: 'portrait.png' });
// Test geolocation
await context.setGeolocation({
latitude: 37.7749,
longitude: -122.4194
});
await context.grantPermissions(['geolocation']);
// Test offline behavior
await context.setOffline(true);
await page.reload();
await context.setOffline(false);
// Test dark mode
await page.emulateMedia({ colorScheme: 'dark' });
await page.screenshot({ path: 'dark-mode.png' });
// Test print media
await page.emulateMedia({ media: 'print' });
await page.screenshot({ path: 'print-view.png' });
// Test reduced motion
await page.emulateMedia({ reducedMotion: 'reduce' });interface Geolocation {
/** Latitude coordinate */
latitude: number;
/** Longitude coordinate */
longitude: number;
/** Location accuracy in meters */
accuracy?: number;
}
interface ProxySettings {
/** Proxy server URL */
server: string;
/** Bypass proxy for these patterns */
bypass?: string;
/** Username for authentication */
username?: string;
/** Password for authentication */
password?: string;
}
interface WaitForEventOptions {
/** Event timeout */
timeout?: number;
/** Event predicate */
predicate?: Function;
}
type PageFunction<Arg, R> = string | ((arg: Arg) => R | Promise<R>);Install with Tessl CLI
npx tessl i tessl/npm-playwright