1 项目简介

P

Playwright Skill

v4.1.0 · MIT License

1.5k Stars 85 Forks

Playwright Skill 是一个为 Claude Code 设计的浏览器自动化插件。它使 Claude 能够根据用户请求实时编写和执行 Playwright 自动化代码,从简单的页面测试到复杂的多步骤工作流程。

核心理念

与预构建脚本不同,Claude 会为每个任务生成定制的 Playwright 代码,实现真正的自动化灵活性。

Node.js
运行环境
Playwright
自动化引擎

2 核心功能

自定义代码生成

Claude 为每个自动化请求编写定制的 Playwright 代码,而非使用预构建脚本

可视化浏览器

默认 headless: false,可实时观察自动化执行过程

模块解析

通用执行器消除模块访问错误,确保可靠执行

智能资源管理

自动清理临时文件,避免竞态条件

辅助函数库

提供常用自动化模式的可选工具函数

3 安装配置

环境要求

Node.js 14.0.0 或更高版本

使用 Claude Code 的插件市场系统,支持自动更新和团队分发:

# 从市场添加插件
/plugin marketplace add lackeyjb/playwright-skill

# 安装插件
/plugin install playwright-skill@playwright-skill

# 运行安装脚本
cd ~/.claude/plugins/marketplaces/playwright-skill/skills/playwright-skill
npm run setup

手动克隆并安装到全局 skills 目录:

# 克隆仓库
git clone https://github.com/lackeyjb/playwright-skill.git /tmp/playwright-skill-temp

# 复制到 skills 目录
mkdir -p ~/.claude/skills
cp -r /tmp/playwright-skill-temp/skills/playwright-skill ~/.claude/skills/

# 运行安装脚本
cd ~/.claude/skills/playwright-skill && npm run setup

将 skill 复制到项目的 .claude/skills/ 目录:

# 复制 skill 到项目目录
cp -r skills/playwright-skill <your-project>/.claude/skills/

# 运行安装脚本
cd <your-project>/.claude/skills/playwright-skill
npm run setup

注意

npm run setup 会安装 Playwright 及 Chromium 浏览器,只需运行一次。

4 工作流程

👤
描述需求
🤖
生成代码
▶️
执行脚本
📊
返回结果

关键步骤

1 服务器检测(必须首先执行)
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"
2 创建脚本

脚本写入 /tmp/playwright-test-*.js(不要写入 skill 目录),使用 TARGET_URL 常量参数化 URL。

3 执行脚本
cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js

5 辅助函数

lib/helpers.js 提供常用自动化模式的工具函数:

函数名 功能描述
detectDevServers() 扫描本地端口,识别运行中的开发服务器
safeClick() 带重试逻辑的点击操作(默认3次尝试)
safeType() 带重试逻辑的文本输入
takeScreenshot() 带时间戳的截图功能
handleCookieBanner() 自动处理 Cookie 同意横幅
extractTableData() 解析 HTML 表格为结构化对象
createContext() 创建带自定义 Headers 的浏览器上下文
authenticate() 自动化登录流程
scrollPage() 页面滚动(上/下/顶部/底部)
retryWithBackoff() 指数退避重试策略

6 API 参考

// 启动浏览器
const browser = await chromium.launch({ headless: false, slowMo: 100 });

// 创建隔离上下文
const context = await browser.newContext({
  viewport: { width: 1920, height: 1080 },
  userAgent: 'custom-agent'
});

// 创建新页面
const page = await context.newPage();

// 关闭浏览器
await browser.close();
// 推荐的选择方式
page.locator('[data-testid="submit"]')    // data 属性
page.getByRole('button', { name: '提交' }) // 角色
page.getByText('欢迎')                      // 文本内容
page.getByLabel('用户名')                   // 表单标签
page.getByPlaceholder('请输入...')          // 占位符

// 定位器链式调用
locator.filter({ hasText: '活跃' })         // 文本过滤
locator.nth(0)                              // 索引选择
locator.and(anotherLocator)                 // 组合条件
// 文本输入
await locator.fill('文本内容');           // 替换整个字段
await locator.type('文本', { delay: 50 }); // 模拟逐字输入
await locator.clear();                     // 清空字段

