Files
novalon-website/e2e/visual-regression.spec.ts
T
张翔 636bc4ecde docs(test): 添加设计文档、测试规范与 E2E 测试套件
- 新增 ADR 架构决策记录 (Design DNA 集成与深化)
- 新增 CMS 系统设计文档
- 新增实施计划文档 (Phase1-3)
- 新增 Bain 品牌升级设计规格
- 新增 E2E 分层测试套件 (P1-P4)
- 新增视觉回归测试配置
- 新增光效分析、视觉验证等辅助脚本
- 更新验收测试报告
2026-07-07 06:54:25 +08:00

196 lines
6.7 KiB
TypeScript

import { test, expect, Page } from '@playwright/test';
test.describe.configure({ mode: 'parallel' });
const VISUAL_TEST_PAGES = [
{ path: '/', name: 'home', label: '首页' },
{ path: '/about', name: 'about', label: '关于我们' },
{ path: '/contact', name: 'contact', label: '联系我们' },
{ path: '/products', name: 'products', label: '产品中心' },
{ path: '/products/erp', name: 'product-erp', label: '产品详情-ERP' },
{ path: '/solutions', name: 'solutions', label: '解决方案列表' },
{ path: '/solutions/manufacturing', name: 'solution-manufacturing', label: '解决方案详情-制造' },
{ path: '/services', name: 'services', label: '服务列表' },
{ path: '/services/software', name: 'service-software', label: '服务详情-软件开发' },
{ path: '/news', name: 'news', label: '新闻列表' },
{ path: '/news/company-founded', name: 'news-detail', label: '新闻详情' },
{ path: '/team', name: 'team', label: '团队介绍' },
];
async function waitForPageStable(page: Page) {
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.evaluate(() => {
document.querySelectorAll('img').forEach(img => {
if (!img.complete) {
img.loading = 'eager';
}
});
});
await page.waitForTimeout(1000);
await page.evaluate(() => {
document.fonts?.ready;
});
await page.waitForTimeout(500);
}
test.describe('L1: 全页面视觉回归测试', () => {
test.setTimeout(60000);
for (const { path, name, label } of VISUAL_TEST_PAGES) {
test(`${label} (${path}) - 全页截图对比`, async ({ page }) => {
await page.goto(path, { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
await expect(page).toHaveScreenshot(`${name}-fullpage.png`, {
fullPage: true,
animations: 'disabled',
caret: 'hide',
});
});
}
});
test.describe('L2: 组件视觉状态测试', () => {
test.setTimeout(45000);
test('按钮组件 - 默认/悬停/聚焦状态', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const primaryBtn = page.locator('button, a[role="button"]').first();
if (await primaryBtn.count() > 0 && await primaryBtn.isVisible()) {
await expect(primaryBtn).toHaveScreenshot('button-default.png');
await primaryBtn.hover();
await page.waitForTimeout(300);
await expect(primaryBtn).toHaveScreenshot('button-hover.png');
await primaryBtn.focus();
await page.waitForTimeout(200);
await expect(primaryBtn).toHaveScreenshot('button-focus.png');
}
});
test('导航菜单 - 桌面端视觉', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const header = page.locator('header').first();
if (await header.isVisible()) {
await expect(header).toHaveScreenshot('header-navigation.png');
}
});
test('页脚区域 - 视觉一致性', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const footer = page.locator('footer').first();
if (await footer.isVisible()) {
await expect(footer).toHaveScreenshot('footer-section.png');
}
});
test('卡片组件 - 视觉样式', async ({ page }) => {
await page.goto('/products', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const card = page.locator('[class*="card"]').first();
if (await card.count() > 0 && await card.isVisible()) {
await expect(card).toHaveScreenshot('product-card.png');
}
});
test('表单输入框 - 默认/聚焦状态', async ({ page }) => {
await page.goto('/contact', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const input = page.locator('input[type="text"], input[type="email"]').first();
if (await input.count() > 0 && await input.isVisible()) {
await expect(input).toHaveScreenshot('input-default.png');
await input.click();
await page.waitForTimeout(200);
await expect(input).toHaveScreenshot('input-focused.png');
}
});
});
test.describe('L2: 主题切换视觉测试', () => {
test.setTimeout(45000);
test('浅色主题 - 首页视觉', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const htmlEl = page.locator('html');
const currentTheme = await htmlEl.getAttribute('data-theme') || 'light';
if (currentTheme !== 'light') {
await htmlEl.evaluate(el => el.setAttribute('data-theme', 'light'));
await page.waitForTimeout(300);
}
await expect(page.locator('main').first()).toHaveScreenshot('theme-light-main.png');
});
test('深色主题 - 首页视觉', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const htmlEl = page.locator('html');
await htmlEl.evaluate(el => el.setAttribute('data-theme', 'dark'));
await page.waitForTimeout(500);
await expect(page.locator('main').first()).toHaveScreenshot('theme-dark-main.png');
});
});
test.describe('L3: 排版与色彩验证', () => {
test.setTimeout(30000);
test('首页 H1 标题字体样式', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const h1 = page.locator('h1').first();
if (await h1.count() > 0 && await h1.isVisible()) {
const styles = await h1.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
fontFamily: computed.fontFamily,
color: computed.color,
lineHeight: computed.lineHeight,
};
});
const size = parseFloat(styles.fontSize);
// Support clamp()/variable-based font sizes that may resolve differently
if (!isNaN(size)) {
expect(size).toBeGreaterThanOrEqual(16);
}
expect(styles.color || styles.fontFamily).toBeTruthy();
}
});
test('品牌主色调验证', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await waitForPageStable(page);
const primaryColor = await page.evaluate(() => {
const style = getComputedStyle(document.documentElement);
return {
brand: style.getPropertyValue('--color-brand')?.trim(),
ink: style.getPropertyValue('--color-ink')?.trim(),
bg: style.getPropertyValue('--color-bg-primary')?.trim(),
text: style.getPropertyValue('--color-text-primary')?.trim(),
};
});
expect(primaryColor.brand || primaryColor.ink || primaryColor.bg).toBeTruthy();
});
});