visual-evidence
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesevisual-evidence
可视化证据
Capture browser screenshots and GIF recordings using Playwright headless Chromium. Upload to cloud storage for embedding in PRs and reports.
使用Playwright无头Chromium捕获浏览器截图和GIF录制。上传至云存储以嵌入PR和报告中。
When to Use
使用场景
- After implementing a bug fix (before/after comparison)
- After implementing a feature (demo key flows)
- During PR review to verify visual changes
- Interaction/timing bugs (double-click, loading states, race conditions) → use GIF recording
- 修复Bug后(前后对比)
- 实现功能后(演示关键流程)
- PR评审期间验证视觉变化
- 交互/时序Bug(双击、加载状态、竞态条件)→ 使用GIF录制
When NOT to Use
不适用场景
- Backend-only changes, config/infra, test-only, CLI tools, APIs without UI
- Skip if Playwright install fails — visual evidence is best-effort, never blocks the pipeline
- 仅后端变更、配置/基础设施、仅测试代码、CLI工具、无UI的API
- 若Playwright安装失败则跳过——可视化证据为尽力而为,绝不阻塞流水线
Prerequisites
前置条件
Install Playwright and Chromium in the remote environment:
bash
cd /tmp && npm install playwright 2>&1 | tail -3 && \
npx playwright install-deps chromium 2>&1 | tail -3 && \
npx playwright install chromium 2>&1 | tail -3 && \
echo PW_READYFor GIF recording, also install ffmpeg:
bash
sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg 2>&1 | tail -1 && echo FFMPEG_READY在远程环境中安装Playwright和Chromium:
bash
cd /tmp && npm install playwright 2>&1 | tail -3 && \
npx playwright install-deps chromium 2>&1 | tail -3 && \
npx playwright install chromium 2>&1 | tail -3 && \
echo PW_READY若要录制GIF,还需安装ffmpeg:
bash
sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg 2>&1 | tail -1 && echo FFMPEG_READYWorkflow
工作流程
1. Screenshot Capture
1. 截图捕获
Create :
/tmp/screenshot.mjsjavascript
import { chromium } from '/tmp/node_modules/playwright/index.mjs';
const [url, output] = process.argv.slice(2);
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
console.log(`Navigating to ${url}...`);
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
const title = await page.title();
console.log(`Page title: ${title}`);
await page.screenshot({ path: output, fullPage: true });
console.log(`Screenshot saved: ${output}`);
await browser.close();Usage:
bash
node /tmp/screenshot.mjs http://localhost:3000 /tmp/evidence-home.png
node /tmp/screenshot.mjs http://localhost:3000/admin /tmp/evidence-admin.png创建:
/tmp/screenshot.mjsjavascript
import { chromium } from '/tmp/node_modules/playwright/index.mjs';
const [url, output] = process.argv.slice(2);
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
console.log(`Navigating to ${url}...`);
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
const title = await page.title();
console.log(`Page title: ${title}`);
await page.screenshot({ path: output, fullPage: true });
console.log(`Screenshot saved: ${output}`);
await browser.close();用法:
bash
node /tmp/screenshot.mjs http://localhost:3000 /tmp/evidence-home.png
node /tmp/screenshot.mjs http://localhost:3000/admin /tmp/evidence-admin.png2. GIF Recording (Interaction Evidence)
2. GIF录制(交互证据)
Create :
/tmp/record.mjsjavascript
import { chromium } from '/tmp/node_modules/playwright/index.mjs';
const [url, output, ...actions] = process.argv.slice(2);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
recordVideo: { dir: '/tmp/videos', size: { width: 1280, height: 720 } }
});
const page = await context.newPage();
console.log(`Navigating to ${url}...`);
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
// Execute actions passed as JSON array
if (actions[0]) {
const steps = JSON.parse(actions[0]);
for (const step of steps) {
if (step.action === 'click') {
console.log(`Clicking ${step.selector}...`);
await page.click(step.selector);
} else if (step.action === 'dblclick') {
console.log(`Double-clicking ${step.selector}...`);
await page.click(step.selector);
await page.waitForTimeout(200);
await page.click(step.selector);
} else if (step.action === 'wait') {
console.log(`Waiting ${step.ms}ms...`);
await page.waitForTimeout(step.ms);
} else if (step.action === 'fill') {
console.log(`Filling ${step.selector}...`);
await page.fill(step.selector, step.value);
} else if (step.action === 'screenshot') {
console.log(`Snapshot: ${step.label}`);
await page.screenshot({ path: `/tmp/evidence-${step.label}.png` });
}
}
}
await page.waitForTimeout(500);
await page.close();
await context.close();
await browser.close();
// Convert to GIF
const { execSync } = await import('child_process');
const webm = execSync('ls -t /tmp/videos/*.webm | head -1').toString().trim();
console.log(`Video: ${webm}`);
execSync(`ffmpeg -y -i "${webm}" -vf "fps=10,scale=1280:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 "${output}" 2>&1 | tail -3`);
console.log(`GIF saved: ${output}`);Usage:
bash
node /tmp/record.mjs http://localhost:3000/form /tmp/evidence-before.gif \
'[{"action":"click","selector":".submit-btn"},{"action":"wait","ms":2000}]'创建:
/tmp/record.mjsjavascript
import { chromium } from '/tmp/node_modules/playwright/index.mjs';
const [url, output, ...actions] = process.argv.slice(2);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
recordVideo: { dir: '/tmp/videos', size: { width: 1280, height: 720 } }
});
const page = await context.newPage();
console.log(`Navigating to ${url}...`);
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
// Execute actions passed as JSON array
if (actions[0]) {
const steps = JSON.parse(actions[0]);
for (const step of steps) {
if (step.action === 'click') {
console.log(`Clicking ${step.selector}...`);
await page.click(step.selector);
} else if (step.action === 'dblclick') {
console.log(`Double-clicking ${step.selector}...`);
await page.click(step.selector);
await page.waitForTimeout(200);
await page.click(step.selector);
} else if (step.action === 'wait') {
console.log(`Waiting ${step.ms}ms...`);
await page.waitForTimeout(step.ms);
} else if (step.action === 'fill') {
console.log(`Filling ${step.selector}...`);
await page.fill(step.selector, step.value);
} else if (step.action === 'screenshot') {
console.log(`Snapshot: ${step.label}`);
await page.screenshot({ path: `/tmp/evidence-${step.label}.png` });
}
}
}
await page.waitForTimeout(500);
await page.close();
await context.close();
await browser.close();
// Convert to GIF
const { execSync } = await import('child_process');
const webm = execSync('ls -t /tmp/videos/*.webm | head -1').toString().trim();
console.log(`Video: ${webm}`);
execSync(`ffmpeg -y -i "${webm}" -vf "fps=10,scale=1280:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 "${output}" 2>&1 | tail -3`);
console.log(`GIF saved: ${output}`);用法:
bash
node /tmp/record.mjs http://localhost:3000/form /tmp/evidence-before.gif \
'[{"action":"click","selector":".submit-btn"},{"action":"wait","ms":2000}]'3. Upload to Cloud Storage
3. 上传至云存储
Use the evidence-upload skill to upload screenshots and GIFs to the pylot assets backend. and are already in every operator/worker environment — no static AWS keys needed.
$PYLOT_GATEWAY_URL$PYLOT_DISPATCH_TOKENRun for each captured file and capture the returned for embedding in Step 4.
/evidence-uploadpublic_url使用evidence-upload技能将截图和GIF上传至pylot资产后端。和已配置在所有操作员/工作环境中——无需静态AWS密钥。
$PYLOT_GATEWAY_URL$PYLOT_DISPATCH_TOKEN为每个捕获的文件运行,并捕获返回的以便在步骤4中嵌入。
/evidence-uploadpublic_url4. PR Embedding
4. 嵌入PR
Before/After (bug fixes):
markdown
undefined前后对比(Bug修复):
markdown
undefinedVisual Evidence
可视化证据
<details>
<summary>Before / After</summary>
Before (bug present on default branch):

