mcpField
P
playwright-skill

playwright-skill

github.com/dedalus-erp-pas测试与质量220 次安装
代码仓库

关于 playwright-skill

用于 Playwright 网页测试与自动化的技能,可编写自定义 JavaScript 脚本。它指导开发者管理测试服务器、检查页面并执行交互与验证。

主要功能

  1. 01自定义 JavaScript 浏览器自动化
  2. 02检查或启动测试服务器
  3. 03交互前侦察页面 DOM
  4. 04测试表单与导航流程
  5. 05截图和网络检查

适用场景

  1. 01测试登录与表单流程
  2. 02检查不同视口下的响应式布局
  3. 03查找失效链接和未加载图片
  4. 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

关键工作流

  1. 检查测试服务器是否正在运行 - 检测正在运行的开发服务器,或根据需要启动服务器
  2. 将脚本写入 /tmp - 绝不要将测试文件写入技能目录;始终使用 /tmp/playwright-test-*.js
  3. 默认使用可视化浏览器 - 除非用户明确要求无头模式,否则始终使用 headless: false
  4. 等待动态内容 - 检查动态 Web 应用前,使用 waitForLoadState('networkidle')
  5. 将 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