- 新增 ADR 架构决策记录 (Design DNA 集成与深化) - 新增 CMS 系统设计文档 - 新增实施计划文档 (Phase1-3) - 新增 Bain 品牌升级设计规格 - 新增 E2E 分层测试套件 (P1-P4) - 新增视觉回归测试配置 - 新增光效分析、视觉验证等辅助脚本 - 更新验收测试报告
196 lines
6.7 KiB
TypeScript
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();
|
|
});
|
|
});
|