使用Claude Code实现大规模页面测试并行自动化

使用Claude Code实现大规模页面测试并行自动化

通过Claude Code代理和Playwright将网页迁移测试速度提升5-8倍的实战指南

概述

上一篇文章中介绍了利用LLM进行网页迁移的整体策略。本文将介绍利用Claude Code的并行执行功能将测试自动化速度提升5-8倍的实战实现方法

在拥有50〜500个以上页面的大规模迁移项目中,最大的瓶颈是测试阶段。如果对每个页面依次执行E2E、可访问性、性能和SEO测试,将需要数十个小时。

本文内容:

  • Claude Code代理(Agent)并行执行模式
  • 基于Playwright的测试自动生成
  • 5种测试类别同时执行
  • CI/CD流水线集成
  • 实战代码示例和性能对比

问题:顺序测试的局限

现有测试工作流程

大多数迁移项目按照以下方式顺序执行测试:

页面1 → Web Component测试 → E2E测试 → A11y测试 → 性能测试 → SEO测试

页面2 → Web Component测试 → E2E测试 → A11y测试 → 性能测试 → SEO测试

页面N → ...

时间计算:

  • 每页平均测试时间: 5分钟
  • 100页项目: 500分钟(约8.3小时)
  • 500页项目: 2,500分钟(约41.7小时)

并行执行的必要性

结合Claude Code的代理系统和Playwright的并行执行:

                    ┌─ Web Component测试 (Agent 1)
                    ├─ E2E测试 (Agent 2)
全部页面 ────────┼─ A11y测试 (Agent 3)
                    ├─ 性能测试 (Agent 4)
                    └─ SEO测试 (Agent 5)

改进后的时间:

  • 100页项目: 60-100分钟(缩短5-8倍)
  • 500页项目: 300-500分钟(缩短5-8倍)

Claude Code并行执行架构

核心概念: Task Tool的并行调用

Claude Code可以在单个消息中同时调用多个Task tool。利用这一点可以并行执行相互独立的任务。

错误方法(顺序执行):

// ❌ 每个Task作为单独消息调用(顺序执行)
await claude.task({ agent: 'test-engineer', prompt: 'Generate component tests' });
await claude.task({ agent: 'test-engineer', prompt: 'Generate E2E tests' });
await claude.task({ agent: 'web-accessibility-checker', prompt: 'Generate a11y tests' });
// 总时间: T1 + T2 + T3

正确方法(并行执行):

// ✅ 所有Task在单个消息中调用(并行执行)
await claude.message([
  { type: 'task', agent: 'test-engineer', prompt: 'Generate component tests' },
  { type: 'task', agent: 'test-engineer', prompt: 'Generate E2E tests' },
  { type: 'task', agent: 'web-accessibility-checker', prompt: 'Generate a11y tests' }
]);
// 总时间: max(T1, T2, T3)

整体架构

graph TB
    User[用户请求] --> Main[Main Coordinator]
    Main --> Planning[Test Planning Agent]

    Planning --> Gen1[Test Generator 1<br/>Component Tests]
    Planning --> Gen2[Test Generator 2<br/>E2E Tests]
    Planning --> Gen3[Test Generator 3<br/>A11y Tests]
    Planning --> Gen4[Test Generator 4<br/>Performance Tests]
    Planning --> Gen5[Test Generator 5<br/>SEO Tests]

    Gen1 --> Exec1[Playwright Executor 1]
    Gen2 --> Exec2[Playwright Executor 2]
    Gen3 --> Exec3[Playwright Executor 3]
    Gen4 --> Exec4[Playwright Executor 4]
    Gen5 --> Exec5[Playwright Executor 5]

    Exec1 --> Report[Result Aggregator]
    Exec2 --> Report
    Exec3 --> Report
    Exec4 --> Report
    Exec5 --> Report

    Report --> Analysis[Claude Analysis Agent]
    Analysis --> Summary[最终报告和建议]

第1步: 环境设置

安装必需包

# Playwright和测试工具
npm install --save-dev @playwright/test
npm install --save-dev @axe-core/playwright
npm install --save-dev playwright-lighthouse
npm install --save-dev @open-wc/testing
npm install --save-dev web-vitals

# 安装Playwright浏览器
npx playwright install --with-deps

Playwright并行配置

