本文适用于 AI 生成或 AI 辅助修改后的前端项目,目标是把"浏览器可以打开"升级为一套可重复的验收流程。
示例来自一个 Vite、TypeScript 与 Three.js 实现的四冲程发动机页面。实际浏览器验收覆盖:
1600 × 900桌面视口;390 × 844移动视口;- 连续 12 秒的动画状态;
- 播放、重置、滑块、剖视和四个相机预设;
- 浏览器控制台与生产构建。
需要先说明:原项目完成了真实浏览器检查和截图验证,但没有新增一套 Playwright Test 自动化工程。本文代码是把这次人工验收过程整理成可复用测试模板,不把它写成原项目已经执行过的自动化测试结果。

1. 安装与启动
在待测项目前安装 Playwright Test:
bash
npm install -D @playwright/test
npx playwright install chromium
先启动本地开发服务:
bash
npm run dev -- --host 127.0.0.1
以下示例假定页面地址为:
text
http://127.0.0.1:5173/
也可以在 playwright.config.ts 中配置 webServer,让测试命令自动启动服务:
ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'http://127.0.0.1:5173',
trace: 'retain-on-failure',
},
webServer: {
command: 'npm run dev -- --host 127.0.0.1',
url: 'http://127.0.0.1:5173',
reuseExistingServer: true,
},
});
2. 响应式视口:桌面与手机分别验收
Three.js 页面除了 DOM 布局,还要检查相机构图。宽屏镜头直接用于窄屏时,模型可能被顶部状态栏与底部控制面板夹住。
示例项目最终为桌面与移动端分别维护相机预设,并在跨过 640px 断点时回到对应的全景视角。测试时也应固定两个独立视口:
ts
import { test, expect } from '@playwright/test';
const viewports = [
{ name: 'desktop', width: 1600, height: 900 },
{ name: 'mobile', width: 390, height: 844 },
];
for (const viewport of viewports) {
test(`${viewport.name} viewport`, async ({ page }) => {
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
});
await page.goto('/');
await expect(page.locator('canvas')).toBeVisible();
await expect(page.getByRole('button', { name: /暂停/ })).toBeVisible();
});
}
建议至少检查以下项目:
| 检查项 | 桌面端 | 移动端 |
|---|---|---|
| 关键主体 | 是否完整进入主视区 | 是否被顶部、底部 UI 遮挡 |
| 操作控件 | 是否全部可见、可点击 | 是否裁切或产生横向溢出 |
| 文本标签 | 是否与对象对应 | 是否被屏幕边缘裁切 |
| Canvas | 是否可见且非空白 | 调整尺寸后是否重新渲染 |
3. 动态页面:冻结业务动画后再做截图基线
Playwright Test 支持 expect(page).toHaveScreenshot()。第一次运行生成基线图,后续运行与基线比较。
但 WebGL 页面有一个额外问题:Canvas 通常由 requestAnimationFrame 持续更新。animations: 'disabled' 可以处理 CSS 动画,不会自动暂停 Three.js 渲染循环中的业务状态。
如果页面提供暂停按钮,应先暂停到稳定状态:
ts
test('stable visual snapshot', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
await page.getByRole('button', { name: /暂停/ }).click();
await expect(page.getByRole('button', { name: /播放/ })).toBeVisible();
await expect(page).toHaveScreenshot('engine-mobile.png', {
animations: 'disabled',
maxDiffPixelRatio: 0.01,
});
});
如果项目没有暂停入口,可以在测试环境暴露确定性的时间控制接口,或让渲染逻辑接受固定时间值。不要依赖随机等待后截图,否则每次基线都可能停在不同帧。
Playwright 官方文档还提醒:截图会受到操作系统、浏览器版本、字体、硬件、供电状态和 headless 模式影响。基线与复测应保持同一运行环境,CI 中最好固定浏览器镜像。
4. 动画状态:不能只检查第一帧
发动机页面的四冲程由 720° 状态循环驱动。实际验收连续运行了 12 秒,并检查吸气、喷油、点火、做功和排气是否连贯。
这类页面应把业务状态验收与视觉截图分开:
- 视觉截图回答"这一帧是否正确";
- 状态断言回答"流程是否走到目标节点";
- 完整运行回答"多个节点之间是否连续"。
以页面中的循环计数与阶段文字为例,可以等待状态变化:
ts
test('cycle advances and can reset', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('吸气', { exact: true })).toBeVisible();
await expect(page.locator('#ro-cycle')).not.toHaveText('0', {
timeout: 15_000,
});
await page.getByRole('button', { name: /重置/ }).click();
await expect(page.locator('#ro-cycle')).toHaveText('0');
await expect(page.locator('#ro-angle')).toHaveText('0°');
});
示例中的等待时间与当前默认转速和慢放倍率有关。更稳妥的工程做法是给测试提供可控时钟,而不是把业务速度硬编码进断言。
5. 交互回归:检查切换后的恢复状态
按钮"能点"还不够。相机特写、剖视和重置都会修改多处状态,测试应覆盖从一种状态切到另一种状态后的恢复结果。
ts
test('camera presets and controls', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: /暂停/ }).click();
for (const name of ['全景', '侧面', '缸内', '气门']) {
await page.getByRole('button', { name, exact: true }).click();
await page.waitForTimeout(450);
await expect(page.locator('canvas')).toBeVisible();
}
await page.getByLabel(/剖视/).check();
await expect(page.getByLabel(/剖视/)).toBeChecked();
await page.locator('#in-rpm').fill('300');
await expect(page.locator('#v-rpm')).toHaveText('300');
await page.getByRole('button', { name: /重置/ }).click();
await expect(page.locator('#ro-cycle')).toHaveText('0');
});
示例页面的相机缓动约为 400ms,因此代码等待 450ms。实际项目应优先等待可观察状态,例如按钮状态、目标元素或自定义完成事件;只有缺少可观察信号时才使用固定等待。
6. 控制台、页面异常与生产构建
运行时错误要在 goto() 之前监听:
ts
test('no runtime errors', async ({ page }) => {
const errors: string[] = [];
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text());
});
page.on('pageerror', (error) => {
errors.push(error.message);
});
await page.goto('/');
await page.waitForTimeout(12_000);
expect(errors).toEqual([]);
});
浏览器验收后还要运行生产构建:
bash
npm run build
示例项目的 Vite/TypeScript 构建通过,桌面与移动端浏览器检查也没有 JavaScript 错误。构建仍提示主 JavaScript 包超过 500 kB。这属于需要记录和评估的体积警告,不等同于页面运行失败。
7. 推荐的最小验收清单

提交 AI 生成的前端代码前,至少完成以下检查:
- 固定桌面和移动视口并分别截图;
- 确认关键 DOM、Canvas 与操作控件可见;
- 让动画或异步流程覆盖完整业务状态;
- 冻结动态内容后生成视觉基线;
- 操作全部关键控件并检查恢复状态;
- 监听
console error与pageerror; - 运行生产构建并记录警告;
- 在固定环境中复测视觉差异。
"能打开"适合作为冒烟检查。只有视口、状态、交互和错误都得到明确结论,才适合作为交付验收。
标签:Playwright、AI 编程、前端测试、视觉回归、Three.js、响应式测试