// 表单控件
await locator.check();                     // 勾选复选框
await page.selectOption('#dropdown', 'value'); // 下拉选择
await page.setInputFiles('#upload', 'file.pdf'); // 文件上传

// 鼠标操作
await page.click('#btn');                  // 点击
await page.dblclick('#btn');               // 双击
await page.hover('#menu');                 // 悬停
await page.dragAndDrop('#src', '#dest');   // 拖放

// 键盘操作
await page.keyboard.press('Enter');        // 按键
await page.keyboard.press('Control+A');    // 组合键
// 元素状态等待
await locator.waitFor({ state: 'visible' });
await locator.waitFor({ state: 'hidden' });
await locator.waitFor({ state: 'attached' });

// 页面加载等待
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');

// 自定义条件等待
await page.waitForFunction(() => document.title === '首页');

// 网络请求等待
await page.waitForResponse(resp => resp.url().includes('/api/'));
await page.waitForRequest(req => req.method() === 'POST');
// 页面级断言
await expect(page).toHaveTitle('首页');
await expect(page).toHaveURL(/dashboard/);

// 元素状态断言
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();

// 内容断言
await expect(locator).toHaveText('确切文本');
await expect(locator).toContainText('部分文本');
await expect(locator).toHaveValue('输入值');
await expect(locator).toHaveAttribute('href', '/link');
await expect(locator).toHaveCSS('color', 'red');
await expect(locator).toHaveCount(5);
// 拦截并修改请求
await page.route('**/api/**', route => {
  route.continue({ headers: { ...route.request().headers(), 'X-Custom': 'value' } });
});

// 模拟响应
await page.route('**/api/data', route => {
  route.fulfill({ status: 200, body: JSON.stringify({ mock: true }) });
});

// 阻止资源加载
await page.route('**/*.{png,jpg}', route => route.abort());

7 使用示例

测试不同视口尺寸的页面展示:

const viewports = [
  { name: 'desktop', width: 1920, height: 1080 },
  { name: 'tablet', width: 768, height: 1024 },
  { name: 'mobile', width: 375, height: 667 }
];

for (const vp of viewports) {
  await page.setViewportSize({ width: vp.width, height: vp.height });
  await page.screenshot({ path: `/tmp/${vp.name}.png`, fullPage: true });
}
await page.goto(TARGET_URL + '/login');

// 填写凭据
await page.fill('[name="username"]', 'testuser');
await page.fill('[name="password"]', 'password123');

// 提交表单
await page.click('button[type="submit"]');

// 验证登录成功
await page.waitForURL('**/dashboard');
await expect(page.getByText('欢迎')).toBeVisible();
await page.goto(TARGET_URL + '/register');

// 填写表单
await page.fill('#name', '张三');
await page.fill('#email', 'zhang@example.com');
await page.selectOption('#country', 'CN');
await page.check('#terms');

// 提交
await page.click('#submit');

// 验证成功
await page.waitForSelector('.success-message');
const msg = await page.textContent('.success-message');
console.log('注册结果:', msg);
await page.goto(TARGET_URL);

const links = await page.locator('a[href]').all();
const brokenLinks = [];

for (const link of links) {
  const href = await link.getAttribute('href');
  if (href.startsWith('http')) {
    const response = await page.request.head(href);
    if (!response.ok()) {
      brokenLinks.push({ url: href, status: response.status() });
    }
  }
}

console.log('失效链接:', brokenLinks);

8 配置选项

默认配置

Headless 模式
false(可见浏览器)
慢动作延迟
100ms
超时时间
30 秒
截图目录
/tmp/

自定义 Headers

通过环境变量设置请求头:

# 单个 Header
PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill

# 多个 Headers(JSON 格式)
PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","Authorization":"Bearer token"}'

安装路径

插件 ~/.claude/plugins/marketplaces/playwright-skill/skills/playwright-skill
全局 ~/.claude/skills/playwright-skill
项目 <project>/.claude/skills/playwright-skill