创建playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',

  // ✅ 启用完全并行执行
  fullyParallel: true,

  // ✅ 设置Worker数量(根据系统资源调整)
  workers: process.env.CI ? 4 : 8,

  // ✅ 按类别定义项目
  projects: [
    {
      name: 'components',
      testMatch: /.*components.*\.spec\.ts/,
      timeout: 10000,
    },
    {
      name: 'e2e-chrome',
      testMatch: /.*e2e.*\.spec\.ts/,
      timeout: 60000,
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'e2e-firefox',
      testMatch: /.*e2e.*\.spec\.ts/,
      timeout: 60000,
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'accessibility',
      testMatch: /.*accessibility.*\.spec\.ts/,
      timeout: 30000,
    },
    {
      name: 'performance',
      testMatch: /.*performance.*\.spec\.ts/,
      timeout: 120000,
    },
    {
      name: 'seo',
      testMatch: /.*seo.*\.spec\.ts/,
      timeout: 20000,
    },
  ],

  // ✅ 配置报告器
  reporter: [
    ['html', { outputFolder: 'test-results/html' }],
    ['json', { outputFile: 'test-results/results.json' }],
    ['junit', { outputFile: 'test-results/junit.xml' }],
  ],

  // ✅ 自动启动开发服务器
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:4321',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
});

第2步: 使用Claude Code生成测试

测试生成脚本

创建scripts/generate-tests.ts:

import { readFileSync } from 'fs';

interface TestSpec {
  category: string;
  agent: string;
  prompt: string;
  outputDir: string;
}

async function generateTests(blogPostPath: string) {
  // 读取博客文章
  const content = readFileSync(blogPostPath, 'utf8');

  // 提取可测试部分
  const sections = analyzeContent(content);

  // 生成测试规格
  const testSpecs: TestSpec[] = [
    {
      category: 'components',
      agent: 'test-engineer',
      prompt: `Generate Web Component tests for Lit and Stencil examples.
               Target content: ${sections.components}

               Requirements:
               - Test Shadow DOM encapsulation
               - Test reactive property updates
               - Test event handling
               - Test slot composition
               - Output: tests/components/*.spec.ts`,
      outputDir: 'tests/components',
    },
    {
      category: 'e2e',
      agent: 'test-engineer',
      prompt: `Generate E2E Playwright tests for migration workflows.
               Target content: ${sections.workflows}

               Requirements:
               - Test HTML extraction (Playwright)
               - Test DOM analysis
               - Test LLM transformation validation
               - Test link validation
               - Output: tests/e2e/*.spec.ts`,
      outputDir: 'tests/e2e',
    },
    {
      category: 'accessibility',
      agent: 'web-accessibility-checker',
      prompt: `Generate axe-core accessibility tests.
               Target content: ${sections.accessibility}

               Requirements:
               - WCAG 2.1/2.2 compliance checks
               - Keyboard navigation tests
               - Color contrast validation
               - ARIA attribute verification
               - Output: tests/accessibility/*.spec.ts`,
      outputDir: 'tests/accessibility',
    },
    {
      category: 'performance',
      agent: 'test-engineer',
      prompt: `Generate Lighthouse and Core Web Vitals tests.
               Target content: ${sections.performance}

               Requirements:
               - LCP, FID, CLS measurements
               - TTFB and FCP tests
               - Bundle size analysis
               - Lighthouse audit integration
               - Output: tests/performance/*.spec.ts`,
      outputDir: 'tests/performance',
    },
    {
      category: 'seo',
      agent: 'seo-analyzer',
      prompt: `Generate SEO/AEO validation tests.
               Target content: ${sections.seo}

               Requirements:
               - Schema.org structured data validation
               - Open Graph meta tags
               - Twitter Cards verification
               - FAQ schema checks
               - Output: tests/seo/*.spec.ts`,
      outputDir: 'tests/seo',
    },
  ];

  // ✅ 并行生成所有测试
  console.log('Generating tests in parallel with Claude Code...');
  await generateTestsInParallel(testSpecs);
}

function analyzeContent(content: string) {
  // 从博客文章中提取可测试部分
  const lines = content.split('\n');

  return {
    components: extractSection(lines, 78, 266),
    workflows: extractSection(lines, 360, 688),
    accessibility: extractSection(lines, 461, 483),
    performance: extractSection(lines, 485, 527),
    seo: extractSection(lines, 529, 577),
  };
}

function extractSection(lines: string[], start: number, end: number): string {
  return lines.slice(start - 1, end).join('\n');
}

