用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/tomevault-io/skills-registry --skill visual-regression-testing命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
| Use when this capability is needed.
> Use when this capability is needed.
Review architecture and API design for the vfs-s3 project. Use when the user mentions @architect, asks to review an issue's design, discuss module boundaries, API shape, or architectural decisions for vfs-s3. Also trigger when the user wants to create an ADR (Architecture Decision Record) or evaluate a technical approach for the project. Intended for dispatch from Codex automation or Claude routines; GitHub trigger phrase: @vfs-s3-bot please prepare design doc Use when this capability is needed.
基于 SOC 职业分类
正在显示 SKILL.md
| name | visual-regression-testing |
| description | > Use when this capability is needed. |
Best practices for visual regression testing with Playwright to catch unintended UI changes.
import { test, expect } from '@playwright/test';
test('homepage visual regression', async ({ page }) => {
await page.goto('/');
// Wait for page to be fully loaded
await page.waitForLoadState('networkidle');
// Take full page screenshot and compare
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
});
});
test('product card visual regression', async ({ page }) => {
await page.goto('/products');
// Screenshot specific component
const productCard = page.getByTestId('product-card').first();
await expect(productCard).toHaveScreenshot('product-card.png');
});
test('navigation menu visual regression', async ({ page }) => {
await page.goto('/');
const navbar = page.getByRole('navigation');
await expect(navbar).toHaveScreenshot('navigation.png');
});
const viewports = [
{ width: 1920, height: 1080, name: 'desktop' },
{ width: 1024, height: 768, name: 'tablet' },
{ width: 375, height: 667, name: 'mobile' },
];
for (const viewport of viewports) {
test(`homepage at ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot(`homepage-${viewport.name}.png`, {
fullPage: true,
});
});
}
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Snapshot settings
expect: {
toHaveScreenshot: {
// Maximum allowed pixel difference
maxDiffPixels: 100,
// Maximum allowed ratio of different pixels (0-1)
maxDiffPixelRatio: 0.01,
// Threshold for comparing colors (0-1)
threshold: 0.2,
// Animation handling
animations: 'disabled',
// Caret blinking
caret: 'hide',
// Scale
scale: 'css',
},
},
// Update snapshots mode
updateSnapshots: process.env.UPDATE_SNAPSHOTS ? 'all' : 'missing',
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// Consistent screenshots across runs
launchOptions: {
args: ['--font-render-hinting=none'],
},
},
},
{
name: 'firefox',
: { ...devices[] },
},
{
: ,
: { ...devices[] },
},
],
});
test('configurable screenshot options', async ({ page }) => {
await page.goto('/products');
await expect(page).toHaveScreenshot('products-page.png', {
// Full page vs viewport
fullPage: true,
// Pixel difference threshold
maxDiffPixels: 50,
// Percentage difference threshold
maxDiffPixelRatio: 0.005,
// Color comparison threshold
threshold: 0.1,
// Disable animations
animations: 'disabled',
// Hide caret in inputs
caret: 'hide',
// Mask dynamic elements
mask: [
page.getByTestId('timestamp'),
page.getByTestId('user-avatar'),
],
// Mask color
maskColor: '#FF00FF',
// Omit background
omitBackground: false,
// Scale factor
scale: 'css',
// Timeout
timeout: 10000,
});
});
test.describe('Button Component Visual Tests', () => {
test('primary button states', async ({ page }) => {
await page.goto('/storybook/buttons');
const primaryButton = page.getByRole('button', { name: 'Primary' });
// Default state
await expect(primaryButton).toHaveScreenshot('button-primary-default.png');
// Hover state
await primaryButton.hover();
await expect(primaryButton).toHaveScreenshot('button-primary-hover.png');
// Focus state
await primaryButton.focus();
await expect(primaryButton).toHaveScreenshot('button-primary-focus.png');
// Disabled state
const disabledButton = page.getByRole('button', { name: 'Disabled' });
await expect(disabledButton).toHaveScreenshot('button-primary-disabled.png');
});
});
test.describe('Form Visual States', () => {
test('input field states', async ({ page }) => {
await page.goto('/login');
const emailInput = page.getByLabel('Email');
// Empty state
await expect(emailInput).toHaveScreenshot('input-empty.png');
// Filled state
await emailInput.fill('user@example.com');
await expect(emailInput).toHaveScreenshot('input-filled.png');
// Error state
await emailInput.fill('invalid-email');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(emailInput).toHaveScreenshot('input-error.png');
});
});
test.describe('Dark Mode Visual Tests', () => {
test('homepage in dark mode', async ({ page }) => {
// Enable dark mode via media query emulation
await page.emulateMedia({ colorScheme: 'dark' });
await page.goto('/');
await expect(page).toHaveScreenshot('homepage-dark.png', {
fullPage: true,
});
});
test('homepage in light mode', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'light' });
await page.goto('/');
await expect(page).toHaveScreenshot('homepage-light.png', {
fullPage: true,
});
});
});
test.describe('Responsive Design', () => {
const breakpoints = {
mobile: { width: 375, height: 667 },
tablet: { width: 768, height: 1024 },
desktop: { width: 1440, height: 900 },
};
for (const [name, size] of Object.entries(breakpoints)) {
test(`navigation at ${name} breakpoint`, async ({ page }) => {
await page.setViewportSize(size);
await page.goto('/');
const nav = page.getByRole('navigation');
await expect(nav).toHaveScreenshot(`nav-${name}.png`);
});
}
});
test('page with dynamic content', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
// Mask timestamps
page.locator('[data-testid="timestamp"]'),
// Mask user-specific data
page.locator('[data-testid="user-name"]'),
// Mask random images
page.locator('img[src*="avatar"]'),
// Mask charts with live data
page.locator('[data-testid="live-chart"]'),
],
});
});
test('page with lazy loaded content', async ({ page }) => {
await page.goto('/products');
// Wait for images to load
await page.waitForFunction(() => {
const images = document.querySelectorAll('img');
return Array.from(images).every(img => img.complete);
});
// Wait for animations to complete
await page.waitForTimeout(500); // Allow CSS animations to settle
await expect(page).toHaveScreenshot('products-loaded.png', {
fullPage: true,
});
});
test('page with animations', async ({ page }) => {
await page.goto('/');
// Disable all animations and transitions
await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`,
});
await expect(page).toHaveScreenshot('homepage-no-animations.png');
});
test('page with dates', async ({ page }) => {
// Mock the date to ensure consistent screenshots
await page.addInitScript(() => {
const fixedDate = new Date('2026-01-15T10:00:00Z');
Date.now = () => fixedDate.getTime();
});
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard-fixed-date.png');
});
# .github/workflows/visual-tests.yml
name: Visual Regression Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
visual-tests:
runs-on: ubuntu-latest
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: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run visual tests
run: npx playwright test
# Update all snapshots
npx playwright test --update-snapshots
# Update snapshots for specific test
npx playwright test homepage.spec.ts --update-snapshots
# Update snapshots in CI (via env variable)
UPDATE_SNAPSHOTS=1 npx playwright test
// playwright.config.ts
export default defineConfig({
snapshotDir: './snapshots',
snapshotPathTemplate: '{snapshotDir}/{testFilePath}/{arg}{ext}',
});
snapshots/
├── auth/
│ ├── login-page.png
│ ├── register-page.png
│ └── forgot-password.png
├── products/
│ ├── catalog-desktop.png
│ ├── catalog-mobile.png
│ └── product-details.png
└── checkout/
├── cart.png
├── shipping.png
└── payment.png
// Good - Descriptive names
await expect(page).toHaveScreenshot('checkout-cart-with-items.png');
await expect(page).toHaveScreenshot('checkout-cart-empty-state.png');
await expect(page).toHaveScreenshot('product-card-out-of-stock.png');
// Bad - Generic names
await expect(page).toHaveScreenshot('screenshot1.png');
await expect(page).toHaveScreenshot('page.png');
test.describe('Product Card Visual States', () => {
test('in stock state', async ({ page }) => {
await page.goto('/products/in-stock-item');
await expect(page.getByTestId('product-card')).toHaveScreenshot('product-in-stock.png');
});
test('out of stock state', async ({ page }) => {
await page.goto('/products/out-of-stock-item');
await expect(page.getByTestId('product-card')).toHaveScreenshot('product-out-of-stock.png');
});
test('on sale state', async ({ page }) => {
await page.goto('/products/sale-item');
await expect(page.getByTestId('product-card')).toHaveScreenshot('product-on-sale.png');
});
});
# .gitignore
# Ignore test results, but keep snapshots
test-results/
playwright-report/
# Keep snapshots in version control
# !snapshots/
// Add comments explaining what should be in the screenshot
test('checkout summary', async ({ page }) => {
await page.goto('/checkout');
// Expected: Order summary with item list, subtotal, tax, and total
// Should include: Shipping address form, payment method selection
await expect(page).toHaveScreenshot('checkout-summary.png', {
fullPage: true,
});
});
// Full page
await expect(page).toHaveScreenshot('name.png', { fullPage: true });
// Viewport only
await expect(page).toHaveScreenshot('name.png');
// Specific element
await expect(locator).toHaveScreenshot('name.png');
// With options
await expect(page).toHaveScreenshot('name.png', {
maxDiffPixels: 100,
threshold: 0.2,
animations: 'disabled',
mask: [locator1, locator2],
});
# Update all snapshots
npx playwright test --update-snapshots
# Update specific file
npx playwright test file.spec.ts --update-snapshots
# Interactive mode
npx playwright test --ui
Converted and distributed by TomeVault — claim your Tome and manage your conversions.