关于 playwright-skill
用于 Playwright 网页测试与自动化的技能,可编写自定义 JavaScript 脚本。它指导开发者管理测试服务器、检查页面并执行交互与验证。
主要功能
- 01自定义 JavaScript 浏览器自动化
- 02检查或启动测试服务器
- 03交互前侦察页面 DOM
- 04测试表单与导航流程
- 05截图和网络检查
适用场景
- 01测试登录与表单流程
- 02检查不同视口下的响应式布局
- 03查找失效链接和未加载图片
- 04自动提取网页数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dedalus-erp-pas/ foundation-skills --skill playwright-skill安装后在 Claude Code 中输入/playwright-skill确认已加载。
详细介绍
Playwright Web 测试与自动化
使用 Playwright 进行全面的 Web 测试。编写自定义 JavaScript 代码,执行任意测试或自动化任务。
前提条件
- Node.js:必需 — 从 https://nodejs.org 安装
- playwright npm 包:
npm install playwright(会自动安装无头 Chromium)
决策树:选择方法
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 testing
关键工作流
- 检查测试服务器是否正在运行 - 检测正在运行的开发服务器,或根据需要启动服务器
- 将脚本写入 /tmp - 绝不要将测试文件写入技能目录;始终使用
/tmp/playwright-test-*.js - 默认使用可视化浏览器 - 除非用户明确要求无头模式,否则始终使用
headless: false - 等待动态内容 - 检查动态 Web 应用前,使用
waitForLoadState('networkidle') - 将 URL 参数化 - 始终通过脚本顶部的常量配置 URL
先侦察后操作模式
对于无法预先了解 DOM 结构的动态 Web 应用:
// /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();
})();
服务器管理
检查正在运行的服务器
# Check if port is in use
lsof -i :3000 # Mac/Linux
netstat -ano | findstr :3000 # Windows
测试前启动服务器
// /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');
})();
常见模式
测试页面(多个视口)
// /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();
})();
测试登录流程
// /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"]', '[email protected]');
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();
})();
填写并提交表单
// /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"]', '[email protected]');
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();
})();
检查失效链接
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();
})();
截图并处理错误
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();
}
})();
测试响应式设计
// /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`);
网络与控制台
// 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());
});
最佳实践
✅ 应该做
- 在动态应用中等待 networkidle - 检查 SPA/React/Vue 应用的 DOM 前,始终使用
page.waitForLoadState('networkidle') - 使用侦察模式 - 截图、检查 DOM,然后根据发现采取操作
- 默认使用可视化浏览器 - 使用
headless: false,便于调试 - 使用语义明确的选择器 - 优先使用
text=、role=或数据属性,而不是脆弱的 CSS 选择器 - 处理错误 - 使用 try-catch 块封装自动化操作
- 清理资源 - 始终关闭浏览器并终止服务器
❌ 不应该做
- 不要在 networkidle 前检查 - 对于动态 Web 应用,DOM 可能尚未就绪
- 不要使用固定超时 - 使用
waitForSelector()或waitForLoadState(),而不是任意等待 - 不要写入技能目录 - 测试脚本始终使用
/tmp - 不要对 URL 进行硬编码 - 在脚本顶部使用常量,便于修改eadless: false, // 可视化浏览器 slowMo: 50 // 延迟 50ms });
const page = await browser.newPage();
// 导航 await page.goto('https://example.com', { waitUntil: 'networkidle' // 等待网络空闲 });
// 关闭 await browser.close();
### Selectors & Interactions
```javascript
// Click
await page.click('button.submit');
await page.dblclick('.item');
// Fill input
await page.fill('input[name="email"]', '[email protected]');
await page.getByLabel('Email').fill('[email protected]');
// 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 });
等待策略
// 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);
截图
// 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');
网络
// 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 参数化 - 将检测到或提供的 URL 放在每个脚本顶部的
TARGET_URL常量中 - 降低速度: 使用
slowMo: 100,让操作更容易观察和跟进 - 等待策略: 使用
waitForURL、waitForSelector、waitForLoadState,而不是固定超时 - 错误处理: 始终使用 try-catch,确保自动化稳健可靠
- 控制台输出: 使用
console.log()跟踪进度并显示当前操作
常见用例
视觉测试:
- 在不同视口下截图
- 比较视觉变化
- 测试响应式设计
功能测试:
- 测试登录流程
- 表单验证
- 导航流程
- 错误处理
验证:
- 检查失效链接
- 验证图片是否加载
- 测试页面加载时间
- 检查无障碍性
自动化:
- 自动填写表单
- 点击完成用户流程
- 从页面提取数据
- 生成报告
备注
- 每项自动化均根据你的具体需求定制编写
- 不限于预构建脚本——任何浏览器任务都可以完成
- 自动检测正在运行的开发服务器,避免硬编码 URL
- 测试脚本写入
/tmp,以便自动清理(不留杂物) - 渐进式呈现——仅在需要时加载高级文档
参考资料
评论
支持 Markdown