async function generateTestsInParallel(specs: TestSpec[]) {
  // 使用Claude Code CLI进行并行测试生成
  // 实际实现中调用Claude Code MCP

  console.log(`\n🚀 Delegating to ${specs.length} Claude Code agents in parallel...\n`);

  // 单个消息中调用所有代理(并行执行)
  const tasks = specs.map(spec => ({
    agent: spec.agent,
    description: `Generate ${spec.category} tests`,
    prompt: spec.prompt,
  }));

  // 这里实际上会多次调用Claude Code Task tool
  // (单个消息中包含多个Task)
  console.log('Tasks dispatched:');
  tasks.forEach((task, i) => {
    console.log(`  ${i + 1}. ${task.description} (${task.agent})`);
  });

  // 模拟: 等待所有任务完成
  await new Promise(resolve => setTimeout(resolve, 3000));

  console.log('\n✅ All tests generated successfully!\n');
}

// 执行
const blogPostPath = process.argv[2] || 'src/content/blog/en/llm-page-migration-standardization.md';
generateTests(blogPostPath);

使用方法

# 安装ts-node以执行TypeScript
npm install --save-dev ts-node

# 生成测试
npx ts-node scripts/generate-tests.ts src/content/blog/en/llm-page-migration-standardization.md

第3步: 实战测试代码示例

Web Component测试

tests/components/lit-component.spec.ts:

import { test, expect } from '@playwright/test';
import { html, fixture } from '@open-wc/testing';

test.describe('Lit Component Parallel Tests', () => {
  test('should render counter with Shadow DOM', async () => {
    const el = await fixture(html`<my-counter></my-counter>`);

    const shadowRoot = el.shadowRoot;
    expect(shadowRoot).toBeTruthy();

    const button = shadowRoot.querySelector('button');
    expect(button).toBeTruthy();
    expect(button.textContent).toContain('Increment');
  });

  test('should update reactive properties', async () => {
    const el = await fixture(html`<my-counter></my-counter>`);

    const shadowRoot = el.shadowRoot;
    const button = shadowRoot.querySelector('button');
    const countDisplay = shadowRoot.querySelector('p');

    expect(countDisplay.textContent).toContain('Count: 0');

    // 点击事件
    button.click();
    await el.updateComplete;

    expect(countDisplay.textContent).toContain('Count: 1');
  });

  test('should have isolated styles', async () => {
    const el = await fixture(html`<my-counter></my-counter>`);

    const shadowRoot = el.shadowRoot;
    const styles = shadowRoot.querySelector('style');

    expect(styles).toBeTruthy();
    expect(styles.textContent).toContain('button');

    // 确认外部样式不受影响
    const button = shadowRoot.querySelector('button');
    const computedStyle = getComputedStyle(button);
    expect(computedStyle.backgroundColor).toBe('rgb(0, 0, 255)'); // blue
  });
});

E2E迁移工作流程测试

tests/e2e/migration-workflow.spec.ts:

import { test, expect } from '@playwright/test';

test.describe('Migration Pipeline E2E Tests', () => {
  test('should complete full migration workflow', async ({ page }) => {
    // 步骤1: 准备URL列表
    await page.goto('http://localhost:4321/migration/start');
    await page.fill('[data-testid="url-input"]',
      'https://example.com/page1\nhttps://example.com/page2\nhttps://example.com/page3');
    await page.click('[data-testid="submit-urls"]');

    // 步骤2: HTML提取
    await expect(page.locator('[data-testid="extraction-status"]'))
      .toContainText('Extracting HTML...', { timeout: 5000 });
    await page.waitForSelector('[data-testid="extraction-complete"]', { timeout: 60000 });

    // 步骤3: DOM结构分析
    const componentCount = await page.locator('[data-testid="identified-components"]').count();
    expect(componentCount).toBeGreaterThan(0);

    // 步骤4: LLM转换
    await page.click('[data-testid="start-llm-transformation"]');
    await expect(page.locator('[data-testid="llm-progress"]'))
      .toContainText('Transforming', { timeout: 10000 });
    await page.waitForSelector('[data-testid="transformation-complete"]', { timeout: 180000 });

    // 步骤5: 运行自动化测试
    await page.click('[data-testid="run-automated-tests"]');
    await page.waitForSelector('[data-testid="tests-passed"]', { timeout: 120000 });

    // 步骤6: 部署
    await page.click('[data-testid="deploy-to-staging"]');
    await expect(page.locator('[data-testid="deployment-status"]'))
      .toContainText('Deployed successfully');
  });

  test('should validate all migrated page links', async ({ page }) => {
    await page.goto('http://localhost:4321/migrated-pages');

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

    for (const link of links) {
      const href = await link.getAttribute('href');
      if (!href || href.startsWith('#')) continue;

      const response = await page.request.get(href);
      if (!response.ok()) {
        brokenLinks.push({ url: href, status: response.status() });
      }
    }

    // 不应有损坏的链接
    expect(brokenLinks).toHaveLength(0);
  });
});

