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
  配置收敛。
This commit is contained in:
2026-09-28 10:48:08 +08:00
parent a366bd1400
commit 6bb7c557ee
142 changed files with 6597 additions and 2653 deletions
+209 -372
View File
@@ -1,4 +1,4 @@
import { test, expect, Page } from '@playwright/test';
import { test, expect, Page } from './fixtures';
/**
* P1: 品牌视觉审计测试套件
@@ -111,79 +111,71 @@ test.describe('P1: 品牌视觉审计 - 全站文本扫描', { tag: '@regression
// 等待客户端渲染完成
await page.waitForTimeout(2000);
// 获取页面完整文本内容
const pageText = await page.evaluate(() => {
return document.body?.innerText || '';
});
// 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; // 新增:标记是否为技术性上下文
}> = [];
// 检查是否包含禁止的文本
if (FORBIDDEN_TEXT.test(pageText)) {
// 获取所有包含 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
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null
);
// 首页 Hero 品牌标识中的英文品牌名 NOVALON 是有意展示的内容,予以豁免
const reportMatches = path === '/'
? visibleNonTechnicalMatches.filter(el => el.text.trim().toUpperCase() !== 'NOVALON')
: visibleNonTechnicalMatches;
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();
// 断言:不应有可见的非技术性禁止文本
expect(
reportMatches.length,
`发现 ${reportMatches.length} 处可见的非技术性 "novalon" 文本:\n` +
reportMatches.map(el => ` [${el.tag}] "${el.text}"`).join('\n')
).toBe(0);
} else {
// 没有匹配到,测试通过
expect(true).toBeTruthy();
}
// 检查是否为技术性上下文(邮箱、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);
});
});
@@ -297,80 +289,18 @@ test.describe('P1: 品牌视觉审计 - 关键位置验证', { tag: '@regression
await safeGoto(page, '/');
await page.waitForTimeout(1000);
// 查找 Logo 元素(放宽选择器条件)
const logoSelectors = [
'header img',
'header svg',
'[data-testid="logo"] img',
'[data-testid="logo"] svg',
'a[href="/"] img', // Logo通常是首页链接
'nav img', // 导航栏中的图片
'.logo img', // class为logo的元素
'#logo', // id为logo的元素
];
// 真实契约(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 });
let logoFound = false;
for (const selector of logoSelectors) {
try {
const logo = page.locator(selector).first();
if (await logo.count() > 0) {
// 检查是否可见(有些可能是隐藏的)
const isVisible = await logo.isVisible().catch(() => false);
if (isVisible) {
logoFound = true;
// 如果是图片,检查 alt 属性或 src 属性
const tagName = await logo.evaluate(el => el.tagName.toLowerCase());
if (tagName === 'img') {
const alt = await logo.getAttribute('alt') || '';
const src = await logo.getAttribute('src') || '';
// 至少应该有 alt 或 src 属性包含品牌信息
const hasBrandInfo = alt.includes(BRAND_NAME) ||
src.toLowerCase().includes('logo') ||
alt.length > 0;
console.log(`Logo found: ${selector}, alt="${alt}", src="${src}"`);
expect(hasBrandInfo).toBeTruthy();
} else if (tagName === 'svg') {
// SVG Logo 应该包含中文品牌名
const svgText = await logo.textContent() || '';
console.log(`SVG Logo found: ${selector}, text="${svgText.substring(0, 50)}"`);
// SVG 可能包含"睿新致远"或其他品牌标识
expect(svgText.length).toBeGreaterThan(0);
}
break;
}
}
} catch {
// 忽略单个选择器的错误,继续尝试下一个
continue;
}
}
// 如果没找到可见的Logo,检查页面中是否有任何图片或SVG(可能结构不同)
if (!logoFound) {
console.log('Standard logo selectors failed, trying broader search...');
// 更广泛的搜索:查找页面中的所有图片和SVG
const allImages = page.locator('img');
const allSvgs = page.locator('svg');
const imageCount = await allImages.count();
const svgCount = await allSvgs.count();
console.log(`Page contains: ${imageCount} images, ${svgCount} SVGs`);
// 只要页面中有图片或SVG,就认为Logo可能存在(只是定位器需要调整)
if (imageCount > 0 || svgCount > 0) {
logoFound = true;
console.log('Found images/SVGs on page - logo likely exists but needs selector adjustment');
}
}
// 最终断言:至少应该找到一些视觉元素
expect(logoFound, '未找到 Header Logo 元素或页面中没有图片/SVG').toBeTruthy();
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 }) => {
@@ -404,119 +334,46 @@ test.describe('P1: 品牌视觉审计 - 关键位置验证', { tag: '@regression
test('Meta 描述和关键词应使用中文品牌名', async ({ page }) => {
await safeGoto(page, '/');
const description = await page.locator('meta[name="description"]').getAttribute('content');
if (description) {
expect(
hasVisibleForbiddenText(description),
`Meta描述包含非法 "novalon" 文本: ${description}`
).toBe(false);
}
// 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 SVG 文件不应包含 "NOVALON" 英文文本', async ({ page }) => {
test('Logo 图形资产内的文字受控', async ({ page }) => {
await safeGoto(page, '/');
await page.waitForTimeout(1500);
// 查找所有可能的 Logo 元素(更广泛的选择器)
const logoSelectors = [
'header svg',
'[data-testid="logo"] svg',
'img[src*="logo"]',
'a[href="/"] svg',
'nav svg',
'.logo svg',
'header img',
'[data-testid="logo"] img',
];
let logoChecked = false;
for (const selector of logoSelectors) {
try {
const logos = page.locator(selector);
const count = await logos.count();
if (count > 0) {
for (let i = 0; i < Math.min(count, 3); i++) { // 最多检查前3个
const logo = logos.nth(i);
const tagName = await logo.evaluate(el => el.tagName.toLowerCase());
// 检查是否可见
const isVisible = await logo.isVisible().catch(() => false);
if (!isVisible) continue;
if (tagName === 'svg') {
// 检查 SVG 内部文本内容
const svgTextContent = await logo.evaluate((element) => {
return element.textContent || '';
});
console.log(`SVG found (${selector}): "${svgTextContent.substring(0, 100)}"`);
// 检查是否有 NOVALON 英文文本(在 <text> 元素中)
if (/NOVALON/i.test(svgTextContent)) {
const hasEnglishText = await logo.evaluate((el) => {
const textElements = el.querySelectorAll('text');
for (const textEl of textElements) {
if (textEl.textContent && /NOVALON/i.test(textEl.textContent)) {
return true;
}
}
return false;
});
// 如果确实包含英文NOVALON,这是一个问题
expect(hasEnglishText).toBe(false);
}
// SVG 应该包含中文品牌名或至少有内容
expect(svgTextContent.length).toBeGreaterThan(0);
logoChecked = true;
} else if (tagName === 'img') {
// 如果是 img 标签,检查 src 和 alt
const alt = await logo.getAttribute('alt') || '';
const src = await logo.getAttribute('src') || '';
console.log(`Image Logo found (${selector}): alt="${alt}", src="${src}"`);
// Alt 属性不应包含英文 novalon
if (alt) {
expect(
hasVisibleForbiddenText(alt),
`Logo alt属性包含非法文本: ${alt}`
).toBe(false);
}
logoChecked = true;
}
}
if (logoChecked) break; // 找到一个有效的Logo就停止
}
} catch (e) {
console.log(`Error checking ${selector}:`, e instanceof Error ? e.message : e);
continue;
// 契约:品牌文本通道禁止的是「页面文案里的 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');
}
}
// 如果没有找到任何Logo元素,记录警告但不失败(可能是页面结构特殊)
if (!logoChecked) {
console.log('Warning: No visible SVG/img logo elements found with standard selectors');
// 检查页面中是否存在任何图片或SVG(作为后备验证)
const hasAnyVisualElements = await page.evaluate(() => {
const images = document.querySelectorAll('img');
const svgs = document.querySelectorAll('svg');
return images.length > 0 || svgs.length > 0;
});
console.log(`Page has visual elements: ${hasAnyVisualElements}`);
// 只要有视觉元素存在,就认为测试通过(Logo可能使用了非标准结构)
expect(hasAnyVisualElements).toBeTruthy();
}
// DOM 侧:Header 文本层(读屏与视觉一致的可见文本)不得出现非法 novalon
const headerText = await page.locator('header').innerText();
expect(
hasVisibleForbiddenText(headerText),
'Header 文本层包含非法 "novalon" 文本'
).toBe(false);
});
test('Logo 图片 alt 属性正确设置', async ({ page }) => {
@@ -526,29 +383,27 @@ test.describe('P1: 品牌视觉审计 - SVG Logo 详细检查', { tag: '@regress
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);
try {
// 检查是否可见
const isVisible = await logo.isVisible().catch(() => false);
if (!isVisible) continue;
if (!(await logo.isVisible())) continue;
const alt = await logo.getAttribute('alt');
const alt = await logo.getAttribute('alt');
// alt="" 是装饰图的合法写法,是否缺 alt 由 axe 的 image-alt 规则负责
if (!alt) continue;
withAlt += 1;
if (alt) {
console.log(`Header image ${i}: alt="${alt}"`);
expect(
hasVisibleForbiddenText(alt),
`Logo alt属性包含非法文本: ${alt}`
).toBe(false);
console.log(`Header image ${i}: alt="${alt}"`);
expect(
hasVisibleForbiddenText(alt),
`Logo alt属性包含非法文本: ${alt}`
).toBe(false);
// Alt 应该有合理的长度
expect(alt.length).toBeGreaterThan(2);
}
} catch (e) {
console.log(`Error checking logo ${i}:`, e instanceof Error ? e.message : e);
}
expect(alt.length, `Header 第 ${i} 张图片 alt 过短: "${alt}"`).toBeGreaterThan(2);
}
// 旧实现把上述断言包在 try/catch + console.log 中,一次异常即静默通过(§5.21)
expect(withAlt, 'header 中没有带非空 alt 的可见图片可供检查').toBeGreaterThan(0);
});
});
@@ -561,13 +416,14 @@ test.describe('P1: 品牌视觉审计 - 交互状态一致性', { tag: '@regress
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);
// 检查链接是否可见(跳过隐藏的链接)
const isVisible = await link.isVisible().catch(() => false);
// 检查链接是否可见(跳过隐藏的链接;isVisible() 对 nth() 精确定位的单元素
// 不会抛错,N-27 修复:去掉 `.catch(() => false)` 吞错壳)
const isVisible = await link.isVisible();
if (!isVisible) {
console.log(`Skipping hidden link ${i}`);
continue;
}
@@ -575,56 +431,58 @@ test.describe('P1: 品牌视觉审计 - 交互状态一致性', { tag: '@regress
// 悬停状态
await link.hover({ timeout: 2000 });
await page.waitForTimeout(100);
// 检查 tooltip 或 title 属性
const title = await link.getAttribute('title');
if (title) {
expect(
hasVisibleForbiddenText(title),
`导航链接标题包含非法 "novalon" 文本: ${title}`
).toBe(false);
}
} 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);
// 点击移动端菜单按钮
const mobileMenuButton = page.locator('[data-testid="mobile-menu-button"], button[aria-label*="menu"], .hamburger, .menu-toggle');
if (await mobileMenuButton.isVisible({ timeout: 3000 }).catch(() => false)) {
await mobileMenuButton.click();
await page.waitForTimeout(500);
// 真实 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 });
// 检查菜单内容
const mobileNav = page.locator('[data-testid="mobile-navigation"], nav.mobile, .mobile-menu');
if (await mobileNav.isVisible({ timeout: 2000 }).catch(() => false)) {
const navText = await mobileNav.textContent();
await mobileMenuButton.click();
const mobileNav = page.locator('[data-testid="mobile-navigation"]');
await expect(mobileNav, '点击菜单按钮后移动端导航未展开').toBeVisible({ timeout: 5000 });
expect(
hasVisibleForbiddenText(navText || ''),
`移动端导航包含非法 "novalon" 文本`
).toBe(false);
}
} else {
console.log('Mobile menu button not found - may use different structure');
// 即使没有找到移动端菜单按钮,也不应该失败(可能是桌面版布局)
expect(true).toBeTruthy();
}
} catch (error) {
console.log('Mobile menu test error:', error instanceof Error ? error.message : error);
// 移动端测试失败不应阻断整个测试套件
expect(true).toBeTruthy(); // 标记为通过,但记录问题
const navText = await mobileNav.textContent();
expect(
hasVisibleForbiddenText(navText || ''),
`移动端导航包含非法 "novalon" 文本`
).toBe(false);
} finally {
// 恢复默认视口
await page.setViewportSize({ width: 1280, height: 720 });
@@ -658,15 +516,9 @@ test.describe('P1: 品牌视觉审计 - 响应式布局验证', { tag: '@regress
await safeGoto(page, '/');
}
} catch (error) {
// 导航失败是真缺陷,不再按视口特例伪造通过(safeGoto 自带重试)
console.log(`Navigation failed for ${name}:`, error instanceof Error ? error.message : error);
// 如果导航失败,标记为跳过但不失败
if (isMobileS) {
console.log(`Skipping ${name} due to navigation timeout`);
expect(true).toBeTruthy(); // 标记为通过
return;
} else {
throw error; // 其他视口仍然抛出错误
}
throw error;
}
// 增加等待时间,确保响应式布局渲染完成
@@ -676,16 +528,10 @@ test.describe('P1: 品牌视觉审计 - 响应式布局验证', { tag: '@regress
const header = page.locator('header');
await expect(header).toBeVisible({ timeout: 10000 });
// Logo 可见(允许某些视口下 Logo 不可见的情况)
const logo = page.locator('header img, header svg, [data-testid="logo"]').first();
if (await logo.count() > 0) {
try {
await expect(logo).toBeVisible({ timeout: 2000 });
} catch {
// 某些小屏幕下 Logo 可能不可见,这是可接受的
console.log(`Warning: Logo not visible in ${name} viewport`);
}
}
// 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();
@@ -704,16 +550,19 @@ test.describe('P1: 品牌视觉审计 - 响应式布局验证', { tag: '@regress
test.describe('P1: 品牌视觉审计 - 特殊场景覆盖', { tag: '@regression' }, () => {
test('404 错误页面品牌显示正常', async ({ page }) => {
// 访问一个不存在的页面
await safeGoto(page, '/nonexistent-page-12345')
.then(() => true)
.catch(() => false);
// 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" 文本`
@@ -734,23 +583,27 @@ test.describe('P1: 品牌视觉审计 - 特殊场景覆盖', { tag: '@regression
'blockquote',
];
let checked = 0;
for (const selector of testimonialSelectors) {
try {
const element = page.locator(selector).first();
if (await element.count() > 0 && await element.isVisible({ timeout: 2000 }).catch(() => false)) {
const text = await element.textContent();
if (text && text.trim().length > 0) {
expect(
hasVisibleForbiddenText(text),
`客户评价区域包含非法 "novalon" 文本`
).toBe(false);
}
}
} catch (e) {
console.log(`Error checking ${selector}:`, e instanceof Error ? e.message : e);
continue;
}
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 }) => {
@@ -782,14 +635,9 @@ test.describe('P1: 品牌视觉审计 - 性能与截图基线', { tag: '@regress
await safeGoto(page, '/');
// 等待 Logo 加载完成(使用更宽松的超时)
const logo = page.locator('header img, header svg, [data-testid="logo"]').first();
try {
await logo.waitFor({ state: 'visible', timeout: 10000 });
} catch {
console.log('Logo not visible within 10s - may use different structure');
// Logo 可能使用了非标准结构,继续测试
}
// 等待 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;
@@ -797,42 +645,31 @@ test.describe('P1: 品牌视觉审计 - 性能与截图基线', { tag: '@regress
expect(loadTime).toBeLessThan(30000);
});
test('生成品牌视觉截图用于人工审核', async ({ page }) => {
test('生成品牌视觉截图用于人工审核', async ({ page }, testInfo) => {
// 设置更长的超时时间用于截图
test.setTimeout(90000);
await safeGoto(page, '/');
await page.waitForTimeout(1500);
try {
// 截取 Header 区域
const header = page.locator('header');
await expect(header).toBeVisible({ timeout: 10000 });
await header.screenshot({
path: 'test-results/p1-brand-audit-header.png',
fullPage: false,
}).catch((e) => console.log('Header screenshot failed:', e instanceof Error ? e.message : e));
// 截图是人工审核交付物,必须真正产出非空 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' });
// 截取 Footer 区域
const footer = page.locator('[data-testid="footer"], footer').first();
await expect(footer).toBeVisible({ timeout: 10000 });
await footer.screenshot({
path: 'test-results/p1-brand-audit-footer.png',
fullPage: false,
}).catch((e) => console.log('Footer screenshot failed:', e instanceof Error ? e.message : e));
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' });
// 截取 Logo 特写
const logo = page.locator('header img, header svg, [data-testid="logo"]').first();
if (await logo.count() > 0 && await logo.isVisible({ timeout: 3000 }).catch(() => false)) {
await logo.screenshot({
path: 'test-results/p1-brand-audit-logo.png',
}).catch((e) => console.log('Logo screenshot failed:', e instanceof Error ? e.message : e));
}
} catch (error) {
console.log('Screenshot generation encountered an error:', error instanceof Error ? error.message : error);
}
// 测试通过(截图用于人工审核,即使部分失败也不应阻断)
expect(true).toBeTruthy();
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' });
});
});