返回 Skill 列表
extension
分类: 其它需要 API Key

winson-html-dashboard-auto-test-skill

专为 HTML Dashboard 设计的自动化测试 + AI 自动修复 Skill。测试发现问题后自动调用 AI 诊断并生成代码修复,循环验证直到通过,实现真正的无人值守质量守护。

person作者: user_add7f3d3hubcommunity

Winson HTML Dashboard 自动化测试 + AI 自动修复 Skill

一、核心哲学

1.1 唯一铁律

测试发现问题只是开始,AI 自动修复才是目标。

传统的自动化测试只负责"发现问题",把修复工作丢给开发者。这个 Skill 的铁律是:测试失败 → AI 自动诊断 → 生成修复代码 → 应用修复 → 重新验证。人工只在最后一步确认,中间全部自动化。

1.2 三个底层认知

| 认知 | 说明 | |------|------| | 测试是手段,修复是目的 | 只测不修等于浪费算力。AI 修复能力让这个 Skill 从"监控工具"升级为"自治系统"。 | | 循环不是重复测试,是修复验证闭环 | 每次循环不是简单重测,而是"测试→修复→重测"的完整闭环,直到问题消失。 | | 人工确认是最后防线,不是日常负担 | AI 修复后需要人工 review,但日常开发中开发者只需关注最终通过的绿色结果。 |

1.3 核心公式

自治质量保障 = (测试发现问题 + AI 自动修复 + 循环验证) × 持续运行

二、完整执行流程

Phase 1:环境准备(5分钟)

Step 1.1:安装核心工具链

# 安装 Playwright(测试引擎)
npm init playwright@latest

# 安装性能测试依赖
npm install --save-dev playwright-lighthouse get-port

# 安装文件监听(本地持续运行用)
npm install --save-dev chokidar

# 安装 AI 修复辅助库
npm install --save-dev @anthropic-ai/sdk  # 或 openai, 根据你用的 AI 服务

检验标准:运行 npx playwright --version 显示版本号 ≥ 1.40

Step 1.2:配置 Playwright + AI 修复

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: 0,  // 不重试,失败直接触发 AI 修复
  workers: 1,  // 单 worker,确保修复顺序执行
  reporter: [
    ['html', { open: 'never' }],
    ['json', { outputFile: 'test-results/results.json' }],
    ['./reporters/ai-fix-reporter.ts'],  // AI 修复报告器
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on',
    screenshot: 'on',
    video: 'on',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'Performance', use: { ...devices['Desktop Chrome'] } },
  ],
});

检验标准:运行 npx playwright test --list 能列出所有测试项目


Phase 2:AI 自动修复核心引擎

Step 2.1:AI 修复报告器(测试失败时自动触发)

// reporters/ai-fix-reporter.ts
import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';

class AIFixReporter implements Reporter {
  private failures: Array<{
    test: string;
    error: string;
    tracePath: string;
    screenshotPath: string;
  }> = [];
  
  private maxFixAttempts = 3;
  private currentAttempt = 0;

  onTestEnd(test: TestCase, result: TestResult) {
    if (result.status === 'failed') {
      // 收集失败证据
      const tracePath = result.attachments.find(a => a.name === 'trace')?.path || '';
      const screenshotPath = result.attachments.find(a => a.name === 'screenshot')?.path || '';
      
      this.failures.push({
        test: test.title,
        error: result.error?.message || 'Unknown error',
        tracePath,
        screenshotPath,
      });
    }
  }

  async onEnd() {
    if (this.failures.length === 0) {
      console.log('✅ 所有测试通过,无需修复');
      return;
    }

    console.log(`\n🤖 检测到 ${this.failures.length} 个测试失败,启动 AI 自动修复...\n`);
    
    for (const failure of this.failures) {
      await this.attemptFix(failure);
    }
  }

