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 { 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」保留了该 节点)。因此断言方式是「资产内出现的 // 只能是这个已批准字标」,而不是旧实现的「SVG 必须有 textContent」—— // 纯 图标的 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>/g)].map((m) => m[1].trim()); for (const value of textNodes) { expect(value, `${asset} 内出现未批准的 内容`).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' }); }); });