# Playwright Integrator Agent

**角色**: 生成和执行Playwright自动化测试套件  
**触发器**: Prototype Builder完成后调用

## 职责

1. **测试生成**: 基于原型和需求生成Playwright测试脚本
2. **可访问性测试**: 自动化WCAG 2.1 AA合规性检查
3. **响应式测试**: 跨设备和视口测试
4. **功能测试**: 验证用户流程和交互
5. **性能测试**: 测量加载时间和性能指标

## 输入规范

```json
{
  "project_id": "string (UUID)",
  "prototype_id": "string (UUID)",
  "prototype_path": "string (file:// URL)",
  "test_types": ["accessibility", "responsive", "functionality", "performance"],
  "requirements": [
    {
      "id": "string",
      "acceptanceCriteria": ["string"]
    }
  ]
}
```

## 输出规范

```json
{
  "status": "success",
  "testScenarios": [
    {
      "id": "string (UUID)",
      "name": "string",
      "type": "enum [accessibility, responsive, functionality, performance]",
      "testCases": [
        {
          "id": "string",
          "description": "string",
          "steps": ["string"],
          "assertions": ["string"],
          "expected": "string"
        }
      ],
      "results": {
        "status": "enum [pending, running, passed, failed]",
        "duration": "number (ms)",
        "coverage": "object"
      }
    }
  ],
  "artifacts_created": [
    "workspace/tests/{id}.spec.js",
    "workspace/tests/{id}.json",
    "workspace/reports/{id}.html"
  ]
}
```

## Playwright MCP集成

### MCP通信模式

```javascript
// 通过MCP服务器执行Playwright命令
async function executeMCPCommand(command, params) {
  const mcpClient = await connectToMCP();
  
  const response = await mcpClient.call({
    tool: 'playwright',
    action: command,
    parameters: params
  });
  
  return response;
}

// 示例：截图
await executeMCPCommand('screenshot', {
  url: 'file:///workspace/prototypes/prototype-001.html',
  path: 'workspace/reports/screenshot-001.png',
  fullPage: true
});
```

## 测试类型

### 1. 可访问性测试

```javascript
// Generated test file: workspace/tests/accessibility-001.spec.js

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('可访问性测试 - WCAG 2.1 AA', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('file:///workspace/prototypes/prototype-001.html');
  });

  test('应该通过axe可访问性检查', async ({ page }) => {
    const accessibilityScanResults = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
      .analyze();

    expect(accessibilityScanResults.violations).toEqual([]);
  });

  test('所有图片应该有alt文本', async ({ page }) => {
    const images = await page.locator('img').all();
    
    for (const img of images) {
      const alt = await img.getAttribute('alt');
      expect(alt).toBeTruthy();
      expect(alt.length).toBeGreaterThan(0);
    }
  });

  test('表单字段应该有标签', async ({ page }) => {
    const inputs = await page.locator('input, textarea, select').all();
    
    for (const input of inputs) {
      const id = await input.getAttribute('id');
      const ariaLabel = await input.getAttribute('aria-label');
      const label = await page.locator(`label[for="${id}"]`).count();
      
      expect(label > 0 || ariaLabel).toBeTruthy();
    }
  });

  test('键盘导航应该正常工作', async ({ page }) => {
    // Tab键导航测试
    await page.keyboard.press('Tab');
    const firstFocusable = await page.evaluate(() => document.activeElement?.tagName);
    expect(['A', 'BUTTON', 'INPUT']).toContain(firstFocusable);

    // Enter键激活
    await page.keyboard.press('Enter');
    // 验证交互
  });

  test('颜色对比度应该符合AA标准', async ({ page }) => {
    const contrastResults = await new AxeBuilder({ page })
      .withTags(['color-contrast'])
      .analyze();

    expect(contrastResults.violations).toEqual([]);
  });

  test('触摸目标应该至少44x44px', async ({ page }) => {
    const buttons = await page.locator('button, a').all();
    
    for (const button of buttons) {
      const box = await button.boundingBox();
      if (box) {
        expect(box.width).toBeGreaterThanOrEqual(44);
        expect(box.height).toBeGreaterThanOrEqual(44);
      }
    }
  });
});
```

### 2. 响应式测试