  private async attemptFix(failure: any) {
    this.currentAttempt++;
    
    if (this.currentAttempt > this.maxFixAttempts) {
      console.error(`❌ 达到最大修复次数 (${this.maxFixAttempts}),放弃修复: ${failure.test}`);
      return;
    }

    console.log(`\n🔧 尝试修复 (${this.currentAttempt}/${this.maxFixAttempts}): ${failure.test}`);

    // 1. 收集上下文信息
    const context = await this.gatherContext(failure);
    
    // 2. 调用 AI 生成修复
    const fix = await this.callAI(context);
    
    if (!fix) {
      console.log('❌ AI 无法生成修复方案');
      return;
    }

    // 3. 应用修复
    await this.applyFix(fix);
    
    // 4. 重新运行失败的测试验证
    console.log('🔄 重新运行测试验证修复...');
    const passed = await this.reRunTest(failure.test);
    
    if (passed) {
      console.log(`✅ 修复成功: ${failure.test}`);
    } else {
      console.log(`❌ 修复未通过,继续尝试...`);
      await this.attemptFix(failure);
    }
  }

  private async gatherContext(failure: any) {
    // 读取相关源代码
    const srcFiles = this.findRelatedFiles(failure.test);
    const testFile = this.findTestFile(failure.test);
    
    return {
      testName: failure.test,
      errorMessage: failure.error,
      tracePath: failure.tracePath,
      screenshotPath: failure.screenshotPath,
      sourceFiles: srcFiles,
      testFile: testFile,
      projectRoot: process.cwd(),
    };
  }

  private findRelatedFiles(testName: string): string[] {
    // 根据测试名称推断相关源文件
    const srcDir = path.join(process.cwd(), 'src');
    const files: string[] = [];
    
    // 读取 src 目录下的所有文件
    const readDir = (dir: string) => {
      const items = fs.readdirSync(dir);
      for (const item of items) {
        const fullPath = path.join(dir, item);
        const stat = fs.statSync(fullPath);
        if (stat.isDirectory()) {
          readDir(fullPath);
        } else if (/\.(js|ts|jsx|tsx|vue|html|css)$/.test(item)) {
          files.push(fullPath);
        }
      }
    };
    
    if (fs.existsSync(srcDir)) {
      readDir(srcDir);
    }
    
    return files;
  }

  private findTestFile(testName: string): string {
    const testDir = path.join(process.cwd(), 'tests');
    const files = fs.readdirSync(testDir);
    return files.find(f => f.includes(testName.split(' ')[0])) || '';
  }

  private async callAI(context: any): Promise<{ filePath: string; originalCode: string; fixedCode: string } | null> {
    // 读取截图(base64 编码)
    let screenshotBase64 = '';
    if (context.screenshotPath && fs.existsSync(context.screenshotPath)) {
      screenshotBase64 = fs.readFileSync(context.screenshotPath).toString('base64');
    }

    // 读取关键源文件内容
    const sourceContents = context.sourceFiles.slice(0, 5).map((file: string) => {
      try {
        return { path: file, content: fs.readFileSync(file, 'utf-8') };
      } catch {
        return null;
      }
    }).filter(Boolean);

    // 构建 AI Prompt
    const prompt = this.buildPrompt(context, sourceContents, screenshotBase64);

    // 调用 AI API(示例用 Claude,可替换为 OpenAI 等)
    try {
      const response = await fetch('https://api.anthropic.com/v1/messages', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': process.env.ANTHROPIC_API_KEY || '',
          'anthropic-version': '2023-06-01',
        },
        body: JSON.stringify({
          model: 'claude-3-sonnet-20240229',
          max_tokens: 4096,
          messages: [{ role: 'user', content: prompt }],
        }),
      });

      const data = await response.json();
      const aiResponse = data.content?.[0]?.text || '';