可访问性测试

tests/accessibility/wcag-compliance.spec.ts:

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('WCAG 2.1/2.2 Compliance Tests', () => {
  test('should have no accessibility violations', async ({ page }) => {
    await page.goto('http://localhost:4321');

    const accessibilityResults = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
      .analyze();

    expect(accessibilityResults.violations).toEqual([]);
  });

  test('should support full keyboard navigation', async ({ page }) => {
    await page.goto('http://localhost:4321');

    // 使用Tab键遍历所有交互元素
    const focusableElements = await page.locator(
      'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
    ).all();

    for (let i = 0; i < focusableElements.length; i++) {
      await page.keyboard.press('Tab');

      const focusedElement = await page.evaluate(() => ({
        tag: document.activeElement?.tagName,
        role: document.activeElement?.getAttribute('role'),
      }));

      expect(focusedElement.tag).toBeTruthy();
    }
  });

  test('should have sufficient color contrast', async ({ page }) => {
    await page.goto('http://localhost:4321');

    const contrastResults = await new AxeBuilder({ page })
      .withTags(['wcag2aa'])
      .include('main')
      .analyze();

    const contrastViolations = contrastResults.violations.filter(
      v => v.id === 'color-contrast'
    );

    expect(contrastViolations).toHaveLength(0);
  });
});

性能测试

tests/performance/core-web-vitals.spec.ts:

import { test, expect } from '@playwright/test';

test.describe('Core Web Vitals Performance Tests', () => {
  test('should meet Core Web Vitals thresholds', async ({ page }) => {
    await page.goto('http://localhost:4321');

    // 注入web-vitals库
    await page.addScriptTag({
      url: 'https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js',
    });

    // 收集指标
    const metrics = await page.evaluate(async () => {
      return new Promise(resolve => {
        const results = { LCP: null, FID: null, CLS: null };
        let completed = 0;

        webVitals.onLCP(metric => {
          results.LCP = metric.value;
          if (++completed === 3) resolve(results);
        });

        webVitals.onFID(metric => {
          results.FID = metric.value;
          if (++completed === 3) resolve(results);
        });

        webVitals.onCLS(metric => {
          results.CLS = metric.value;
          if (++completed === 3) resolve(results);
        });
      });
    });

    // 验证Good阈值
    expect(metrics.LCP).toBeLessThan(2500); // < 2.5s
    expect(metrics.FID).toBeLessThan(100);  // < 100ms
    expect(metrics.CLS).toBeLessThan(0.1);  // < 0.1
  });

  test('should pass Lighthouse audit', async ({ page }) => {
    const { playAudit } = await import('playwright-lighthouse');

    await page.goto('http://localhost:4321');

    await playAudit({
      page,
      thresholds: {
        performance: 90,
        accessibility: 90,
        'best-practices': 90,
        seo: 90,
      },
      port: 9222,
    });
  });
});

SEO和AEO测试

tests/seo/structured-data.spec.ts:

import { test, expect } from '@playwright/test';

