playwright by secondsky/claude-skills
npx skills add https://github.com/secondsky/claude-skills --skill playwright使用 Playwright 进行浏览器自动化和端到端测试的专家知识 - 一个现代化的跨浏览器测试框架。
重要提示 - 路径解析: 此技能可以安装在不同的位置。在执行命令之前,请根据加载此 SKILL.md 文件的位置确定技能目录,并在所有命令中使用该路径。将 $SKILL_DIR 替换为实际发现的路径。
常见安装路径:
~/.claude/plugins/*/playwright/skills/playwright~/.claude/skills/playwright<project>/.claude/skills/playwright当自动化浏览器任务时:
自动检测开发服务器 - 对于 localhost 测试,务必首先运行服务器检测:
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
将脚本写入 /tmp - 切勿将测试文件写入技能目录;始终使用 /tmp/playwright-test-*.js
默认使用可见浏览器 - 除非用户特别要求无头模式,否则始终使用 headless: false
参数化 URL - 始终通过脚本顶部的常量使 URL 可配置
通过 run.js 执行 - 始终运行:cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js
# 导航到技能目录
cd $SKILL_DIR
# 使用 bun 安装(推荐)
bun run setup
# 或使用 npm
npm run setup:npm
这将安装 Playwright 和 Chromium 浏览器。只需执行一次。
# 使用 Bun(推荐)
bun add -d @playwright/test
bunx playwright install
# 使用 npm
npm init playwright@latest
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: true,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'bun run dev',
url: 'http://localhost:3000',
},
})
/tmp/playwright-test-*.js 中编写自定义 Playwright 代码cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js// /tmp/playwright-test-responsive.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // 自动检测
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 100 });
const page = await browser.newPage();
// 桌面测试
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 });
// 移动端测试
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: '/tmp/mobile.png', fullPage: true });
await browser.close();
})();
执行:cd $SKILL_DIR && node run.js /tmp/playwright-test-responsive.js
// /tmp/playwright-test-login.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // 自动检测
(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"]');
await page.waitForURL('**/dashboard');
console.log('✅ Login successful, redirected to dashboard');
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();
})();
# 运行所有测试
bunx playwright test
# 有头模式(可见浏览器)
bunx playwright test --headed
# 特定文件
bunx playwright test tests/login.spec.ts
# 调试模式
bunx playwright test --debug
# UI 模式(交互式)
bunx playwright test --ui
# 特定浏览器
bunx playwright test --project=chromium
# 生成报告
bunx playwright show-report
import { test, expect } from '@playwright/test'
test.describe('Login flow', () => {
test('successful login', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Login' }).click()
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('wrong@example.com')
await page.getByLabel('Password').fill('wrongpassword')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByText('Invalid credentials')).toBeVisible()
})
})
// ✅ 基于角色的(推荐)
await page.getByRole('button', { name: 'Submit' })
await page.getByRole('link', { name: 'Home' })
// ✅ 文本/标签
await page.getByText('Hello World')
await page.getByLabel('Email')
// ✅ 测试 ID(备用方案)
await page.getByTestId('submit-button')
// ❌ 避免 CSS 选择器(脆弱)
await page.locator('.btn-primary')
// 可见性
await expect(page.getByText('Success')).toBeVisible()
await expect(page.getByRole('button')).toBeEnabled()
// 文本
await expect(page.getByRole('heading')).toHaveText('Welcome')
await expect(page.getByRole('alert')).toContainText('error')
// 属性
await expect(page.getByRole('link')).toHaveAttribute('href', '/home')
// URL/标题
await expect(page).toHaveURL('/dashboard')
await expect(page).toHaveTitle('Dashboard')
// 数量
await expect(page.getByRole('listitem')).toHaveCount(5)
// 点击
await page.getByRole('button').click()
await page.getByText('File').dblclick()
// 输入
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Search').press('Enter')
// 选择
await page.getByLabel('Country').selectOption('us')
// 文件上传
await page.getByLabel('Upload').setInputFiles('path/to/file.pdf')
test('mocks API response', async ({ page }) => {
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test User' }]),
})
})
await page.goto('/users')
await expect(page.getByText('Test User')).toBeVisible()
})
test('captures screenshot', async ({ page }) => {
await page.goto('/')
await page.screenshot({ path: 'screenshot.png', fullPage: true })
await expect(page).toHaveScreenshot('homepage.png')
})
// 登录后保存状态
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign in' }).click()
await page.context().storageState({ path: 'auth.json' })
})
// 在配置中重用
use: { storageState: 'auth.json' }
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test'
export class LoginPage {
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
constructor(page: Page) {
this.emailInput = page.getByLabel('Email')
this.passwordInput = page.getByLabel('Password')
this.submitButton = page.getByRole('button', { name: 'Sign in' })
}
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
}
// 使用
const loginPage = new LoginPage(page)
await loginPage.login('user@example.com', 'password123')
lib/helpers.js 中的可选实用函数:
const helpers = require('./lib/helpers');
// 检测正在运行的开发服务器(关键 - 首先使用这个!)
const servers = await helpers.detectDevServers();
console.log('Found servers:', servers);
// 带重试的安全点击
await helpers.safeClick(page, 'button.submit', { retries: 3 });
// 带清除的安全输入
await helpers.safeType(page, '#username', 'testuser');
// 获取带时间戳的截图
await helpers.takeScreenshot(page, 'test-result');
// 处理 Cookie 横幅
await helpers.handleCookieBanner(page);
// 提取表格数据
const data = await helpers.extractTableData(page, 'table.results');
// 创建带有自定义标头的上下文
const context = await helpers.createContext(browser);
通过环境变量为所有 HTTP 请求配置自定义标头:
# 单个标头(常见情况)
PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \
cd $SKILL_DIR && node run.js /tmp/my-script.js
# 多个标头(JSON 格式)
PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Debug":"true"}' \
cd $SKILL_DIR && node run.js /tmp/my-script.js
使用 helpers.createContext() 时,标头会自动应用。
用于快速的一次性任务:
cd $SKILL_DIR && node run.js "
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3001');
await page.screenshot({ path: '/tmp/quick-screenshot.png', fullPage: true });
console.log('Screenshot saved');
await browser.close();
"
何时使用:
detectDevServers()/tmp/playwright-test-*.js,切勿写入技能目录TARGET_URL 常量headless: falsepage.route()slowMo: 100 使操作可见waitForURL、waitForSelector、waitForLoadState 而不是固定的超时console.log() 跟踪进度Playwright 未安装:
cd $SKILL_DIR && bun run setup
模块未找到: 确保通过 run.js 包装器从技能目录运行
浏览器未打开: 检查 headless: false 并确保显示可用
元素未找到: 添加等待:await page.waitForSelector('.element', { timeout: 10000 })
vitest-testing - 单元和集成测试api-testing - HTTP API 测试test-quality-analysis - 测试质量模式当您需要时加载 references/API_REFERENCE.md:
每周安装次数
281
仓库
GitHub 星标数
91
首次出现
Jan 23, 2026
安全审计
安装于
gemini-cli261
opencode260
cursor255
codex254
github-copilot252
kimi-cli243
Expert knowledge for browser automation and end-to-end testing with Playwright - a modern cross-browser testing framework.
IMPORTANT - Path Resolution: This skill can be installed in different locations. Before executing commands, determine the skill directory based on where you loaded this SKILL.md file, and use that path in all commands. Replace $SKILL_DIR with the actual discovered path.
Common installation paths:
~/.claude/plugins/*/playwright/skills/playwright~/.claude/skills/playwright<project>/.claude/skills/playwrightWhen automating browser tasks:
Auto-detect dev servers - For localhost testing, ALWAYS run server detection FIRST:
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"
Write scripts to /tmp - NEVER write test files to skill directory; always use /tmp/playwright-test-*.js
Use visible browser by default - Always use headless: false unless user specifically requests headless mode
Parameterize URLs - Always make URLs configurable via constant at top of script
Execute via run.js - Always run: cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js
# Navigate to skill directory
cd $SKILL_DIR
# Install using bun (preferred)
bun run setup
# Or using npm
npm run setup:npm
This installs Playwright and Chromium browser. Only needed once.
# Using Bun (preferred)
bun add -d @playwright/test
bunx playwright install
# Using npm
npm init playwright@latest
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: true,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'bun run dev',
url: 'http://localhost:3000',
},
})
/tmp/playwright-test-*.jscd $SKILL_DIR && node run.js /tmp/playwright-test-*.js// /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();
})();
Execute: cd $SKILL_DIR && node run.js /tmp/playwright-test-responsive.js
// /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"]');
await page.waitForURL('**/dashboard');
console.log('✅ Login successful, redirected to dashboard');
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();
})();
# Run all tests
bunx playwright test
# Headed mode (see browser)
bunx playwright test --headed
# Specific file
bunx playwright test tests/login.spec.ts
# Debug mode
bunx playwright test --debug
# UI mode (interactive)
bunx playwright test --ui
# Specific browser
bunx playwright test --project=chromium
# Generate report
bunx playwright show-report
import { test, expect } from '@playwright/test'
test.describe('Login flow', () => {
test('successful login', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Login' }).click()
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('wrong@example.com')
await page.getByLabel('Password').fill('wrongpassword')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByText('Invalid credentials')).toBeVisible()
})
})
// ✅ Role-based (recommended)
await page.getByRole('button', { name: 'Submit' })
await page.getByRole('link', { name: 'Home' })
// ✅ Text/Label
await page.getByText('Hello World')
await page.getByLabel('Email')
// ✅ Test ID (fallback)
await page.getByTestId('submit-button')
// ❌ Avoid CSS selectors (brittle)
await page.locator('.btn-primary')
// Visibility
await expect(page.getByText('Success')).toBeVisible()
await expect(page.getByRole('button')).toBeEnabled()
// Text
await expect(page.getByRole('heading')).toHaveText('Welcome')
await expect(page.getByRole('alert')).toContainText('error')
// Attributes
await expect(page.getByRole('link')).toHaveAttribute('href', '/home')
// URL/Title
await expect(page).toHaveURL('/dashboard')
await expect(page).toHaveTitle('Dashboard')
// Count
await expect(page.getByRole('listitem')).toHaveCount(5)
// Clicking
await page.getByRole('button').click()
await page.getByText('File').dblclick()
// Typing
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Search').press('Enter')
// Selecting
await page.getByLabel('Country').selectOption('us')
// File Upload
await page.getByLabel('Upload').setInputFiles('path/to/file.pdf')
test('mocks API response', async ({ page }) => {
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test User' }]),
})
})
await page.goto('/users')
await expect(page.getByText('Test User')).toBeVisible()
})
test('captures screenshot', async ({ page }) => {
await page.goto('/')
await page.screenshot({ path: 'screenshot.png', fullPage: true })
await expect(page).toHaveScreenshot('homepage.png')
})
// Save state after login
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign in' }).click()
await page.context().storageState({ path: 'auth.json' })
})
// Reuse in config
use: { storageState: 'auth.json' }
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test'
export class LoginPage {
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
constructor(page: Page) {
this.emailInput = page.getByLabel('Email')
this.passwordInput = page.getByLabel('Password')
this.submitButton = page.getByRole('button', { name: 'Sign in' })
}
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
}
// Usage
const loginPage = new LoginPage(page)
await loginPage.login('user@example.com', 'password123')
Optional utility functions in lib/helpers.js:
const helpers = require('./lib/helpers');
// Detect running dev servers (CRITICAL - use this first!)
const servers = await helpers.detectDevServers();
console.log('Found servers:', servers);
// Safe click with retry
await helpers.safeClick(page, 'button.submit', { retries: 3 });
// Safe type with clear
await helpers.safeType(page, '#username', 'testuser');
// Take timestamped screenshot
await helpers.takeScreenshot(page, 'test-result');
// Handle cookie banners
await helpers.handleCookieBanner(page);
// Extract table data
const data = await helpers.extractTableData(page, 'table.results');
// Create context with custom headers
const context = await helpers.createContext(browser);
Configure custom headers for all HTTP requests via environment variables:
# Single header (common case)
PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \
cd $SKILL_DIR && node run.js /tmp/my-script.js
# Multiple headers (JSON format)
PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Debug":"true"}' \
cd $SKILL_DIR && node run.js /tmp/my-script.js
Headers are automatically applied when using helpers.createContext().
For quick one-off tasks:
cd $SKILL_DIR && node run.js "
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3001');
await page.screenshot({ path: '/tmp/quick-screenshot.png', fullPage: true });
console.log('Screenshot saved');
await browser.close();
"
When to use:
detectDevServers() before writing test code/tmp/playwright-test-*.js, never to skill directoryTARGET_URL constantheadless: false unless explicitly requestedpage.route()slowMo: 100 to make actions visiblewaitForURL, waitForSelector, waitForLoadState instead of fixed timeoutsconsole.log() to track progressPlaywright not installed:
cd $SKILL_DIR && bun run setup
Module not found: Ensure running from skill directory via run.js wrapper
Browser doesn't open: Check headless: false and ensure display available
Element not found: Add wait: await page.waitForSelector('.element', { timeout: 10000 })
vitest-testing - Unit and integration testingapi-testing - HTTP API testingtest-quality-analysis - Test quality patternsLoad references/API_REFERENCE.md when you need:
Weekly Installs
281
Repository
GitHub Stars
91
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykWarn
Installed on
gemini-cli261
opencode260
cursor255
codex254
github-copilot252
kimi-cli243
Skills CLI 使用指南:AI Agent 技能包管理器安装与管理教程
27,400 周安装