      // 解析 AI 返回的修复代码
      return this.parseFixResponse(aiResponse, context.sourceFiles);
    } catch (error) {
      console.error('AI 调用失败:', error);
      return null;
    }
  }

  private buildPrompt(context: any, sourceContents: any[], screenshotBase64: string): string {
    return `你是一个前端代码修复专家。请分析以下测试失败信息,并生成修复代码。

## 测试失败信息
- 测试名称: ${context.testName}
- 错误信息: ${context.errorMessage}

## 相关源代码文件
${sourceContents.map((f: any) => `
### ${f.path}
\`\`\`
${f.content.substring(0, 3000)}
\`\`\`
`).join('\n')}

## 任务
1. 分析错误原因
2. 确定需要修改的文件
3. 生成修复后的完整代码
4. 用以下格式返回修复:

FIX_START
文件路径: [文件路径]
原始代码:
[需要替换的原始代码块]
修复后代码:
[修复后的代码块]
FIX_END

如果无法确定修复方案,请说明原因。`;
  }

  private parseFixResponse(response: string, sourceFiles: string[]): { filePath: string; originalCode: string; fixedCode: string } | null {
    const fixMatch = response.match(/FIX_START\s*文件路径:\s*(.+?)\s*原始代码:\s*([\s\S]+?)\s*修复后代码:\s*([\s\S]+?)\s*FIX_END/);
    
    if (!fixMatch) {
      return null;
    }

    return {
      filePath: fixMatch[1].trim(),
      originalCode: fixMatch[2].trim(),
      fixedCode: fixMatch[3].trim(),
    };
  }

  private async applyFix(fix: { filePath: string; originalCode: string; fixedCode: string }) {
    console.log(`📝 应用修复到: ${fix.filePath}`);
    
    try {
      const content = fs.readFileSync(fix.filePath, 'utf-8');
      
      if (!content.includes(fix.originalCode)) {
        console.error('❌ 原始代码不匹配,无法应用修复');
        return;
      }

      // 备份原文件
      const backupPath = `${fix.filePath}.backup.${Date.now()}`;
      fs.writeFileSync(backupPath, content);
      
      // 应用修复
      const newContent = content.replace(fix.originalCode, fix.fixedCode);
      fs.writeFileSync(fix.filePath, newContent);
      
      console.log(`✅ 修复已应用,备份保存到: ${backupPath}`);
    } catch (error) {
      console.error('❌ 应用修复失败:', error);
    }
  }

  private async reRunTest(testName: string): Promise<boolean> {
    try {
      execSync(`npx playwright test --grep "${testName}" --reporter=line`, {
        stdio: 'inherit',
        timeout: 120000,
      });
      return true;
    } catch {
      return false;
    }
  }
}

export default AIFixReporter;

检验标准:运行测试失败后,控制台显示 "🤖 启动 AI 自动修复..."


Phase 3:测试层(功能 + 视觉 + 性能)

测试用例与原版相同,但增加了更详细的错误信息收集,方便 AI 诊断。

Step 3.1:增强版功能测试(带诊断信息)

// tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Dashboard 核心功能', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/dashboard');
    await page.waitForSelector('[data-testid="dashboard-grid"]', { 
      state: 'visible',
      timeout: 10000 
    });
  });

  test('所有关键组件正常渲染', async ({ page }) => {
    const components = ['stats-cards', 'main-chart', 'data-table'];
    
    for (const component of components) {
      const element = page.getByTestId(component);
      const isVisible = await element.isVisible().catch(() => false);
      
      if (!isVisible) {
        // 收集诊断信息
        const html = await page.content();
        const screenshot = await page.screenshot();
        
        console.error(`组件 ${component} 未找到。当前页面 HTML 片段:`, 
          html.substring(0, 500));
      }
      
      await expect(element, `组件 ${component} 应该可见`).toBeVisible();
    }
  });

  test('图表交互正常', async ({ page }) => {
    const chart = page.getByTestId('main-chart');
    
    await test.step('悬停显示 tooltip', async () => {
      await chart.hover();
      const tooltip = page.locator('.chart-tooltip');
      await expect(tooltip, 'Tooltip 应该在悬停后显示').toBeVisible({ timeout: 5000 });
    });
    
    await test.step('点击筛选数据', async () => {
      await chart.click();
      await expect(page, '点击后应该跳转到筛选页面').toHaveURL(/.*filtered=true/);
    });
  });

  test('数据表格排序与分页', async ({ page }) => {
    const table = page.getByTestId('data-table');
    
    await test.step('点击表头排序', async () => {
      const sortHeader = table.locator('th[data-sort="date"]');
      await sortHeader.click();
      
      const firstCell = table.locator('tbody tr:first-child td:first-child');
      await expect(firstCell, '排序后第一行应该包含 2024').toContainText('2024');
    });
    
    await test.step('分页切换', async () => {
      const nextButton = page.getByRole('button', { name: '下一页' });
      await nextButton.click();
      
      const rows = table.locator('tbody tr');
      const count = await rows.count();
      expect(count, `分页后应该有 10 行,实际有 ${count} 行`).toBe(10);
    });
  });
});

Step 3.2:视觉回归测试(AI 可对比截图差异)

// tests/visual.spec.ts
import { test, expect } from '@playwright/test';

test.describe('视觉回归测试', () => {
  test('Dashboard 首页截图对比', async ({ page }) => {
    await page.goto('/dashboard');
    
    // 等待图表渲染完成
    await page.waitForFunction(() => {
      const charts = document.querySelectorAll('.chart-container');
      return Array.from(charts).every(c => c.getAttribute('data-rendered') === 'true');
    }, { timeout: 10000 });
    
    // 截图并对比
    await expect(page).toHaveScreenshot('dashboard-fullpage.png', {
      fullPage: true,
      maxDiffPixels: 100,
    });
  });

  test('响应式布局 - 多设备截图', async ({ page }) => {
    const viewports = [
      { name: 'mobile', width: 375, height: 667 },
      { name: 'tablet', width: 768, height: 1024 },
      { name: 'desktop', width: 1920, height: 1080 },
    ];

    for (const viewport of viewports) {
      await page.setViewportSize({ width: viewport.width, height: viewport.height });
      await page.goto('/dashboard');
      
      await expect(page).toHaveScreenshot(`dashboard-${viewport.name}.png`, {
        fullPage: true,
      });
    }
  });
});

Step 3.3:性能测试(阈值失败触发 AI 优化)

// tests/performance/metrics.spec.ts
import { test, expect } from '@playwright/test';

test.describe('核心性能指标', () => {
  test('LCP 小于 2.5 秒', async ({ page }) => {
    await page.goto('/dashboard');
    
    const lcp = await page.evaluate(() => {
      return new Promise<number>((resolve) => {
        new PerformanceObserver((list) => {
          const entries = list.getEntries();
          resolve(entries[entries.length - 1].startTime);
        }).observe({ entryTypes: ['largest-contentful-paint'] });
      });
    });
    
    expect(lcp, `LCP 为 ${lcp}ms,应小于 2500ms`).toBeLessThan(2500);
  });

  test('内存使用监控', async ({ page }) => {
    await page.goto('/dashboard');
    
    // 执行交互后检查内存
    await page.click('[data-testid="refresh-data"]');
    await page.waitForTimeout(1000);
    
    const memory = await page.evaluate(() => {
      return (performance as any).memory?.usedJSHeapSize || 0;
    });
    
    const memoryMB = Math.round(memory / 1024 / 1024);
    expect(memory, `内存使用为 ${memoryMB}MB,应小于 100MB`).toBeLessThan(100 * 1024 * 1024);
  });
});

Phase 4:循环运行方案(测试 → 修复 → 验证 闭环)

Step 4.1:AI 修复循环脚本

// scripts/ai-fix-loop.js
const { spawn } = require('child_process');
const fs = require('fs');

const CONFIG = {
  interval: 30 * 1000,        // 测试间隔 30 秒
  maxFixAttempts: 3,          // 单次失败最大修复次数
  maxTotalLoops: 100,         // 最大总循环次数
  requireHumanConfirm: true,  // 是否人工确认修复
};

let loopCount = 0;
let consecutiveFailures = 0;

async function runLoop() {
  loopCount++;
  console.log(`\n========== 循环 ${loopCount}/${CONFIG.maxTotalLoops} ==========`);
  console.log(`[${new Date().toLocaleString()}] 开始测试运行...`);

  // 1. 运行测试
  const testPassed = await runTests();

  if (testPassed) {
    console.log('✅ 所有测试通过');
    consecutiveFailures = 0;
  } else {
    console.log('❌ 测试失败,AI 修复流程已启动(见上方输出)');
    consecutiveFailures++;

    if (consecutiveFailures >= 3) {
      console.error('🚨 告警:连续 3 轮测试失败,请人工介入检查');
      process.exit(1);
    }
  }

  // 2. 检查是否需要人工确认
  if (fs.existsSync('.pending-confirm')) {
    console.log('⏸️  有修复待人工确认,暂停循环');
    console.log('   请检查修改后运行: node scripts/confirm-fix.js');
    return; // 暂停循环
  }

  // 3. 定时下一次
  if (loopCount < CONFIG.maxTotalLoops) {
    setTimeout(runLoop, CONFIG.interval);
  } else {
    console.log('🏁 达到最大循环次数,停止运行');
  }
}

function runTests() {
  return new Promise((resolve) => {
    const proc = spawn('npx', ['playwright', 'test', '--reporter=line'], {
      stdio: 'inherit',
      shell: true,
    });

    proc.on('close', (code) => {
      resolve(code === 0);
    });
  });
}

// 启动
console.log('🚀 启动 AI 自动修复测试循环');
console.log(`配置: 间隔 ${CONFIG.interval / 1000}s, 最大修复 ${CONFIG.maxFixAttempts} 次`);
runLoop();

Step 4.2:人工确认脚本

// scripts/confirm-fix.js
const fs = require('fs');
const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

console.log('🔍 待确认的修复:');

// 读取备份文件列表
const backups = fs.readdirSync('.').filter(f => f.endsWith('.backup'));

if (backups.length === 0) {
  console.log('没有待确认的修复');
  process.exit(0);
}

backups.forEach((backup, i) => {
  const original = backup.replace(/\.backup\.\d+$/, '');
  console.log(`${i + 1}. ${original}`);
  console.log(`   备份: ${backup}`);
});

rl.question('\n确认应用这些修复? (y/n): ', (answer) => {
  if (answer.toLowerCase() === 'y') {
    // 删除备份(确认修复)
    backups.forEach(backup => fs.unlinkSync(backup));
    fs.unlinkSync('.pending-confirm');
    console.log('✅ 修复已确认');
  } else {
    // 回滚修复
    backups.forEach(backup => {
      const original = backup.replace(/\.backup\.\d+$/, '');
      fs.copyFileSync(backup, original);
      fs.unlinkSync(backup);
      console.log(`↩️  已回滚: ${original}`);
    });
    fs.unlinkSync('.pending-confirm');
    console.log('↩️  所有修复已回滚');
  }
  rl.close();
});

Step 4.3:package.json 脚本

{
  "scripts": {
    "test": "playwright test",
    "test:watch": "node scripts/watch-test.js",
    "test:loop": "node scripts/ai-fix-loop.js",
    "test:confirm": "node scripts/confirm-fix.js",
    "test:ui": "playwright test --ui"
  }
}

Phase 5:AI 修复 Prompt 模板库

Step 5.1:常见错误类型的 AI Prompt

// lib/ai-prompts.ts

export const prompts = {
  // 元素未找到
  elementNotFound: (error: string, html: string) => `