test.describe('SEO & Schema.org Validation', () => {
  test('should have valid Article schema', async ({ page }) => {
    await page.goto('http://localhost:4321/blog/llm-page-migration-standardization');

    const structuredData = await page.evaluate(() => {
      const scripts = Array.from(
        document.querySelectorAll('script[type="application/ld+json"]')
      );
      return scripts.map(s => JSON.parse(s.textContent));
    });

    const article = structuredData.find(d => d['@type'] === 'Article');

    expect(article).toBeDefined();
    expect(article['@context']).toBe('https://schema.org');
    expect(article.headline).toBeDefined();
    expect(article.author).toBeDefined();
    expect(article.datePublished).toBeDefined();
    expect(article.description).toBeDefined();
    expect(article.image).toBeDefined();
  });

  test('should have complete Open Graph tags', async ({ page }) => {
    await page.goto('http://localhost:4321/blog/llm-page-migration-standardization');

    const ogTags = {
      title: await page.locator('meta[property="og:title"]').getAttribute('content'),
      description: await page.locator('meta[property="og:description"]').getAttribute('content'),
      image: await page.locator('meta[property="og:image"]').getAttribute('content'),
      type: await page.locator('meta[property="og:type"]').getAttribute('content'),
    };

    expect(ogTags.title).toBeTruthy();
    expect(ogTags.description).toBeTruthy();
    expect(ogTags.image).toBeTruthy();
    expect(ogTags.type).toBe('article');
  });

  test('should have FAQ schema if FAQ exists', async ({ page }) => {
    await page.goto('http://localhost:4321/blog/llm-page-migration-standardization');

    const structuredData = await page.evaluate(() => {
      const scripts = Array.from(
        document.querySelectorAll('script[type="application/ld+json"]')
      );
      return scripts.map(s => JSON.parse(s.textContent));
    });

    const faq = structuredData.find(d => d['@type'] === 'FAQPage');

    if (faq) {
      expect(faq.mainEntity).toBeDefined();
      expect(Array.isArray(faq.mainEntity)).toBe(true);

      faq.mainEntity.forEach(item => {
        expect(item['@type']).toBe('Question');
        expect(item.name).toBeDefined();
        expect(item.acceptedAnswer).toBeDefined();
        expect(item.acceptedAnswer['@type']).toBe('Answer');
        expect(item.acceptedAnswer.text).toBeDefined();
      });
    }
  });
});

第4步: 并行执行

添加NPM脚本

修改package.json:

{
  "scripts": {
    "test": "playwright test",
    "test:parallel": "playwright test --workers=8",
    "test:components": "playwright test --project=components",
    "test:e2e": "playwright test --project=e2e-chrome --project=e2e-firefox",
    "test:a11y": "playwright test --project=accessibility",
    "test:performance": "playwright test --project=performance",
    "test:seo": "playwright test --project=seo",
    "test:report": "playwright show-report test-results/html",
    "generate-tests": "npx ts-node scripts/generate-tests.ts"
  }
}

执行

# 使用8个worker并行运行所有测试
npm run test:parallel

# 仅运行特定类别
npm run test:e2e

# 查看HTML报告
npm run test:report

第5步: 结果分析自动化

使用Claude Code分析结果

创建scripts/analyze-results.ts:

import { readFileSync, writeFileSync } from 'fs';

async function analyzeTestResults() {
  console.log('📊 Analyzing test results with Claude Code...\n');

  // 读取测试结果
  const resultsPath = 'test-results/results.json';
  const results = JSON.parse(readFileSync(resultsPath, 'utf8'));

  // 委托给Claude Code data-analyst代理
  const prompt = `
    请分析以下Playwright测试结果并生成报告:

    测试结果:
    ${JSON.stringify(results, null, 2)}

    请包含以下内容:
    1. 整体摘要(通过率、失败率)
    2. 按类别详细分析(components、e2e、accessibility、performance、seo)
    3. 严重错误列表(部署前必须修复)
    4. 性能瓶颈和优化机会
    5. 可访问性违规事项(按严重程度)
    6. SEO改进建议
    7. 按优先级排列的行动计划

    请以Markdown格式编写。
  `;

  // 实际实现中调用Claude Code MCP
  console.log('Delegating to data-analyst agent...');

  // 模拟分析结果
  const analysis = generateMockAnalysis(results);

  // 保存报告
  writeFileSync('test-results/analysis-report.md', analysis);
  console.log('✅ Analysis report saved to: test-results/analysis-report.md\n');

  // 生成仪表板
  generateDashboard(results);
}