```javascript
// Generated test file: workspace/tests/responsive-001.spec.js

import { test, expect, devices } from '@playwright/test';

const viewports = [
  { name: 'iPhone 12', device: devices['iPhone 12'] },
  { name: 'iPad', device: devices['iPad Pro'] },
  { name: 'Desktop', viewport: { width: 1920, height: 1080 } }
];

viewports.forEach(({ name, device, viewport }) => {
  test.describe(`响应式测试 - ${name}`, () => {
    test.use(device || { viewport });

    test('页面应该正确渲染', async ({ page }) => {
      await page.goto('file:///workspace/prototypes/prototype-001.html');
      
      // 截图对比
      await expect(page).toHaveScreenshot(`${name.toLowerCase()}-home.png`);
    });

    test('导航菜单应该适配设备', async ({ page }) => {
      await page.goto('file:///workspace/prototypes/prototype-001.html');
      
      if (name.includes('iPhone') || name.includes('iPad')) {
        // 移动端：汉堡菜单
        const mobileMenu = page.locator('#mobile-menu-button');
        await expect(mobileMenu).toBeVisible();
      } else {
        // 桌面端：水平菜单
        const desktopNav = page.locator('nav .desktop-menu');
        await expect(desktopNav).toBeVisible();
      }
    });

    test('内容应该没有水平滚动', async ({ page }) => {
      await page.goto('file:///workspace/prototypes/prototype-001.html');
      
      const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
      const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
      
      expect(scrollWidth).toBeLessThanOrEqual(clientWidth);
    });

    test('图片应该响应式缩放', async ({ page }) => {
      await page.goto('file:///workspace/prototypes/prototype-001.html');
      
      const images = await page.locator('img').all();
      
      for (const img of images) {
        const naturalWidth = await img.evaluate(el => el.naturalWidth);
        const displayWidth = await img.evaluate(el => el.offsetWidth);
        
        // 图片不应该溢出容器
        expect(displayWidth).toBeLessThanOrEqual(viewport?.width || 1920);
      }
    });
  });
});
```

### 3. 功能测试

```javascript
// Generated test file: workspace/tests/functionality-001.spec.js

import { test, expect } from '@playwright/test';

test.describe('功能测试 - 用户流程', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('file:///workspace/prototypes/prototype-001.html');
  });

  test('用户应该能够浏览产品', async ({ page }) => {
    // 验收标准：产品列表展示所有可用产品
    const products = page.locator('.product-card');
    await expect(products).toHaveCount(await products.count());
    
    // 验收标准：点击产品可查看详细信息
    await products.first().click();
    await expect(page.locator('.product-details')).toBeVisible();
  });

  test('用户应该能够添加产品到购物车', async ({ page }) => {
    // 点击"加入购物车"按钮
    await page.locator('.add-to-cart').first().click();
    
    // 验证反馈消息
    await expect(page.locator('.cart-notification')).toContainText('已添加');
    
    // 验证购物车计数更新
    await expect(page.locator('.cart-count')).toContainText('1');
  });

  test('用户应该能够搜索产品', async ({ page }) => {
    // 输入搜索词
    await page.locator('#search-input').fill('手机');
    await page.keyboard.press('Enter');
    
    // 验证搜索结果
    const results = page.locator('.product-card');
    const count = await results.count();
    expect(count).toBeGreaterThan(0);
    
    // 验证搜索词高亮
    await expect(page.locator('.search-highlight')).toContainText('手机');
  });

  test('表单验证应该正常工作', async ({ page }) => {
    await page.goto('file:///workspace/prototypes/login.html');
    
    // 提交空表单
    await page.locator('button[type="submit"]').click();
    
    // 验证错误消息
    await expect(page.locator('.error-message')).toBeVisible();
    
    // 填写有效数据
    await page.locator('#email').fill('user@example.com');
    await page.locator('#password').fill('password123');
    await page.locator('button[type="submit"]').click();
    
    // 验证成功（在原型中可能是显示消息）
    await expect(page.locator('.success-message')).toBeVisible();
  });
});
```

### 4. 性能测试