测试报错: ${error}

当前页面 HTML 结构:
\`\`\`html
${html.substring(0, 2000)}
\`\`\`

问题分析:
1. 检查选择器是否正确
2. 检查元素是否在 DOM 中
3. 检查是否有异步加载导致元素未出现

修复方案:
- 如果元素是异步加载的,添加等待逻辑
- 如果选择器错误,修正选择器
- 如果元素被条件渲染,检查条件逻辑

请生成修复代码。
`,

  // 视觉回归差异
  visualRegression: (diffPixels: number, screenshotPath: string) => `
视觉回归测试失败: 截图差异 ${diffPixels} 像素

截图路径: ${screenshotPath}

可能原因:
1. CSS 样式变更导致布局变化
2. 字体渲染差异
3. 动画/过渡效果未禁用
4. 动态内容(时间、随机数)导致差异

修复方案:
- 检查最近的 CSS 修改
- 在测试环境中禁用动画
- 对动态内容使用 mock 数据

请生成修复代码。
`,

  // 性能不达标
  performance: (metric: string, value: number, threshold: number) => `
性能测试失败: ${metric}
当前值: ${value}
阈值: ${threshold}

优化建议:
- 检查是否有阻塞渲染的资源
- 优化图片/资源加载
- 减少 JavaScript 执行时间
- 使用代码分割/懒加载

请生成性能优化代码。
`,

  // 交互失败
  interaction: (action: string, expected: string, actual: string) => `
交互测试失败:
操作: ${action}
期望结果: ${expected}
实际结果: ${actual}

可能原因:
1. 事件监听未正确绑定
2. 异步操作未完成
3. 状态管理错误
4. DOM 更新延迟

请生成修复代码。
`,
};