After (fix applied):

</details>
```
Interaction demo (GIF):
markdown
undefined<details>
<summary>修复前 / 修复后</summary>
修复前(默认分支存在Bug):

修复后(已应用修复):

</details>
```
交互演示(GIF):
markdown
undefinedVisual Evidence
可视化证据
<details>
<summary>Interaction demo</summary>

Caption: what this demonstrates
</details>
```<details>
<summary>交互演示</summary>

说明:该演示展示的内容
</details>
```Decision Table: Screenshot vs GIF
决策表:截图 vs GIF
| Scenario | Format |
|---|---|
| Visual layout change | Screenshot (PNG) |
| New page / feature | Screenshot (PNG) |
| Button disable on click | GIF |
| Loading spinner / skeleton | GIF |
| Double-click prevention | GIF |
| Form validation feedback | GIF |
| Transition / animation | GIF |
| Error → retry flow | GIF |
| 场景 | 格式 |
|---|---|
| 视觉布局变更 | 截图(PNG) |
| 新页面/功能 | 截图(PNG) |
| 按钮点击后禁用 | GIF |
| 加载动画/骨架屏 | GIF |
| 双击防重复 | GIF |
| 表单验证反馈 | GIF |
| 过渡/动画效果 | GIF |
| 错误→重试流程 | GIF |
Error Handling
错误处理
Visual evidence is best-effort. Never block the pipeline.
- Playwright install fails → log, skip evidence, proceed
- Dev server won't start → log, skip evidence, proceed
- Screenshot fails (404, timeout) → log, skip evidence, proceed
- Upload fails (no creds, permission denied) → log, skip evidence, proceed
- Hard timeout: 120 seconds for the entire evidence phase. If exceeded, kill and proceed.
可视化证据为尽力而为。绝不阻塞流水线。
- Playwright安装失败→记录日志,跳过证据采集,继续执行
- 开发服务器无法启动→记录日志,跳过证据采集,继续执行
- 截图失败(404、超时)→记录日志,跳过证据采集,继续执行
- 上传失败(无凭据、权限不足)→记录日志,跳过证据采集,继续执行
- 硬超时: 整个证据采集阶段超时时间为120秒。若超时,终止进程并继续执行。
Critical Rules
核心规则
- Never block the pipeline — evidence is best-effort
- 120 second hard timeout — kill and move on
- Always verify upload before embedding URLs in PRs
- Screenshots in are ephemeral — they disappear when the environment stops
/tmp/
- 绝不阻塞流水线——证据采集为尽力而为
- 120秒硬超时——终止进程并继续执行
- 嵌入PR前务必验证上传结果
- 中的截图为临时文件——环境停止后会消失
/tmp/