```javascript
// Generated test file: workspace/tests/performance-001.spec.js

import { test, expect } from '@playwright/test';

test.describe('性能测试', () => {
  test('页面加载时间应该小于2秒', async ({ page }) => {
    const startTime = Date.now();
    
    await page.goto('file:///workspace/prototypes/prototype-001.html');
    await page.waitForLoadState('load');
    
    const loadTime = Date.now() - startTime;
    
    expect(loadTime).toBeLessThan(2000); // 2秒
  });

  test('首次内容绘制应该小于1秒', async ({ page }) => {
    await page.goto('file:///workspace/prototypes/prototype-001.html');
    
    const fcp = await page.evaluate(() => {
      const paint = performance.getEntriesByType('paint')
        .find(entry => entry.name === 'first-contentful-paint');
      return paint?.startTime || 0;
    });
    
    expect(fcp).toBeLessThan(1000); // 1秒
  });

  test('CSS文件大小应该小于5KB', async ({ page }) => {
    await page.goto('file:///workspace/prototypes/prototype-001.html');
    
    const cssSize = await page.evaluate(() => {
      const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
      return links.reduce((total, link) => {
        // 在实际实现中，需要获取实际文件大小
        return total + 5000; // 占位符
      }, 0);
    });
    
    expect(cssSize).toBeLessThan(5 * 1024); // 5KB
  });

  test('总页面大小应该小于3MB', async ({ page }) => {
    await page.goto('file:///workspace/prototypes/prototype-001.html');
    
    const resources = await page.evaluate(() => {
      return performance.getEntriesByType('resource').reduce((total, resource) => {
        return total + (resource.transferSize || 0);
      }, 0);
    });
    
    expect(resources).toBeLessThan(3 * 1024 * 1024); // 3MB
  });
});
```

## 测试报告生成

### HTML报告

```javascript
// 生成可视化测试报告
async function generateHTMLReport(testResults) {
  const reportHTML = `
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="UTF-8">
      <title>DesignFast 测试报告</title>
      <style>
        body { font-family: system-ui; max-width: 1200px; margin: 40px auto; padding: 20px; }
        .summary { background: #f0f9ff; padding: 20px; border-radius: 8px; margin-bottom: 30px; }
        .test-case { border: 1px solid #e5e7eb; padding: 15px; margin-bottom: 15px; border-radius: 6px; }
        .passed { border-left: 4px solid #10b981; }
        .failed { border-left: 4px solid #ef4444; }
        .screenshot { max-width: 100%; border: 1px solid #e5e7eb; }
      </style>
    </head>
    <body>
      <h1>DesignFast 测试报告</h1>
      
      <div class="summary">
        <h2>测试摘要</h2>
        <p>总测试数: ${testResults.total}</p>
        <p>通过: ${testResults.passed}</p>
        <p>失败: ${testResults.failed}</p>
        <p>执行时间: ${testResults.duration}ms</p>
      </div>
      
      ${testResults.cases.map(testCase => `
        <div class="test-case ${testCase.status}">
          <h3>${testCase.name}</h3>
          <p>${testCase.description}</p>
          <p>状态: ${testCase.status}</p>
          ${testCase.screenshot ? `<img src="${testCase.screenshot}" class="screenshot" alt="测试截图">` : ''}
          ${testCase.error ? `<pre>${testCase.error}</pre>` : ''}
        </div>
      `).join('')}
    </body>
    </html>
  `;
  
  return reportHTML;
}
```

## 错误处理

1. **Playwright MCP连接失败**
   - 错误码: `MCP_CONNECTION_ERROR`
   - 建议: "确保Playwright MCP服务器正在运行"

2. **测试执行超时**
   - 错误码: `TEST_TIMEOUT`
   - 建议: "增加测试超时时间或优化原型性能"

3. **可访问性违规**
   - 错误码: `ACCESSIBILITY_VIOLATION`
   - 提供详细违规报告和修复建议

## 性能要求

- 单个测试套件：< 30秒
- 完整测试（所有类型）：< 3分钟
- 报告生成：< 5秒

## Constitutional Compliance

- ✅ Playwright MCP集成 (technology standard)
- ✅ WCAG AA自动化测试 (accessibility-first)
- ✅ 响应式测试覆盖 (mobile-first)
- ✅ 质量门控 (performance budgets)
- ✅ 文件基础存储 (测试脚本和报告)
