Files
novalon-website/e2e/p1-brand-visual-audit.spec.ts
T
zhangxiang 6bb7c557ee test(qa): e2e 装配修正 + 集成/门禁测试与工具链重构
- e2e:修死选择器与蜜罐误命中、补断言、GA4/security-headers 用例诚实标注
  skip 边界;新增 assert-helpers/fixtures/hydrated/primary-nav/touch-targets
  等复用桩。
- 集成层:config/test/jest.integration.config.js + tests-integration/ 真库
  一次性 SQLite 用例,teardown 守卫开发库指纹。
- 门禁工具链:jest.setup 归位到根、scripts/accessibility 全站 axe 节点计数、
  check-brand-text-token/check-motion-constraints 机械守卫、stryker/lighthouse
  配置收敛。
2026-09-28 10:48:08 +08:00

676 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect, Page } from './fixtures';
/**
* P1: 品牌视觉审计测试套件
*
* 目标:确保网站所有页面不存在 "novalon" 字样(用户可见文本),
* 所有品牌名称统一为中文 "睿新致远"
*
* 测试范围:
* - 全站文本内容扫描(大小写不敏感)
* - Logo/SVG 图形元素检查
* - 关键品牌位置验证(Header, Footer, 标题等)
* - 交互状态下的品牌一致性
* - 响应式布局下的品牌显示
*/
// 测试页面路由配置
const TEST_PAGES = [
{ path: '/', name: '首页' },
{ path: '/about', name: '关于我们' },
{ path: '/contact', name: '联系我们' },
{ path: '/products', name: '产品中心' },
{ path: '/services', name: '服务介绍' },
{ path: '/solutions', name: '解决方案' },
{ path: '/news', name: '新闻动态' },
{ path: '/team', name: '团队介绍' },
{ path: '/privacy', name: '隐私政策' },
{ path: '/terms', name: '服务条款' },
];
// 品牌名称配置
const BRAND_NAME = '睿新致远';
const FORBIDDEN_TEXT = /novalon/i; // 大小写不敏感
// 技术性上下文的正则表达式模式(这些是允许的引用)
const TECHNICAL_PATTERNS = [
/@.*\.(cn|com|net|org)/i, // 邮箱地址: contact@novalon.cn
/https?:\/\/[^\s]*novalon/i, // 完整URL
/\bnovalon\.(cn|com|net|org)\b/i, // 域名: novalon.cn
/www\.novalon\./i, // www域名
/[\w.-]*novalon[\w.-]*/i, // 其他子域名格式
];
/**
* 检查文本是否为技术性上下文(允许的引用)
* @param text 要检查的文本
* @returns 如果是技术性上下文返回 true,否则返回 false
*/
function isTechnicalContext(text: string): boolean {
return TECHNICAL_PATTERNS.some(pattern => pattern.test(text));
}
/**
* 从文本中过滤掉技术性引用后检查是否还包含禁止的文本
* @param text 原始文本
* @returns 过滤后的结果:是否包含非法的可见文本
*/
function hasVisibleForbiddenText(text: string): boolean {
// 先尝试整体匹配
if (!FORBIDDEN_TEXT.test(text)) return false;
// 如果匹配到了,检查是否全部都是技术性上下文
const lines = text.split('\n');
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine) continue;
// 如果这行包含禁止的文本但不是技术性上下文
if (FORBIDDEN_TEXT.test(trimmedLine) && !isTechnicalContext(trimmedLine)) {
return true;
}
}
return false;
}
// 设置更长的超时时间
test.setTimeout(90000);
/**
* 安全导航到指定URL,带重试机制
* @param page Playwright Page对象
* @param path 要访问的路径
* @param maxRetries 最大重试次数
*/
async function safeGoto(page: Page, path: string, maxRetries = 2): Promise<void> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
await page.goto(path, {
waitUntil: 'domcontentloaded',
timeout: 60000, // 增加到60秒
});
return; // 成功则返回
} catch (error) {
if (attempt === maxRetries) {
throw error; // 最后一次重试仍然失败,抛出错误
}
console.log(`Navigation to ${path} failed, retrying... (${attempt + 1}/${maxRetries})`);
await page.waitForTimeout(2000); // 等待2秒后重试
}
}
}
test.describe('P1: 品牌视觉审计 - 全站文本扫描', { tag: '@regression' }, () => {
TEST_PAGES.forEach(({ path, name }) => {
test(`${name} (${path}) - 不应包含可见的 "novalon" 字样`, async ({ page }) => {
// 使用安全导航函数(带重试机制)
await safeGoto(page, path);
// 等待客户端渲染完成
await page.waitForTimeout(2000);
// N-27 修复:原实现先跑一遍 body 正则,只有命中才进入深扫分支,
// 未命中分支的 `expect(true).toBeTruthy()` 是零价值占位 ⇒ 断言路径被守卫。
// 深扫本身就具备「无匹配 ⇒ 计数为 0 ⇒ 通过」的完整语义,改为无条件执行并断言。
// 获取所有包含 novalon 的元素进行详细检查
const elementsWithForbiddenText = await page.evaluate(() => {
const results: Array<{
tag: string;
text: string;
isVisible: boolean;
ariaHidden: boolean;
isTechnicalContext: boolean; // 新增:标记是否为技术性上下文
}> = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null
);
let node: Text | null;
while ((node = walker.nextNode() as Text | null)) {
if (node.textContent && /novalon/i.test(node.textContent)) {
const parent = node.parentElement;
if (parent) {
const style = window.getComputedStyle(parent);
const text = node.textContent.trim();
// 检查是否为技术性上下文(邮箱、URL等)
const isTechnicalContext =
/@.*\.(cn|com|net|org)/i.test(text) || // 邮箱地址
/https?:\/\/[^\s]*novalon/i.test(text) || // URL
/\.(cn|com|net|org)/i.test(text); // 域名
results.push({
tag: parent.tagName.toLowerCase(),
text: text,
isVisible: style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0',
ariaHidden: parent.getAttribute('aria-hidden') === 'true',
isTechnicalContext,
});
}
}
}
return results;
});
// 过滤掉不可见、aria-hidden 或技术性上下文的元素
const visibleNonTechnicalMatches = elementsWithForbiddenText.filter(
el => el.isVisible && !el.ariaHidden && !el.isTechnicalContext
);
// 首页 Hero 品牌标识中的英文品牌名 NOVALON 是有意展示的内容,予以豁免
const reportMatches = path === '/'
? visibleNonTechnicalMatches.filter(el => el.text.trim().toUpperCase() !== 'NOVALON')
: visibleNonTechnicalMatches;
// 断言:不应有可见的非技术性禁止文本
expect(
reportMatches.length,
`发现 ${reportMatches.length} 处可见的非技术性 "novalon" 文本:\n` +
reportMatches.map(el => ` [${el.tag}] "${el.text}"`).join('\n')
).toBe(0);
});
});
test('全站 DOM 元素扫描 - 不应包含可见的 "novalon"', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
// 使用 JavaScript 进行深度 DOM 扫描
const scanResults = await page.evaluate(() => {
const findings: Array<{
selector: string;
text: string;
type: 'text' | 'attribute' | 'meta';
isTechnicalContext: boolean; // 标记是否为技术性上下文
}> = [];
// 技术性上下文的正则表达式(与外部定义保持一致)
const technicalPatterns = [
/@.*\.(cn|com|net|org)/i,
/https?:\/\/[^\s]*novalon/i,
/\bnovalon\.(cn|com|net|org)\b/i,
];
function isTechnical(text: string): boolean {
return technicalPatterns.some(p => p.test(text));
}
// 扫描所有文本节点
const allElements = document.querySelectorAll('*');
allElements.forEach(element => {
// 跳过 script 和 style 标签
if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(element.tagName)) return;
// 检查文本内容
if (element.children.length === 0 && element.textContent) {
const text = element.textContent.trim();
if (text && /novalon/i.test(text)) {
const style = window.getComputedStyle(element);
if (style.display !== 'none' &&
style.visibility !== 'hidden' &&
element.getAttribute('aria-hidden') !== 'true') {
findings.push({
selector: `${element.tagName.toLowerCase()}${element.id ? '#' + element.id : ''}`,
text: text.substring(0, 50),
type: 'text',
isTechnicalContext: isTechnical(text),
});
}
}
}
// 检查 title 和 alt 属性(这些属性通常对用户可见)
['title', 'alt', 'aria-label', 'placeholder'].forEach(attr => {
const value = element.getAttribute(attr);
if (value && /novalon/i.test(value)) {
findings.push({
selector: `[${attr}="${value}"]`,
text: value,
type: 'attribute',
isTechnicalContext: isTechnical(value),
});
}
});
});
// 检查 meta 标签
document.querySelectorAll('meta').forEach(meta => {
const content = meta.getAttribute('content');
const name = meta.getAttribute('name') || meta.getAttribute('property');
if (content && /novalon/i.test(content)) {
findings.push({
selector: `meta[${name || 'http-equiv'}]`,
text: content,
type: 'meta',
isTechnicalContext: isTechnical(content),
});
}
});
return findings;
});
// 过滤掉技术性上下文的发现
const nonTechnicalFindings = scanResults.filter(f => !f.isTechnicalContext);
// 首页 Hero 品牌标识中的英文品牌名 NOVALON 是有意展示的内容,予以豁免
const allowedFindings = nonTechnicalFindings.filter(f => {
if (f.type !== 'text') return false;
const text = f.text.trim().toUpperCase();
return text === 'NOVALON';
});
const reportFindings = nonTechnicalFindings.filter(f => {
if (f.type !== 'text') return true;
const text = f.text.trim().toUpperCase();
return text !== 'NOVALON';
});
// 断言:不应有非技术性且非豁免的发现
expect(
reportFindings.length,
`DOM 扫描发现 ${reportFindings.length} 处非法 "novalon" 引用(已豁免 ${allowedFindings.length} 处首页 Hero 品牌标识):\n` +
reportFindings.map(f => ` [${f.type.toUpperCase()}] ${f.selector}: "${f.text}"`).join('\n')
).toBe(0);
});
});
test.describe('P1: 品牌视觉审计 - 关键位置验证', { tag: '@regression' }, () => {
test('Header Logo 应显示 "睿新致远" 品牌', async ({ page }) => {
// 使用安全导航
await safeGoto(page, '/');
await page.waitForTimeout(1000);
// 真实契约(header.tsx:96-118 渲染 `a[aria-label="返回首页"]` 内的明暗双版 Logo 图形):
// Header 必须有一个可见 Logo 图、alt 非空且不含非法文本、src 指向品牌字标资产。
// 旧实现用 8 个猜测式选择器 + `catch {}` 吞异常 + 「页面里只要有 img/svg 就算通过」的兜底,
// 在现行 DOM 下等价于零断言(见 docs/lessons-learned.md §5.21)。
const logo = page.locator('header a[aria-label="返回首页"] img:visible').first();
await expect(logo, 'Header 未渲染可见的品牌 Logo 图形').toBeVisible({ timeout: 10000 });
const alt = await logo.getAttribute('alt') || '';
const src = await logo.getAttribute('src') || '';
expect(alt.length, 'Header Logo 的 alt 为空').toBeGreaterThan(2);
expect(hasVisibleForbiddenText(alt), `Header Logo alt 包含非法文本: ${alt}`).toBe(false);
expect(src, `Header Logo src 应指向品牌字标资产: ${src}`).toContain('logo');
});
test('Footer 应包含 "睿新致远" 品牌信息', async ({ page }) => {
await safeGoto(page, '/');
await page.waitForTimeout(1000);
const footer = page.locator('[data-testid="footer"], footer').first();
await expect(footer).toBeVisible({ timeout: 10000 });
// 检查 Footer 文本中包含品牌名称
const footerText = await footer.textContent();
expect(footerText).toContain(BRAND_NAME);
// 确保 Footer 中不包含禁止的文本(排除技术性引用如域名、邮箱)
expect(
hasVisibleForbiddenText(footerText || ''),
`Footer 中发现非法的可见 "novalon" 文本(排除域名/邮箱等技术性引用)`
).toBe(false);
});
test('页面标题应包含公司全称', async ({ page }) => {
await safeGoto(page, '/');
const title = await page.title();
expect(title).toContain('四川睿新致远科技有限公司');
// 标题中不应包含 novalon
expect(title.toLowerCase()).not.toContain('novalon');
});
test('Meta 描述和关键词应使用中文品牌名', async ({ page }) => {
await safeGoto(page, '/');
// N-27 修复:原 `if (description)` 守卫 ⇒ meta description 消失时静默通过。
// 根 layout(src/app/layout.tsx:49)恒定声明 description ⇒ 该 meta 是硬契约。
const descriptionMeta = page.locator('meta[name="description"]');
await expect(descriptionMeta).toHaveCount(1);
const description = await descriptionMeta.getAttribute('content');
expect(description, 'meta description 应为非空').toBeTruthy();
expect(
hasVisibleForbiddenText(description ?? ''),
`Meta描述包含非法 "novalon" 文本: ${description}`
).toBe(false);
});
});
test.describe('P1: 品牌视觉审计 - SVG Logo 详细检查', { tag: '@regression' }, () => {
test('Logo 图形资产内的文字受控', async ({ page }) => {
await safeGoto(page, '/');
await page.waitForTimeout(1500);
// 契约:品牌文本通道禁止的是「页面文案里的 novalon」;Logo 图形资产内的拉丁字标
// `NOVALON` 是已批准的品牌资产(本文件 :173 对首页 Hero 的既予豁免 + 8d3bd72
// 「unify calligraphy logo」保留了该 <text> 节点)。因此断言方式是「资产内出现的
// <text> 只能是这个已批准字标」,而不是旧实现的「SVG 必须有 textContent」——
// 纯 <path> 图标的 textContent 恒为空,旧断言必然抛错又被 catch 吞掉(§5.21 假绿根因)。
for (const asset of ['/logo.svg', '/logo-white.svg']) {
const res = await page.request.get(asset);
expect(res.status(), `${asset} 应可取回`).toBe(200);
const svg = await res.text();
const textNodes = [...svg.matchAll(/<text[^>]*>([^<]*)<\/text>/g)].map((m) => m[1].trim());
for (const value of textNodes) {
expect(value, `${asset} 内出现未批准的 <text> 内容`).toBe('NOVALON');
}
}
// DOM 侧:Header 文本层(读屏与视觉一致的可见文本)不得出现非法 novalon
const headerText = await page.locator('header').innerText();
expect(
hasVisibleForbiddenText(headerText),
'Header 文本层包含非法 "novalon" 文本'
).toBe(false);
});
test('Logo 图片 alt 属性正确设置', async ({ page }) => {
await safeGoto(page, '/');
await page.waitForTimeout(1000);
const logos = page.locator('header img');
const logoCount = await logos.count();
let withAlt = 0;
for (let i = 0; i < logoCount; i++) {
const logo = logos.nth(i);
if (!(await logo.isVisible())) continue;
const alt = await logo.getAttribute('alt');
// alt="" 是装饰图的合法写法,是否缺 alt 由 axe 的 image-alt 规则负责
if (!alt) continue;
withAlt += 1;
console.log(`Header image ${i}: alt="${alt}"`);
expect(
hasVisibleForbiddenText(alt),
`Logo alt属性包含非法文本: ${alt}`
).toBe(false);
expect(alt.length, `Header 第 ${i} 张图片 alt 过短: "${alt}"`).toBeGreaterThan(2);
}
// 旧实现把上述断言包在 try/catch + console.log 中,一次异常即静默通过(§5.21)
expect(withAlt, 'header 中没有带非空 alt 的可见图片可供检查').toBeGreaterThan(0);
});
});
test.describe('P1: 品牌视觉审计 - 交互状态一致性', { tag: '@regression' }, () => {
test('导航链接悬停状态不显示错误品牌', async ({ page }) => {
await safeGoto(page, '/');
await page.waitForTimeout(1000);
const navLinks = page.locator('nav a, header a');
const linkCount = await navLinks.count();
let hovered = 0;
for (let i = 0; i < Math.min(linkCount, 10); i++) {
const link = navLinks.nth(i);
// 检查链接是否可见(跳过隐藏的链接;isVisible() 对 nth() 精确定位的单元素
// 不会抛错,N-27 修复:去掉 `.catch(() => false)` 吞错壳)
const isVisible = await link.isVisible();
if (!isVisible) {
continue;
}
try {
// 悬停状态
await link.hover({ timeout: 2000 });
await page.waitForTimeout(100);
} catch (hoverError) {
// 悬停本身可能因遮挡失败:这是环境噪声,但必须留下计数(旧实现连失败都不统计)
console.log(`Hover failed for link ${i}:`, hoverError instanceof Error ? hoverError.message : hoverError);
continue;
}
hovered += 1;
// hover 后链接自身文本与 title 都不得引入非法品牌文本(每次都断言,不再依赖 title 是否存在)
expect(
hasVisibleForbiddenText((await link.innerText()).trim()),
`导航链接 hover 后文本包含非法 "novalon"`
).toBe(false);
// N-27 修复:title 为可选属性,原 `if (title)` 让断言只在属性存在时执行;
// 改为无条件检查(缺省按空串处理,结果同样必须为 false)
const title = await link.getAttribute('title');
expect(
hasVisibleForbiddenText(title ?? ''),
`导航链接标题包含非法 "novalon" 文本: ${title ?? ''}`
).toBe(false);
}
expect(hovered, '未成功 hover 任何导航链接,本用例等于零断言').toBeGreaterThan(0);
});
test('移动端菜单展开后品牌显示正常', async ({ page }) => {
// 设置移动端视口
await page.setViewportSize({ width: 375, height: 667 });
// N-27 修复:本例显式设到 375×667(< md 断点 768px),header.tsx:192 的
// mobile-menu-button 在此视口恒渲染 ⇒ 原 `isVisible().catch(() => false)` +
// test.skip 兜底属于可省壳,改为硬断言(按钮消失即红,而不是静默跳过)。
try {
await safeGoto(page, '/');
await page.waitForTimeout(1500);
// 真实 DOM:header.tsx:189 的 data-testid="mobile-menu-button",抽屉为 :222 的 mobile-navigation。
// 旧实现把 4 个选择器并成一个 locator,多匹配时 isVisible() 触发 strict-mode 抛错,
// 再被外层 catch 吞成 expect(true)(§5.21)。
const mobileMenuButton = page.locator('[data-testid="mobile-menu-button"]');
await expect(mobileMenuButton).toBeVisible({ timeout: 5000 });
await mobileMenuButton.click();
const mobileNav = page.locator('[data-testid="mobile-navigation"]');
await expect(mobileNav, '点击菜单按钮后移动端导航未展开').toBeVisible({ timeout: 5000 });
const navText = await mobileNav.textContent();
expect(
hasVisibleForbiddenText(navText || ''),
`移动端导航包含非法 "novalon" 文本`
).toBe(false);
} finally {
// 恢复默认视口
await page.setViewportSize({ width: 1280, height: 720 });
}
});
});
test.describe('P1: 品牌视觉审计 - 响应式布局验证', { tag: '@regression' }, () => {
const viewports = [
{ name: 'Desktop XL', width: 1920, height: 1080 },
{ name: 'Desktop', width: 1280, height: 800 },
{ name: 'Laptop', width: 1024, height: 768 },
{ name: 'Tablet', width: 768, height: 1024 },
{ name: 'Mobile L', width: 428, height: 926 },
{ name: 'Mobile M', width: 375, height: 667 },
{ name: 'Mobile S', width: 320, height: 568 },
];
viewports.forEach(({ name, width, height }) => {
test(`${name} (${width}x${height}) - 品牌显示一致`, async ({ page }) => {
await page.setViewportSize({ width, height });
// 对 Mobile S (320x568) 使用特殊处理(极小屏幕可能加载较慢)
const isMobileS = name === 'Mobile S';
try {
if (isMobileS) {
// Mobile S 使用更长的超时和重试
await safeGoto(page, '/', 3); // 增加重试次数到3次
} else {
await safeGoto(page, '/');
}
} catch (error) {
// 导航失败是真缺陷,不再按视口特例伪造通过(safeGoto 自带重试)
console.log(`Navigation failed for ${name}:`, error instanceof Error ? error.message : error);
throw error;
}
// 增加等待时间,确保响应式布局渲染完成
await page.waitForTimeout(isMobileS ? 3000 : 1500);
// Header 可见且包含品牌
const header = page.locator('header');
await expect(header).toBeVisible({ timeout: 10000 });
// Logo 在每个断点都必须可见:旧实现用 `if (count > 0)` + catch 把「查不到」与「不可见」
// 两条路都通向通过
const logo = page.locator('header img:visible, header svg:visible').first();
await expect(logo, `${name} 视口下 Header 没有可见 Logo`).toBeVisible({ timeout: 5000 });
// Footer 可见且包含品牌
const footer = page.locator('[data-testid="footer"], footer').first();
await expect(footer).toBeVisible({ timeout: 10000 });
const footerText = await footer.textContent();
// Footer 应该包含品牌名称
expect(
footerText?.includes(BRAND_NAME),
`Footer in ${name} viewport should contain brand name`
).toBeTruthy();
});
});
});
test.describe('P1: 品牌视觉审计 - 特殊场景覆盖', { tag: '@regression' }, () => {
test('404 错误页面品牌显示正常', async ({ page }) => {
// N-27 修复:原 `.then(() => true).catch(() => false)` 把导航失败吞成
// 「继续在空白页上断言」⇒ 页面根本没渲染也绿。导航失败现在直接变红,
// 并且必须先证明自定义 404 页面真的渲染了(not-found-content.tsx:14-16 的 h1「404」),
// 再检查品牌文本。
await safeGoto(page, '/nonexistent-page-12345');
// 等待自定义 404 页面渲染
await page.waitForTimeout(1000);
await expect(page.locator('h1').first()).toHaveText('404');
// 即使是 404 页面,也不应显示 novalon(排除技术性引用)
const bodyText = await page.evaluate(() => document.body?.innerText || '');
expect(bodyText.length, '404 页面正文不应为空').toBeGreaterThan(0);
expect(
hasVisibleForbiddenText(bodyText),
`404 页面包含非法 "novalon" 文本`
).toBe(false);
});
test('客户评价/推荐区域品牌引用正确', async ({ page }) => {
await safeGoto(page, '/');
await page.waitForTimeout(1000);
// 查找可能的评价区域
const testimonialSelectors = [
'[data-testid*="testimonial"]',
'[data-testid*="review"]',
'[data-testid*="social-proof"]',
'.testimonial',
'.review',
'blockquote',
];
let checked = 0;
for (const selector of testimonialSelectors) {
const element = page.locator(selector).first();
if (await element.count() === 0) continue;
// N-27 修复:去掉 `isVisible({timeout}).catch(() => false)` 吞错壳;
// 不可见选择器本就可跳过,真正的地板是下方无条件的 checked > 0 断言。
if (!(await element.isVisible())) continue;
const text = (await element.textContent()) || '';
if (!text.trim()) continue;
checked += 1;
expect(
hasVisibleForbiddenText(text),
`客户评价区域包含非法 "novalon" 文本`
).toBe(false);
}
// 首页确实渲染 blockquote(social-proof-section.tsx:102),因此零命中就是契约被破坏,
// 不能像旧实现那样走「catch + console.log」静默通过(§5.21)
expect(checked, '未找到任何可见的客户评价区域,本用例等于零断言').toBeGreaterThan(0);
});
test('打印视图品牌信息保留', async ({ page }) => {
await safeGoto(page, '/');
await page.waitForTimeout(1000);
// 模拟打印媒体查询
await page.evaluate(() => {
// 触发 @media print 样式(如果有的话)
window.dispatchEvent(new Event('beforeprint'));
});
// 在打印视图中,品牌信息应该仍然存在
const bodyText = await page.evaluate(() => document.body?.innerText || '');
expect(bodyText).toContain(BRAND_NAME);
// 确保不包含非法的可见文本(排除技术性引用)
expect(
hasVisibleForbiddenText(bodyText),
`打印视图中发现非法的可见 "novalon" 文本`
).toBe(false);
});
});
test.describe('P1: 品牌视觉审计 - 性能与截图基线', { tag: '@regression' }, () => {
test('品牌元素加载性能', async ({ page }) => {
const startTime = Date.now();
await safeGoto(page, '/');
// 等待 Logo 可见;10s 内不可见即失败(旧实现 catch 后继续跑,等于不做检查)
const logo = page.locator('header a[aria-label="返回首页"] img:visible').first();
await logo.waitFor({ state: 'visible', timeout: 10000 });
const loadTime = Date.now() - startTime;
// Logo 应在 30 秒内加载完成(考虑到网络延迟和首次加载)
expect(loadTime).toBeLessThan(30000);
});
test('生成品牌视觉截图用于人工审核', async ({ page }, testInfo) => {
// 设置更长的超时时间用于截图
test.setTimeout(90000);
await safeGoto(page, '/');
await page.waitForTimeout(1500);
// 截图是人工审核交付物,必须真正产出非空 PNG 并挂到报告附件上。
// 旧实现三处 `.catch(e => console.log(...))` + 结尾 expect(true),一张都没截到也算通过(§5.21)。
const header = page.locator('header');
await expect(header, 'Header 区域不可见').toBeVisible({ timeout: 10000 });
const headerPng = await header.screenshot({ fullPage: false });
expect(headerPng.byteLength, 'Header 截图为空').toBeGreaterThan(1000);
await testInfo.attach('p1-brand-audit-header', { body: headerPng, contentType: 'image/png' });
const footer = page.locator('[data-testid="footer"], footer').first();
await expect(footer, 'Footer 区域不可见').toBeVisible({ timeout: 10000 });
const footerPng = await footer.screenshot({ fullPage: false });
expect(footerPng.byteLength, 'Footer 截图为空').toBeGreaterThan(1000);
await testInfo.attach('p1-brand-audit-footer', { body: footerPng, contentType: 'image/png' });
const logo = page.locator('header a[aria-label="返回首页"] img:visible').first();
await expect(logo, 'Logo 图形不可见').toBeVisible({ timeout: 10000 });
const logoPng = await logo.screenshot();
expect(logoPng.byteLength, 'Logo 截图为空').toBeGreaterThan(200);
await testInfo.attach('p1-brand-audit-logo', { body: logoPng, contentType: 'image/png' });
});
});