三、完整工作流示例

场景:你改了一个 CSS,导致按钮点不了

你修改了 src/components/Button.css
    ↓
运行 npm run test:loop
    ↓
🔄 循环 1: 测试运行
    ❌ dashboard.spec.ts: 按钮点击无响应
    ↓
🤖 AI 自动修复启动
    📸 收集截图证据
    📄 读取 Button.css 和 Button.tsx
    🧠 调用 AI 分析...
    ↓
AI 诊断: "CSS 中 pointer-events: none 导致按钮无法点击"
    ↓
📝 生成修复: 移除 pointer-events: none
    ↓
✅ 应用修复,备份原文件
    ↓
🔄 重新运行测试
    ✅ 测试通过!
    ↓
⏸️  暂停循环,等待人工确认
    ↓
你运行 npm run test:confirm
    查看修改 → 确认无误 → 输入 y
    ↓
✅ 修复确认,删除备份
    ↓
🔄 循环继续...

四、常见陷阱与避坑指南

| 陷阱 | 表现 | 修正方案 | |------|------|----------| | AI 修复越修越坏 | 连续修复后代码无法运行 | 设置 maxFixAttempts=3,超过后停止并告警 | | AI 修改了不该改的文件 | 修复范围扩散 | 限制 AI 只修改与错误相关的文件 | | 修复后功能对了但样式崩了 | 视觉测试未通过 | 功能修复后必须重新运行全部测试 | | AI 生成代码不符合项目规范 | 代码风格不一致 | 在 prompt 中加入项目代码规范 | | 循环消耗大量 API 费用 | AI 调用次数过多 | 设置合理的间隔和最大尝试次数 |


五、质量检验清单

  • [ ] Playwright 安装完成,AI API Key 配置正确
  • [ ] AI 修复报告器能正常触发
  • [ ] 测试失败后 AI 能生成修复代码
  • [ ] 修复能正确应用到源代码
  • [ ] 修复后重新测试能验证通过
  • [ ] 人工确认机制正常工作
  • [ ] 备份和回滚功能正常
  • [ ] 循环运行 10 轮以上无异常
  • [ ] API 费用在可接受范围内

六、执行原则

  1. AI 修复是默认行为,不是可选项:测试失败必须触发 AI 修复,不能只是报告
  2. 人工确认是最后防线:AI 修复后必须人工 review,不能直接提交到生产
  3. 修复范围要受限:AI 只能修改与错误直接相关的代码,不能大范围重构
  4. 保留回滚能力:每次修复必须备份原文件,确保可回滚
  5. 控制 API 成本:设置合理的修复尝试次数和循环间隔