playwright-skill
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePlaywright Web Testing & Automation
Playwright Web测试与自动化
Comprehensive web testing skill using Playwright. Write custom JavaScript code for any testing or automation task.
基于Playwright的全功能Web测试工具,可编写自定义JavaScript代码实现任何测试或自动化任务。
Decision Tree: Choosing Your Approach
决策树:选择合适的实现方案
User task → Is server already running?
├─ Yes → Direct Testing
│ ├─ Static HTML? → Navigate directly (file:// or http://)
│ └─ Dynamic webapp? → Use Reconnaissance-Then-Action pattern
│
└─ No → Server Management Required
├─ Single server → Start server, then test
└─ Multiple servers → Start all servers, coordinate testingUser task → Is server already running?
├─ Yes → Direct Testing
│ ├─ Static HTML? → Navigate directly (file:// or http://)
│ └─ Dynamic webapp? → Use Reconnaissance-Then-Action pattern
│
└─ No → Server Management Required
├─ Single server → Start server, then test
└─ Multiple servers → Start all servers, coordinate testingCRITICAL WORKFLOW
核心工作流
- CheckTesting if server is running - Detect running dev servers OR start servers if needed
- Write scripts to /tmp - NEVER write test files to skill directory; always use
/tmp/playwright-test-*.js - Use visible browser by default - Always use unless user specifically requests headless mode
headless: false - Wait for dynamic content - Use before inspecting dynamic webapps
waitForLoadState('networkidle') - Parameterize URLs - Always make URLs configurable via constant at top of script
- 检查服务器运行状态 - 检测正在运行的开发服务器,必要时启动服务器
- 将脚本写入/tmp目录 - 绝对不要将测试文件写入工具目录,始终使用
/tmp/playwright-test-*.js - 默认使用可见浏览器 - 始终使用 除非用户明确要求无头模式
headless: false - 等待动态内容加载 - 在检查动态Web应用前调用
waitForLoadState('networkidle') - URL参数化 - 始终将URL定义为脚本顶部的常量方便修改
Reconnaissance-Then-Action Pattern
先探测后执行模式
For dynamic webapps where you don't know the DOM structure upfront:
javascript
// /tmp/playwright-test-reconnaissance.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3000';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// STEP 1: Navigate and wait for dynamic content
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle'); // CRITICAL for dynamic apps
// STEP 2: Reconnaissance - discover what's on the page
await page.screenshot({ path: '/tmp/inspect.png', fullPage: true });
const buttons = await page.locator('button').all();
console.log(`Found ${buttons.length} buttons`);
for (let i = 0; i < buttons.length; i++) {
const text = await buttons[i].textContent();
console.log(` Button ${i}: "${text}"`);
}
// STEP 3: Action - interact with discovered elements
const loginButton = page.locator('button:has-text("Login")');
if (await loginButton.isVisible()) {
await loginButton.click();
console.log('✅ Clicked login button');
}
await browser.close();
})();适用于预先不知道DOM结构的动态Web应用:
javascript
// /tmp/playwright-test-reconnaissance.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3000';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// STEP 1: Navigate and wait for dynamic content
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle'); // CRITICAL for dynamic apps
// STEP 2: Reconnaissance - discover what's on the page
await page.screenshot({ path: '/tmp/inspect.png', fullPage: true });
const buttons = await page.locator('button').all();
console.log(`Found ${buttons.length} buttons`);
for (let i = 0; i < buttons.length; i++) {
const text = await buttons[i].textContent();
console.log(` Button ${i}: "${text}"`);
}
// STEP 3: Action - interact with discovered elements
const loginButton = page.locator('button:has-text("Login")');
if (await loginButton.isVisible()) {
await loginButton.click();
console.log('✅ Clicked login button');
}
await browser.close();
})();Server Management
服务器管理
Check for Running Servers
检查运行中的服务器
bash
undefinedbash
undefinedCheck if port is in use
Check if port is in use
lsof -i :3000 # Mac/Linux
netstat -ano | findstr :3000 # Windows
undefinedlsof -i :3000 # Mac/Linux
netstat -ano | findstr :3000 # Windows
undefinedStart Server Before Testing
测试前启动服务器
javascript
// /tmp/playwright-test-with-server.js
const { chromium } = require('playwright');
const { spawn } = require('child_process');
const TARGET_URL = 'http://localhost:3000';
(async () => {
// Start server
console.log('Starting server...');
const server = spawn('npm', ['run', 'dev'], { shell: true });
server.stdout.on('data', (data) => console.log(`Server: ${data}`));
server.stderr.on('data', (data) => console.error(`Server Error: ${data}`));
// Wait for server to be ready
await new Promise(resolve => setTimeout(resolve, 3000));
// Run tests
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle');
// Your test logic here
console.log('Title:', await page.title());
await browser.close();
// Clean up server
server.kill();
console.log('✅ Tests complete, server stopped');
})();javascript
// /tmp/playwright-test-with-server.js
const { chromium } = require('playwright');
const { spawn } = require('child_process');
const TARGET_URL = 'http://localhost:3000';
(async () => {
// Start server
console.log('Starting server...');
const server = spawn('npm', ['run', 'dev'], { shell: true });
server.stdout.on('data', (data) => console.log(`Server: ${data}`));
server.stderr.on('data', (data) => console.error(`Server Error: ${data}`));
// Wait for server to be ready
await new Promise(resolve => setTimeout(resolve, 3000));
// Run tests
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle');
// Your test logic here
console.log('Title:', await page.title());
await browser.close();
// Clean up server
server.kill();
console.log('✅ Tests complete, server stopped');
})();Common Patterns
常用模式
Test a Page (Multiple Viewports)
测试页面(多视口)
javascript
// /tmp/playwright-test-responsive.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 100 });
const page = await browser.newPage();
// Desktop test
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(TARGET_URL);
console.log('Desktop - Title:', await page.title());
await page.screenshot({ path: '/tmp/desktop.png', fullPage: true });
// Mobile test
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: '/tmp/mobile.png', fullPage: true });
await browser.close();
})();javascript
// /tmp/playwright-test-responsive.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 100 });
const page = await browser.newPage();
// Desktop test
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(TARGET_URL);
console.log('Desktop - Title:', await page.title());
await page.screenshot({ path: '/tmp/desktop.png', fullPage: true });
// Mobile test
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: '/tmp/mobile.png', fullPage: true });
await browser.close();
})();Test Login Flow
测试登录流程
javascript
// /tmp/playwright-test-login.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/login`);
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Wait for redirect
await page.waitForURL('**/dashboard');
console.log('✅ Login successful, redirected to dashboard');
await browser.close();
})();javascript
// /tmp/playwright-test-login.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/login`);
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Wait for redirect
await page.waitForURL('**/dashboard');
console.log('✅ Login successful, redirected to dashboard');
await browser.close();
})();Fill and Submit Form
填充并提交表单
javascript
// /tmp/playwright-test-form.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 50 });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/contact`);
await page.fill('input[name="name"]', 'John Doe');
await page.fill('input[name="email"]', 'john@example.com');
await page.fill('textarea[name="message"]', 'Test message');
await page.click('button[type="submit"]');
// Verify submission
await page.waitForSelector('.success-message');
console.log('✅ Form submitted successfully');
await browser.close();
})();javascript
// /tmp/playwright-test-form.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 50 });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/contact`);
await page.fill('input[name="name"]', 'John Doe');
await page.fill('input[name="email"]', 'john@example.com');
await page.fill('textarea[name="message"]', 'Test message');
await page.click('button[type="submit"]');
// Verify submission
await page.waitForSelector('.success-message');
console.log('✅ Form submitted successfully');
await browser.close();
})();Check for Broken Links
检查无效链接
javascript
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3000');
const links = await page.locator('a[href^="http"]').all();
const results = { working: 0, broken: [] };
for (const link of links) {
const href = await link.getAttribute('href');
try {
const response = await page.request.head(href);
if (response.ok()) {
results.working++;
} else {
results.broken.push({ url: href, status: response.status() });
}
} catch (e) {
results.broken.push({ url: href, error: e.message });
}
}
console.log(`✅ Working links: ${results.working}`);
console.log(`❌ Broken links:`, results.broken);
await browser.close();
})();javascript
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3000');
const links = await page.locator('a[href^="http"]').all();
const results = { working: 0, broken: [] };
for (const link of links) {
const href = await link.getAttribute('href');
try {
const response = await page.request.head(href);
if (response.ok()) {
results.working++;
} else {
results.broken.push({ url: href, status: response.status() });
}
} catch (e) {
results.broken.push({ url: href, error: e.message });
}
}
console.log(`✅ Working links: ${results.working}`);
console.log(`❌ Broken links:`, results.broken);
await browser.close();
})();Take Screenshot with Error Handling
带错误处理的截图功能
javascript
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
try {
await page.goto('http://localhost:3000', {
waitUntil: 'networkidle',
timeout: 10000,
});
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true,
});
console.log('📸 Screenshot saved to /tmp/screenshot.png');
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
await browser.close();
}
})();javascript
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
try {
await page.goto('http://localhost:3000', {
waitUntil: 'networkidle',
timeout: 10000,
});
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true,
});
console.log('📸 Screenshot saved to /tmp/screenshot.png');
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
await browser.close();
}
})();Test Responsive Design
测试响应式设计
javascript
// /tmp/playwright-test-responsive-full.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const viewports = [
{ name: 'Desktop', width: 1920, height: 1080 },
{ name: 'Tablet', width: 768, height: 1024 },
{ name: 'Mobile', width: 375, height: 667 },
];
for (const viewport of viewports) {
console.log(
`Testing ${viewport.name} (${viewport.width}x${viewport.height})`,
);
& Discovery
```javascript
// Check visibility
const isVisible = await page.locator('button').isVisible();
// Get text
const text = await page.locator('h1').textContent();
// Get attribute
const href = await page.locator('a').getAttribute('href');
// Find all elements
const allButtons = await page.locator('button').all();
const allLinks = await page.locator('a').all();
// Check if element exists
const count = await page.locator('.modal').count();
console.log(`Found ${count} modals`);javascript
// /tmp/playwright-test-responsive-full.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const viewports = [
{ name: 'Desktop', width: 1920, height: 1080 },
{ name: 'Tablet', width: 768, height: 1024 },
{ name: 'Mobile', width: 375, height: 667 },
];
for (const viewport of viewports) {
console.log(
`Testing ${viewport.name} (${viewport.width}x${viewport.height})`,
);
& Discovery
```javascript
// Check visibility
const isVisible = await page.locator('button').isVisible();
// Get text
const text = await page.locator('h1').textContent();
// Get attribute
const href = await page.locator('a').getAttribute('href');
// Find all elements
const allButtons = await page.locator('button').all();
const allLinks = await page.locator('a').all();
// Check if element exists
const count = await page.locator('.modal').count();
console.log(`Found ${count} modals`);Network & Console
网络与控制台
javascript
// Intercept requests
await page.route('**/api/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ mocked: true })
});
});
// Wait for response
const response = await page.waitForResponse('**/api/data');
console.log(await response.json());
// Capture console logs
page.on('console', msg => {
console.log(`Browser console [${msg.type()}]:`, msg.text());
});javascript
// Intercept requests
await page.route('**/api/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ mocked: true })
});
});
// Wait for response
const response = await page.waitForResponse('**/api/data');
console.log(await response.json());
// Capture console logs
page.on('console', msg => {
console.log(`Browser console [${msg.type()}]:`, msg.text());
});Best Practices
最佳实践
✅ DO
✅ 推荐做法
- Wait for networkidle on dynamic apps - Always use before inspecting DOM on SPAs/React/Vue apps
page.waitForLoadState('networkidle') - Use reconnaissance pattern - Take screenshot, inspect DOM, then act based on what you find
- Visible browser by default - Use for easier debugging
headless: false - Descriptive selectors - Use ,
text=, or data attributes over brittle CSS selectorsrole= - Error handling - Wrap automation in try-catch blocks
- Clean up resources - Always close browser and kill servers
- 动态应用等待networkidle状态 - 在SPA/React/Vue应用中检查DOM前,始终使用
page.waitForLoadState('networkidle') - 使用探测模式 - 先截图、检查DOM,再基于探测结果执行操作
- 默认使用可见浏览器 - 使用 便于调试
headless: false - 使用描述性选择器 - 使用 ,
text=, 或数据属性替代脆弱的CSS选择器role= - 错误处理 - 用try-catch块包裹自动化逻辑
- 资源清理 - 始终关闭浏览器并终止服务器进程
❌ DON'T
❌ 禁止做法
- Don't inspect before networkidle - On dynamic webapps, DOM may not be ready yet
- Don't use fixed timeouts - Use or
waitForSelector()instead of arbitrary waitswaitForLoadState() - Don't write to skill directory - Always use for test scripts
/tmp - Don't hardcode URLs - Use constants at top of script for easy modificationeadless: false, // Visible browser slowMo: 50 // Slow down by 50ms });
const page = await browser.newPage();
// Navigate
await page.goto('https://example.com', {
waitUntil: 'networkidle' // Wait for network to be idle
});
// Close
await browser.close();
undefined- 不要在networkidle前检查DOM - 动态Web应用的DOM可能还未加载完成
- 不要使用固定超时 - 使用 或
waitForSelector()替代随机等待waitForLoadState() - 不要写入工具目录 - 始终使用 存放测试脚本
/tmp - 不要硬编码URL - 在脚本顶部使用常量定义URL方便修改
javascript
headless: false, // Visible browser
slowMo: 50 // Slow down by 50ms
});
const page = await browser.newPage();
// Navigate
await page.goto('https://example.com', {
waitUntil: 'networkidle' // Wait for network to be idle
});
// Close
await browser.close();Selectors & Interactions
选择器与交互操作
javascript
// Click
await page.click('button.submit');
await page.dblclick('.item');
// Fill input
await page.fill('input[name="email"]', 'test@example.com');
await page.getByLabel('Email').fill('user@example.com');
// Checkbox
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');
// Select dropdown
await page.selectOption('select#country', 'usa');
// Type with delay
await page.type('#username', 'testuser', { delay: 100 });javascript
// Click
await page.click('button.submit');
await page.dblclick('.item');
// Fill input
await page.fill('input[name="email"]', 'test@example.com');
await page.getByLabel('Email').fill('user@example.com');
// Checkbox
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');
// Select dropdown
await page.selectOption('select#country', 'usa');
// Type with delay
await page.type('#username', 'testuser', { delay: 100 });Waiting Strategies
等待策略
javascript
// Wait for navigation
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');
// Wait for element
await page.waitForSelector('.success-message');
await page.waitForSelector('.spinner', { state: 'hidden' });
// Wait for timeout (use sparingly)
await page.waitForTimeout(1000);javascript
// Wait for navigation
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');
// Wait for element
await page.waitForSelector('.success-message');
await page.waitForSelector('.spinner', { state: 'hidden' });
// Wait for timeout (use sparingly)
await page.waitForTimeout(1000);Screenshots
截图功能
javascript
// Full page screenshot
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true
});
// Element screenshot
await page.locator('.chart').screenshot({
path: '/tmp/chart.png'
});
```Quick Tips
- **Visible browser** - Always `headless: false` unless explicitly requested
- **Write to /tmp** - Scripts go to `/tmp/playwright-test-*.js`, never to project directories
- **Parameterize URLs** - Use `TARGET_URL` constant at top of script
- **Slow down** - Use `slowMo: 100` to see actions in real-time
- **Wait smart** - Use `waitForLoadState('networkidle')` for dynamic apps before inspecting
- **Error handling** - Wrap in try-catch with proper cleanup in finally block
- **Progress feedback** - Use `console.log()` to
const text = await page.locator('h1').textContent();
// Get attribute
const href = await page.locator('a').getAttribute('href');javascript
// Full page screenshot
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true
});
// Element screenshot
await page.locator('.chart').screenshot({
path: '/tmp/chart.png'
});Network
快速提示
javascript
// Intercept requests
await page.route('**/api/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ mocked: true })
});
});
// Wait for response
const response = await page.waitForResponse('**/api/data');
console.log(await response.json());- 可见浏览器 - 始终使用 除非用户明确要求无头模式
headless: false - 写入/tmp目录 - 脚本放在 ,绝对不要写入项目目录
/tmp/playwright-test-*.js - URL参数化 - 在脚本顶部使用 常量
TARGET_URL - 减慢执行速度 - 使用 可实时查看执行动作
slowMo: 100 - 智能等待 - 动态应用检查前调用
waitForLoadState('networkidle') - 错误处理 - 使用try-catch包裹逻辑,并在finally块中做好资源清理
- 进度反馈 - 使用 输出进度
console.log()
javascript
const text = await page.locator('h1').textContent();
// Get attribute
const href = await page.locator('a').getAttribute('href');Tips
网络操作
- DEFAULT: Visible browser - Always use unless user explicitly asks for headless mode
headless: false - Use /tmp for test files - Write to , never to skill directory or user's project
/tmp/playwright-test-*.js - Parameterize URLs - Put detected/provided URL in a constant at the top of every script
TARGET_URL - Slow down: Use to make actions visible and easier to follow
slowMo: 100 - Wait strategies: Use ,
waitForURL,waitForSelectorinstead of fixed timeoutswaitForLoadState - Error handling: Always use try-catch for robust automation
- Console output: Use to track progress and show what's happening
console.log()
javascript
// Intercept requests
await page.route('**/api/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ mocked: true })
});
});
// Wait for response
const response = await page.waitForResponse('**/api/data');
console.log(await response.json());Common Use Cases
小贴士
Visual Testing:
- Take screenshots at different viewports
- Compare visual changes
- Test responsive design
Functional Testing:
- Test login flows
- Form validation
- Navigation flows
- Error handling
Validation:
- Check for broken links
- Verify images load
- Test page load times
- Check accessibility
Automation:
- Fill forms automatically
- Click through user flows
- Extract data from pages
- Generate reports
- 默认配置:可见浏览器 - 始终使用 除非用户明确要求无头模式
headless: false - 测试文件放在/tmp目录 - 写入 ,绝对不要写入工具目录或用户项目目录
/tmp/playwright-test-*.js - URL参数化 - 将检测到的/用户提供的URL定义为每个脚本顶部的 常量
TARGET_URL - 减慢速度: 使用 让操作可见,更易跟踪
slowMo: 100 - 等待策略: 使用 ,
waitForURL,waitForSelector替代固定超时waitForLoadState - 错误处理: 始终使用try-catch保证自动化逻辑健壮
- 控制台输出: 使用 跟踪进度,展示当前执行状态
console.log()
Notes
常见使用场景
- Each automation is custom-written for your specific request
- Not limited to pre-built scripts - any browser task possible
- Auto-detects running dev servers to eliminate hardcoded URLs
- Test scripts written to for automatic cleanup (no clutter)
/tmp - Progressive disclosure - load advanced documentation only when needed
可视化测试:
- 在不同视口下截图
- 对比视觉变化
- 测试响应式设计
功能测试:
- 测试登录流程
- 表单验证
- 导航流程
- 错误处理
校验:
- 检查无效链接
- 验证图片加载
- 测试页面加载速度
- 检查可访问性
自动化:
- 自动填充表单
- 自动点击完成用户流程
- 从页面提取数据
- 生成报告
References
注意事项
- 每个自动化脚本都是针对你的具体需求自定义编写的
- 不局限于预构建脚本,可实现任何浏览器任务
- 自动检测运行中的开发服务器,无需硬编码URL
- 测试脚本写入 目录可自动清理,不会产生垃圾文件
/tmp - 渐进式展示,仅在需要时加载高级文档
—