mirror of
https://github.com/testdino-hq/playwright-skill.git
synced 2026-09-14 16:30:11 +08:00
554 lines
15 KiB
Markdown
554 lines
15 KiB
Markdown
|
|
# Running Custom Playwright Code
|
||
|
|
|
||
|
|
> **When to use**: When CLI commands aren't sufficient — geolocation, permissions, media emulation, waiting strategies, iframe interaction, file downloads, clipboard access, complex multi-step workflows, or any scenario requiring the full Playwright API.
|
||
|
|
> **Prerequisites**: [core-commands.md](core-commands.md) for basic CLI usage
|
||
|
|
|
||
|
|
## Quick Reference
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Syntax: run-code accepts an async function with page as argument
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
// Full Playwright API available here
|
||
|
|
// page.context() for browser context operations
|
||
|
|
// Return a value to see it in output
|
||
|
|
return await page.title();
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Geolocation
|
||
|
|
|
||
|
|
Override the browser's reported location — essential for testing location-based features like store locators, delivery zones, and weather apps.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Grant permission and set location to New York
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().grantPermissions(['geolocation']);
|
||
|
|
await page.context().setGeolocation({ latitude: 40.7128, longitude: -74.0060 });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# London
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().grantPermissions(['geolocation']);
|
||
|
|
await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# San Francisco
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().grantPermissions(['geolocation']);
|
||
|
|
await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Tokyo
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().grantPermissions(['geolocation']);
|
||
|
|
await page.context().setGeolocation({ latitude: 35.6762, longitude: 139.6503 });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Update location mid-session (simulates user moving)
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().setGeolocation({ latitude: 34.0522, longitude: -118.2437 });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Clear geolocation override
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().clearPermissions();
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Permissions
|
||
|
|
|
||
|
|
Control browser permission grants — notifications, camera, microphone, clipboard, etc.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Grant multiple permissions
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().grantPermissions([
|
||
|
|
'geolocation',
|
||
|
|
'notifications',
|
||
|
|
'camera',
|
||
|
|
'microphone'
|
||
|
|
]);
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Grant permissions for a specific origin only
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], {
|
||
|
|
origin: 'https://example.com'
|
||
|
|
});
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Revoke all permissions
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().clearPermissions();
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
Available permissions: `geolocation`, `notifications`, `camera`, `microphone`, `clipboard-read`, `clipboard-write`, `payment-handler`, `midi`, `midi-sysex`, `ambient-light-sensor`, `accelerometer`, `gyroscope`, `magnetometer`, `accessibility-events`, `background-sync`
|
||
|
|
|
||
|
|
## Media Emulation
|
||
|
|
|
||
|
|
Test how your app behaves under different media conditions — dark mode, reduced motion, print layout.
|
||
|
|
|
||
|
|
### Color Scheme
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Dark mode
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({ colorScheme: 'dark' });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Light mode
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({ colorScheme: 'light' });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# System preference (no override)
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({ colorScheme: 'no-preference' });
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Reduced Motion
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Simulate prefers-reduced-motion: reduce
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Reset to no preference
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Forced Colors (High Contrast)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({ forcedColors: 'active' });
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Print Media
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Emulate print stylesheet (useful before taking PDF)
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({ media: 'print' });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Reset to screen
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({ media: 'screen' });
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Combine Multiple Emulations
|
||
|
|
|
||
|
|
```bash
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.emulateMedia({
|
||
|
|
colorScheme: 'dark',
|
||
|
|
reducedMotion: 'reduce',
|
||
|
|
forcedColors: 'none'
|
||
|
|
});
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Locale and Timezone
|
||
|
|
|
||
|
|
Override browser locale and timezone to test internationalization:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Set locale (affects number formatting, date display, etc.)
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
// Locale is set at context creation; for existing context, use evaluate
|
||
|
|
return await page.evaluate(() => navigator.language);
|
||
|
|
}"
|
||
|
|
|
||
|
|
# For new contexts, set locale and timezone at open time:
|
||
|
|
# playwright-cli open --config=locale-config.json
|
||
|
|
# where locale-config.json contains: { "locale": "de-DE", "timezoneId": "Europe/Berlin" }
|
||
|
|
```
|
||
|
|
|
||
|
|
## Wait Strategies
|
||
|
|
|
||
|
|
When the page needs time to load, render, or settle after an action.
|
||
|
|
|
||
|
|
### Wait for Load States
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Wait for all network requests to finish
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Wait for DOM content loaded
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForLoadState('domcontentloaded');
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Wait for full load (including images, stylesheets)
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForLoadState('load');
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Wait for Elements
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Wait for a loading spinner to disappear
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Wait for content to appear
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForSelector('.search-results', { state: 'visible', timeout: 10000 });
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Wait for element to be removed from DOM entirely
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForSelector('.skeleton-loader', { state: 'detached' });
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Wait for URL Changes
|
||
|
|
|
||
|
|
```bash
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForURL('**/dashboard');
|
||
|
|
}"
|
||
|
|
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForURL(/.*\/order\/\d+/);
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Wait for Custom Conditions
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Wait for a JavaScript variable to be set
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForFunction(() => window.appReady === true);
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Wait for specific number of elements
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForFunction(() => document.querySelectorAll('.item').length >= 10);
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Wait with polling
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.waitForFunction(
|
||
|
|
() => document.querySelector('.status')?.textContent === 'Complete',
|
||
|
|
{ polling: 500, timeout: 30000 }
|
||
|
|
);
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Wait for Network Requests
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Wait for a specific API call to complete
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const responsePromise = page.waitForResponse('**/api/users');
|
||
|
|
await page.click('button#load-users');
|
||
|
|
const response = await responsePromise;
|
||
|
|
return { status: response.status(), url: response.url() };
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Wait for a request to be made
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const requestPromise = page.waitForRequest('**/api/submit');
|
||
|
|
await page.click('button#submit');
|
||
|
|
const request = await requestPromise;
|
||
|
|
return request.postDataJSON();
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Frames and Iframes
|
||
|
|
|
||
|
|
Interact with content inside iframes:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Click a button inside an iframe
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const frame = page.locator('iframe#my-iframe').contentFrame();
|
||
|
|
await frame.locator('button.submit').click();
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Fill a form inside an iframe
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const frame = page.locator('iframe[name=\"checkout\"]').contentFrame();
|
||
|
|
await frame.locator('input[name=\"card-number\"]').fill('4111111111111111');
|
||
|
|
await frame.locator('input[name=\"expiry\"]').fill('12/25');
|
||
|
|
await frame.locator('input[name=\"cvc\"]').fill('123');
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Get all frame URLs
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const frames = page.frames();
|
||
|
|
return frames.map(f => ({ name: f.name(), url: f.url() }));
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Nested iframes
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const outerFrame = page.locator('iframe#outer').contentFrame();
|
||
|
|
const innerFrame = outerFrame.locator('iframe#inner').contentFrame();
|
||
|
|
await innerFrame.locator('button').click();
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## File Downloads
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Trigger download and save the file
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const [download] = await Promise.all([
|
||
|
|
page.waitForEvent('download'),
|
||
|
|
page.click('a.download-link')
|
||
|
|
]);
|
||
|
|
const filename = download.suggestedFilename();
|
||
|
|
await download.saveAs('./downloads/' + filename);
|
||
|
|
return 'Downloaded: ' + filename;
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Download with custom path
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const [download] = await Promise.all([
|
||
|
|
page.waitForEvent('download'),
|
||
|
|
page.getByRole('button', { name: 'Export CSV' }).click()
|
||
|
|
]);
|
||
|
|
await download.saveAs('/tmp/export.csv');
|
||
|
|
return 'Saved to /tmp/export.csv';
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Get download URL without saving
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const [download] = await Promise.all([
|
||
|
|
page.waitForEvent('download'),
|
||
|
|
page.click('#download-btn')
|
||
|
|
]);
|
||
|
|
return download.url();
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Clipboard
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Read clipboard content
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.context().grantPermissions(['clipboard-read']);
|
||
|
|
return await page.evaluate(() => navigator.clipboard.readText());
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Write to clipboard
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!');
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Copy text from an element to clipboard
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const text = await page.locator('.api-key').textContent();
|
||
|
|
await page.evaluate(t => navigator.clipboard.writeText(t), text);
|
||
|
|
return 'Copied: ' + text;
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Page Information
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Page title
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
return await page.title();
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Current URL
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
return page.url();
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Full page HTML content
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
return await page.content();
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Viewport size
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
return page.viewportSize();
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Browser information
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
return await page.evaluate(() => ({
|
||
|
|
userAgent: navigator.userAgent,
|
||
|
|
language: navigator.language,
|
||
|
|
languages: navigator.languages,
|
||
|
|
cookiesEnabled: navigator.cookieEnabled,
|
||
|
|
onLine: navigator.onLine,
|
||
|
|
platform: navigator.platform,
|
||
|
|
screenSize: { width: screen.width, height: screen.height }
|
||
|
|
}));
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## JavaScript Execution
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Execute and return result
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
return await page.evaluate(() => {
|
||
|
|
return {
|
||
|
|
title: document.title,
|
||
|
|
url: window.location.href,
|
||
|
|
elementCount: document.querySelectorAll('*').length,
|
||
|
|
scripts: document.querySelectorAll('script').length
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Pass arguments to evaluate
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const selector = '.product-card';
|
||
|
|
const count = await page.evaluate(
|
||
|
|
sel => document.querySelectorAll(sel).length,
|
||
|
|
selector
|
||
|
|
);
|
||
|
|
return count + ' products found';
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Modify the DOM
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.evaluate(() => {
|
||
|
|
document.querySelector('.banner')?.remove();
|
||
|
|
document.body.style.zoom = '80%';
|
||
|
|
});
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Error Handling
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Try-catch for optional elements
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
try {
|
||
|
|
await page.click('.cookie-consent-accept', { timeout: 2000 });
|
||
|
|
return 'Cookie banner dismissed';
|
||
|
|
} catch (e) {
|
||
|
|
return 'No cookie banner found';
|
||
|
|
}
|
||
|
|
}"
|
||
|
|
|
||
|
|
# Retry pattern
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
||
|
|
try {
|
||
|
|
await page.click('.flaky-button', { timeout: 3000 });
|
||
|
|
return 'Clicked on attempt ' + attempt;
|
||
|
|
} catch (e) {
|
||
|
|
if (attempt === 3) throw e;
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Complex Workflows
|
||
|
|
|
||
|
|
### Login and Save Authentication State
|
||
|
|
|
||
|
|
```bash
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.goto('https://example.com/login');
|
||
|
|
await page.fill('input[name=email]', 'user@example.com');
|
||
|
|
await page.fill('input[name=password]', 'secret');
|
||
|
|
await page.click('button[type=submit]');
|
||
|
|
await page.waitForURL('**/dashboard');
|
||
|
|
await page.context().storageState({ path: 'auth.json' });
|
||
|
|
return 'Login successful, state saved to auth.json';
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scrape Data from Multiple Pages
|
||
|
|
|
||
|
|
```bash
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
const results = [];
|
||
|
|
for (let i = 1; i <= 5; i++) {
|
||
|
|
await page.goto(\`https://example.com/products?page=\${i}\`);
|
||
|
|
await page.waitForSelector('.product-card');
|
||
|
|
const items = await page.locator('.product-card').evaluateAll(cards =>
|
||
|
|
cards.map(card => ({
|
||
|
|
name: card.querySelector('.title')?.textContent?.trim(),
|
||
|
|
price: card.querySelector('.price')?.textContent?.trim(),
|
||
|
|
rating: card.querySelector('.rating')?.getAttribute('data-value')
|
||
|
|
}))
|
||
|
|
);
|
||
|
|
results.push(...items);
|
||
|
|
}
|
||
|
|
return JSON.stringify(results, null, 2);
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Fill Multi-Step Form (Wizard)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
// Step 1: Personal info
|
||
|
|
await page.fill('#firstName', 'Jane');
|
||
|
|
await page.fill('#lastName', 'Doe');
|
||
|
|
await page.fill('#email', 'jane@example.com');
|
||
|
|
await page.click('button:text(\"Next\")');
|
||
|
|
|
||
|
|
// Step 2: Address
|
||
|
|
await page.waitForSelector('#street');
|
||
|
|
await page.fill('#street', '123 Main St');
|
||
|
|
await page.fill('#city', 'Springfield');
|
||
|
|
await page.selectOption('#state', 'IL');
|
||
|
|
await page.fill('#zip', '62701');
|
||
|
|
await page.click('button:text(\"Next\")');
|
||
|
|
|
||
|
|
// Step 3: Confirm and submit
|
||
|
|
await page.waitForSelector('.review-summary');
|
||
|
|
await page.click('button:text(\"Submit\")');
|
||
|
|
await page.waitForURL('**/confirmation');
|
||
|
|
|
||
|
|
return 'Form submitted successfully';
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Infinite Scroll Data Extraction
|
||
|
|
|
||
|
|
```bash
|
||
|
|
playwright-cli run-code "async page => {
|
||
|
|
await page.goto('https://example.com/feed');
|
||
|
|
const items = [];
|
||
|
|
|
||
|
|
while (items.length < 50) {
|
||
|
|
const newItems = await page.locator('.feed-item').evaluateAll(
|
||
|
|
els => els.map(el => el.textContent.trim())
|
||
|
|
);
|
||
|
|
items.push(...newItems.filter(item => !items.includes(item)));
|
||
|
|
|
||
|
|
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
|
||
|
|
// Check if we've reached the end
|
||
|
|
const hasMore = await page.locator('.load-more').isVisible().catch(() => false);
|
||
|
|
if (!hasMore && newItems.length === items.length) break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return items.slice(0, 50);
|
||
|
|
}"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Tips
|
||
|
|
|
||
|
|
- **Return values**: Always `return` data from `run-code` to see it in the CLI output
|
||
|
|
- **`page.context()`**: Access browser context for permissions, cookies, storage, geolocation
|
||
|
|
- **Error messages**: Playwright errors include element snapshots and call logs — read them carefully
|
||
|
|
- **Combine with CLI**: Use `run-code` for setup (permissions, routes) then CLI commands for interaction
|
||
|
|
- **Async/await**: All Playwright operations are async — always `await` them
|