mcpField
P
playwright

playwright

github.com/secondsky测试与质量2401.0k 次安装5 周前更新
代码仓库

关于 playwright

这项技能介绍如何使用 Playwright 进行浏览器自动化与端到端测试,面向需要编写和运行网页测试的开发者。内容涵盖本地服务检测、可视化浏览器脚本执行及常见测试模式。

主要功能

  1. 01检测运行中的开发服务器
  2. 02通过封装脚本运行测试
  3. 03浏览器交互与断言
  4. 04网络模拟与截图
  5. 05身份验证和页面对象模式

适用场景

  1. 01跨视口测试本地网页应用
  2. 02自动执行登录和页面交互
  3. 03在测试中模拟 API 响应
  4. 04截取截图进行视觉检查

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/secondsky/claude-skills --skill playwright

安装后在 Claude Code 中输入/playwright确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

package.json
run.js
examples/
form-test.js
link-checker.js
responsive-test.js
lib/
helpers.js
references/
API_REFERENCE.md

详细介绍

Playwright - 浏览器自动化与端到端测试

掌握使用 Playwright 进行浏览器自动化和端到端测试的专业知识。Playwright 是一个现代化的跨浏览器测试框架。

重要 - 路径解析: 此技能可以安装在不同位置。执行命令前,请根据加载此 SKILL.md 文件的位置确定技能目录,并在所有命令中使用该路径。将 $SKILL_DIR 替换为实际发现的路径。

常见安装路径:

  • 插件系统:~/.claude/plugins/*/playwright/skills/playwright
  • 手动全局安装:~/.claude/skills/playwright
  • 特定项目:<project>/.claude/skills/playwright

关键工作流程 - 请按以下步骤操作

自动化浏览器任务时:

  1. 自动检测开发服务器 - 在本地主机上测试时,务必先运行服务器检测:

    cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"
    
    • 如果找到 1 个服务器:自动使用并告知用户
    • 如果找到多个服务器:询问用户要测试哪一个
    • 如果未找到服务器:询问 URL,或提出协助启动开发服务器
  2. 将脚本写入 /tmp - 切勿将测试文件写入技能目录;始终使用 /tmp/playwright-test-*.js

  3. 默认使用可见浏览器 - 除非用户明确要求无头模式,否则始终使用 headless: false

  4. 将 URL 参数化 - 始终通过脚本顶部的常量配置 URL

  5. 通过 run.js 执行 - 始终运行: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

此操作会安装 Playwright 和 Chromium 浏览器。只需执行一次。

安装(用于 E2E 测试项目)
# 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',
  },
})

浏览器自动化模式

工作原理
  1. 描述要测试或自动化的内容
  2. 我会自动检测正在运行的开发服务器(或询问 URL)
  3. 我会在 /tmp/playwright-test-*.js 中编写自定义 Playwright 代码
  4. 我会通过以下命令执行:cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js
  5. 实时显示结果,并显示浏览器窗口
测试页面(多个视口)
// /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();
})();

执行: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"]', '[email protected]');
  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();
})();

E2E 测试模式

运行测试
# 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('[email protected]')
    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('[email protected]')
    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('[email protected]')
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('[email protected]')
  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('[email protected]', 'password123')

可用辅助函数

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);

自定义 HTTP 请求头

通过环境变量为所有 HTTP 请求配置自定义请求头:

# 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

使用 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 存放测试文件 - 将文件写入 /tmp/playwright-test-*.js,切勿写入技能目录
  • 将 URL 参数化 - 将检测到或提供的 URL 放入 TARGET_URL 常量
  • 默认:使用可见浏览器 - 除非明确要求,否则始终使用 headless: false
  • 优先使用基于角色的选择器 - 比 CSS 选择器更稳定
  • 信任自动等待 - 无需手动休眠
  • 每项测试使用全新的上下文 - 自动隔离
  • 并行运行测试 - 默认行为
  • 模拟外部依赖 - 使用 page.route()
  • 使用 trace viewer - 进行时光回溯式调试

提示

  • 放慢速度: 使用 slowMo: 100 让操作过程清晰可见
  • 等待策略: 使用 waitForURL、waitForSelector、waitForLoadState,不要使用固定超时
  • 错误处理: 始终使用 try-catch,确保自动化稳健运行
  • 控制台输出: 使用 console.log() 跟踪进度

故障排查

未安装 Playwright:

cd $SKILL_DIR && bun run setup

找不到模块: 确保通过 run.js 封装程序从技能目录运行

浏览器未打开: 检查 headless: false,并确保显示环境可用

找不到元素: 添加等待:await page.waitForSelector('.element', { timeout: 10000 })

安全安装

安装 Playwright 和浏览器二进制文件时,请遵循供应链安全最佳实践:

  • 阻止安装后脚本 — npm config set ignore-scripts true(然后手动运行 npx playwright install 下载浏览器)
  • 冷却期 — 等待 7 天,让社区审查新版本的软件包
  • 安装前审计 — 运行 socket package score npm <pkg>,或使用 socket npm install <pkg> 检查软件包

加载 dependency-upgrade 技能,获取完整的安全配置,包括 Socket CLI 集成、冷却期设置、锁文件验证和 CI 强制执行。

另请参阅

  • vitest-testing - 单元和集成测试
  • api-testing - HTTP API 测试
  • test-quality-analysis - 测试质量模式

何时加载参考资料

需要以下内容时,加载 references/API_REFERENCE.md:

  • 高级选择器模式和定位器策略
  • 网络拦截以及请求/响应模拟
  • 身份验证模式和会话管理
  • 视觉回归测试设置
  • 移动设备模拟配置
  • 性能测试和指标
  • 调试技术(追踪查看器、检查器)
  • CI/CD 流水线集成
  • 使用 axe-core 进行无障碍测试
  • 数据驱动和参数化测试
  • 页面对象模型的高级模式
  • 并行执行策略

评论

支持 Markdown