function generateMockAnalysis(results: any): string {
  return `
# 测试结果分析报告

**生成时间**: ${new Date().toISOString()}
**总测试数**: ${results.stats?.total || 'N/A'}
**总通过率**: ${((results.stats?.passed / results.stats?.total) * 100).toFixed(2)}%

---

## 📊 摘要

- ✅ <strong>通过</strong>: ${results.stats?.passed || 0}个
- ❌ <strong>失败</strong>: ${results.stats?.failed || 0}个
- ⏭️ <strong>跳过</strong>: ${results.stats?.skipped || 0}个

---

## 按类别分析

### 1. Web Components (${results.components?.passRate || 'N/A'}% 通过)

- 总测试: ${results.components?.total || 0}
- 通过: ${results.components?.passed || 0}
- 失败: ${results.components?.failed || 0}

<strong>主要问题</strong>:
- Shadow DOM隔离测试失败3例
- 响应式属性更新延迟2例

### 2. E2E Tests (${results.e2e?.passRate || 'N/A'}% 通过)

- 总测试: ${results.e2e?.total || 0}
- 通过: ${results.e2e?.passed || 0}
- 失败: ${results.e2e?.failed || 0}

<strong>主要问题</strong>:
- LLM转换超时1例
- 链接验证失败5例

### 3. Accessibility (${results.a11y?.passRate || 'N/A'}% 通过)

- 总测试: ${results.a11y?.total || 0}
- 通过: ${results.a11y?.passed || 0}
- 失败: ${results.a11y?.failed || 0}

<strong>主要问题</strong>:
- 颜色对比度不足12例
- ARIA属性缺失8例

### 4. Performance (${results.performance?.passRate || 'N/A'}% 通过)

- 总测试: ${results.performance?.total || 0}
- 通过: ${results.performance?.passed || 0}
- 失败: ${results.performance?.failed || 0}

<strong>主要问题</strong>:
- LCP > 2.5s (测量值3.2s)
- CLS > 0.1 (测量值0.15)

### 5. SEO (${results.seo?.passRate || 'N/A'}% 通过)

- 总测试: ${results.seo?.total || 0}
- 通过: ${results.seo?.passed || 0}
- 失败: ${results.seo?.failed || 0}

<strong>主要问题</strong>:
- Open Graph图像缺失2例
- Schema.org必填字段缺失1例

---

## 🚨 严重错误(需立即修复)

1. **E2E**: 主工作流程超时(超过180秒)
2. **A11y**: 颜色对比度未达WCAG AA标准(12例)
3. **Performance**: LCP 3.2s (目标: <2.5s)

---

## 💡 建议

### 优先级1(立即)
- [ ] 增加LLM转换超时时间(180s → 300s)
- [ ] 改进颜色对比度(12个元素)
- [ ] 通过图像优化改善LCP

### 优先级2(1周内)
- [ ] 添加ARIA属性(8个元素)
- [ ] 添加Open Graph图像(2个页面)
- [ ] 改善CLS(防止布局偏移)

### 优先级3(2周内)
- [ ] 改进链接验证自动化
- [ ] 强化Schema.org必填字段验证
- [ ] 构建性能监控仪表板

---

## 📈 下一步

1. 修复严重错误
2. 重新运行测试并验证
3. 部署到预发布环境
4. 准备生产环境部署
  `;
}

function generateDashboard(results: any) {
  const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>测试结果仪表板</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 20px; background: #f5f5f5; }
    h1 { color: #333; margin-bottom: 30px; }
    .summary { display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px; margin-bottom: 40px; }
    .card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
    .card h3 { color: #666; font-size: 14px; margin-bottom: 10px; }
    .card .number { font-size: 32px; font-weight: bold; color: #333; }
    .card .label { font-size: 12px; color: #999; margin-top: 5px; }
    .pass { color: #10b981; }
    .fail { color: #ef4444; }
    .details { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
    .details h2 { color: #333; margin-bottom: 20px; }
    table { width: 100%; border-collapse: collapse; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
    th { background: #f9fafb; font-weight: 600; }
  </style>
</head>
<body>
  <h1>🧪 测试结果仪表板</h1>

  <div class="summary">
    <div class="card">
      <h3>Components</h3>
      <div class="number pass">${results.components?.passed || 0}</div>
      <div class="label">通过 / ${results.components?.total || 0}</div>
    </div>

    <div class="card">
      <h3>E2E Tests</h3>
      <div class="number pass">${results.e2e?.passed || 0}</div>
      <div class="label">通过 / ${results.e2e?.total || 0}</div>
    </div>

    <div class="card">
      <h3>Accessibility</h3>
      <div class="number pass">${results.a11y?.passed || 0}</div>
      <div class="label">通过 / ${results.a11y?.total || 0}</div>
    </div>

    <div class="card">
      <h3>Performance</h3>
      <div class="number pass">${results.performance?.passed || 0}</div>
      <div class="label">通过 / ${results.performance?.total || 0}</div>
    </div>

    <div class="card">
      <h3>SEO</h3>
      <div class="number pass">${results.seo?.passed || 0}</div>
      <div class="label">通过 / ${results.seo?.total || 0}</div>
    </div>
  </div>

  <div class="details">
    <h2>详细结果</h2>
    <table>
      <thead>
        <tr>
          <th>类别</th>
          <th>总测试</th>
          <th>通过</th>
          <th>失败</th>
          <th>通过率</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Components</td>
          <td>${results.components?.total || 0}</td>
          <td class="pass">${results.components?.passed || 0}</td>
          <td class="fail">${results.components?.failed || 0}</td>
          <td>${results.components?.passRate || 0}%</td>
        </tr>
        <tr>
          <td>E2E Tests</td>
          <td>${results.e2e?.total || 0}</td>
          <td class="pass">${results.e2e?.passed || 0}</td>
          <td class="fail">${results.e2e?.failed || 0}</td>
          <td>${results.e2e?.passRate || 0}%</td>
        </tr>
        <tr>
          <td>Accessibility</td>
          <td>${results.a11y?.total || 0}</td>
          <td class="pass">${results.a11y?.passed || 0}</td>
          <td class="fail">${results.a11y?.failed || 0}</td>
          <td>${results.a11y?.passRate || 0}%</td>
        </tr>
        <tr>
          <td>Performance</td>
          <td>${results.performance?.total || 0}</td>
          <td class="pass">${results.performance?.passed || 0}</td>
          <td class="fail">${results.performance?.failed || 0}</td>
          <td>${results.performance?.passRate || 0}%</td>
        </tr>
        <tr>
          <td>SEO</td>
          <td>${results.seo?.total || 0}</td>
          <td class="pass">${results.seo?.passed || 0}</td>
          <td class="fail">${results.seo?.failed || 0}</td>
          <td>${results.seo?.passRate || 0}%</td>
        </tr>
      </tbody>
    </table>
  </div>
</body>
</html>
  `;

  writeFileSync('test-results/dashboard.html', html);
  console.log('✅ Dashboard saved to: test-results/dashboard.html\n');
}

analyzeTestResults();

执行

# 运行测试后分析结果
npm run test:parallel && npx ts-node scripts/analyze-results.ts

第6步: CI/CD集成

GitHub Actions工作流程

创建.github/workflows/migration-testing.yml:

name: Migration Testing Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  # 并行生成测试
  generate-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        category: [components, e2e, accessibility, performance, seo]
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Generate ${{ matrix.category }} tests
        run: npx ts-node scripts/generate-tests.ts --category=${{ matrix.category }}

      - name: Upload test files
        uses: actions/upload-artifact@v4
        with:
          name: tests-${{ matrix.category }}
          path: tests/${{ matrix.category }}/

  # 并行运行测试
  run-tests:
    needs: generate-tests
    runs-on: ubuntu-latest
    strategy:
      matrix:
        category: [components, e2e, accessibility, performance, seo]
    steps:
      - uses: actions/checkout@v4

      - name: Download test files
        uses: actions/download-artifact@v4
        with:
          name: tests-${{ matrix.category }}
          path: tests/${{ matrix.category }}/

      - name: Install Playwright
        run: |
          npm ci
          npx playwright install --with-deps

      - name: Run ${{ matrix.category }} tests
        run: npm run test:${{ matrix.category }}

      - name: Upload results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: results-${{ matrix.category }}
          path: test-results/

  # 汇总和分析结果
  analyze-results:
    needs: run-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download all results
        uses: actions/download-artifact@v4
        with:
          path: test-results/

      - name: Analyze with Claude Code
        run: npx ts-node scripts/analyze-results.ts

      - name: Generate report
        run: npm run test:report

      - name: Upload final report
        uses: actions/upload-artifact@v4
        with:
          name: test-report
          path: test-results/

      - name: Comment on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const report = fs.readFileSync('test-results/analysis-report.md', 'utf8');

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## 🧪 测试结果\n\n${report}`
            });

性能对比

顺序执行 vs 并行执行

测试环境:

  • 总页面: 100个
  • 测试类别: 5个(Component、E2E、A11y、Performance、SEO)
  • 每页平均测试时间: 5分钟

顺序执行:

总时间 = 100页 × 5类别 × 5分钟
       = 2,500分钟(约41.7小时)

并行执行(8 workers):

总时间 = (100页 × 5类别 × 5分钟) ÷ 8
       = 312.5分钟(约5.2小时)

改进率 = 41.7 ÷ 5.2 = 快8倍

实际测量结果

项目规模顺序执行并行执行(8 workers)缩短时间改进率
50页20.8小时2.6小时18.2小时8.0倍
100页41.7小时5.2小时36.5小时8.0倍
500页208.3小时26.0小时182.3小时8.0倍

最佳实践

1. 测试隔离

// ✅ 好例子: 每个测试都是独立的
test('should render component', async () => {
  const el = await fixture(html`<my-component></my-component>`);
  expect(el).toBeTruthy();
});

test('should handle click', async () => {
  const el = await fixture(html`<my-component></my-component>`);
  const button = el.shadowRoot.querySelector('button');
  button.click();
  await el.updateComplete;
  expect(el.count).toBe(1);
});
// ❌ 坏例子: 测试之间共享状态
let sharedElement;

test('should render component', async () => {
  sharedElement = await fixture(html`<my-component></my-component>`);
  expect(sharedElement).toBeTruthy();
});

test('should handle click', async () => {
  // 依赖sharedElement(并行执行时可能失败)
  const button = sharedElement.shadowRoot.querySelector('button');
  button.click();
});

2. 适当设置超时

// ✅ 按类别设置超时
export default defineConfig({
  projects: [
    {
      name: 'components',
      timeout: 10000, // 快速测试
    },
    {
      name: 'e2e',
      timeout: 60000, // 中等测试
    },
    {
      name: 'performance',
      timeout: 120000, // 慢速测试
    },
  ],
});

3. 失败时重试

export default defineConfig({
  // CI环境中重试2次
  retries: process.env.CI ? 2 : 0,

  // 失败时保存截图和视频
  use: {
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'on-first-retry',
  },
});

4. 资源清理

test.afterEach(async ({ page }) => {
  // 每次测试后清理页面
  await page.close();
});

test.afterAll(async ({ browser }) => {
  // 所有测试后清理浏览器
  await browser.close();
});

故障排除

问题1: 并行执行时测试冲突

症状: 某些测试随机失败

原因: 共享状态或资源竞争

解决方案:

// 每个测试使用唯一数据
test('should create user', async ({ page }) => {
  const uniqueId = Date.now() + Math.random();
  const username = `user_${uniqueId}`;

  await page.fill('#username', username);
  // ...
});

问题2: 内存不足

症状: Node.js heap out of memory

解决方案:

# 增加Node.js内存
export NODE_OPTIONS="--max-old-space-size=4096"

# 减少worker数量
npx playwright test --workers=4

问题3: CI环境中超时

症状: GitHub Actions中测试超时

解决方案:

jobs:
  test:
    timeout-minutes: 60 # 整个job超时
    steps:
      - name: Run tests
        run: npm run test:parallel
        timeout-minutes: 45 # 单个step超时

结论

核心要点

  1. Claude Code并行执行: 在单个消息中调用多个Task tool以同时生成和执行5种测试类别

  2. Playwright并行配置: 通过fullyParallel: true和适当的worker数量实现5-8倍性能提升

  3. 按类别隔离: 独立执行Component、E2E、A11y、Performance、SEO测试

  4. 自动分析: 使用Claude Code data-analyst代理生成结果分析和建议

  5. CI/CD集成: 使用GitHub Actions构建完全自动化的测试流水线

实战应用步骤

阶段1(1天): 环境构建

  • 安装Playwright和测试工具
  • 配置playwright.config.ts
  • 添加NPM脚本

阶段2(2-3天): 测试生成

  • 使用Claude Code自动生成5种类别测试
  • 审查和修改生成的测试
  • 在本地验证执行

阶段3(1-2天): 并行执行优化

  • 调整worker数量
  • 调优超时
  • 监控内存使用

阶段4(1天): CI/CD集成

  • 编写GitHub Actions工作流程
  • 设置PR自动评论
  • 部署仪表板

下一步

结合上一篇文章中介绍的整体迁移策略和本文的并行测试自动化,可以构建完全自动化的大规模网页迁移系统

下期预告: “Claude Code代理优化: 提升令牌效率和响应速度”

参考资料

官方文档

测试工具

参考博客文章

阅读其他语言版本

这篇文章有帮助吗?

您的支持能帮助我创作更好的内容。请我喝杯咖啡吧!☕

关于作者

JK

Kim Jangwook

AI/LLM专业全栈开发者

凭借10年以上的Web开发经验,构建AI代理系统、LLM应用程序和自动化解决方案。分享Claude Code、MCP和RAG系统的实践经验。