feat(detail): Layer 3 信任层空数据兜底骨架(L3SignalsSlot + L3EmptyFallback + EarlyAccessNotice)
- 新增 L3SignalsSlot:4 类可验证替代信号卡(方法论/团队/历程/服务承诺), 服务承诺卡 disabled 占位待业务确认;零编造约束下替代真实案例/认证 - 新增 EarlyAccessNotice:首批客户共创状态条(单一真源 company.ts EARLY_ACCESS) - 新增 L3EmptyFallback:caseStudies/certifications/dataProofs 三者全空时渲染兜底, 任一非空 return null 保留本地真实 section 视觉 - 接入 5 处详情页:solution v3 / service v4 / product v3 / erp-upgrade-v3 / standalone/[id] - 修复 StaggerReveal 网格布局:grid 类须挂在 StaggerReveal 自身(否则卡片堆单列) - 更新 solution 测试至新行为(空数据→兜底渲染)+ 补 lucide 图标 mock - 修复 header.test.tsx 双 logo 断言(2593599 遗留) - 新增 scripts/audit/l3-fallback-verify.mjs 运行时验证(浅/深双版 PASS) 注:必须用 localhost 而非 127.0.0.1 —— Next dev allowedDevOrigins 默认白名单 不含 127.0.0.1,chunk 请求带该 Origin 被 403 → hydration 不发生 → 动画全冻结 验证:type-check 0 错 · lint 0 错(127 既有 warnings) · 单测 129 套件/1628 通过 · L3 兜底浅深双版渲染截图 PASS
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// 验证 Layer 3 空数据兜底(L3EmptyFallback):信号卡 + 共创占位渲染
|
||||
// 用法:node scripts/audit/l3-fallback-verify.mjs
|
||||
//
|
||||
// 断言(solution 详情页,当前无 caseStudies/certifications → 兜底应渲染):
|
||||
// 1. section[aria-labelledby=l3-signals-heading] 存在且可见(opacity→1)
|
||||
// 2. 4 张信号卡齐全,其中 3 张为链接(/methodology /team /about/brand),1 张 disabled
|
||||
// 3. EarlyAccessNotice(首批客户共创中)存在
|
||||
// 4. 深色模式下同样成立(截图目视 + token 反色由 globals.css 变量层保证)
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
// 注意:必须用 localhost(非 127.0.0.1)。Next dev 的 allowedDevOrigins 默认白名单
|
||||
// 不含 127.0.0.1,chunk 子资源请求带 Origin: http://127.0.0.1:3000 会被 403 拒掉,
|
||||
// 导致 JS 加载失败 → hydration 不发生 → 全站 whileInView 动画冻结(页面空白)。
|
||||
// 代理问题由 chromium --no-proxy-server 解决(见 launch 参数),与 URL 无关。
|
||||
const BASE = 'http://localhost:3000';
|
||||
const OUT = 'dogfood-output/l3-fallback-verify';
|
||||
const PAGE_URL = `${BASE}/solutions/manufacturing`;
|
||||
|
||||
const results = [];
|
||||
|
||||
async function probe(page, theme) {
|
||||
await page.goto(PAGE_URL, { waitUntil: 'networkidle' });
|
||||
// dev 模式 hydration 较慢(Turbopack 按需编译):等待 framer-motion 生效的标志——
|
||||
// 页面上任一 motion div 的 opacity 变为 '1'(说明 IO + rAF 循环已运行)
|
||||
let hydrated = true;
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
() => [...document.querySelectorAll('div[style]')].some((d) => d.style.opacity === '1'),
|
||||
{ timeout: 20000 }
|
||||
);
|
||||
} catch {
|
||||
hydrated = false;
|
||||
}
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const sec = page.locator('section[aria-labelledby="l3-signals-heading"]');
|
||||
const secCount = await sec.count();
|
||||
|
||||
if (secCount === 0) {
|
||||
results.push({ theme, pass: false, reason: 'L3 signals section not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 滚动触发 whileInView,等动画完成(轮询 opacity,上限 10s)
|
||||
await sec.scrollIntoViewIfNeeded();
|
||||
let headingOpacity = '0';
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const h = document.getElementById('l3-signals-heading');
|
||||
if (!h) return false;
|
||||
const o = Number(getComputedStyle(h.closest('div')).opacity);
|
||||
return o > 0.9;
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
headingOpacity = '1';
|
||||
} catch {
|
||||
headingOpacity = await sec
|
||||
.locator('#l3-signals-heading')
|
||||
.evaluate((el) => getComputedStyle(el.closest('div')).opacity);
|
||||
}
|
||||
|
||||
const cards = await sec.locator('a, [aria-disabled="true"]').count();
|
||||
const linkHrefs = await sec.locator('a').evaluateAll((as) => as.map((a) => a.getAttribute('href')));
|
||||
const disabledCard = await sec.locator('[aria-disabled="true"]').count();
|
||||
|
||||
const notice = page.locator('section[aria-labelledby="early-access-notice-heading"]');
|
||||
const noticeCount = await notice.count();
|
||||
const noticeText = noticeCount > 0 ? await notice.locator('p').first().textContent() : null;
|
||||
|
||||
await page.screenshot({ path: `${OUT}/l3-${theme}-full.png` });
|
||||
await sec.screenshot({ path: `${OUT}/l3-${theme}-section.png` });
|
||||
if (noticeCount > 0) {
|
||||
await notice.screenshot({ path: `${OUT}/notice-${theme}.png` });
|
||||
}
|
||||
|
||||
const pass =
|
||||
Number(headingOpacity) > 0.9 &&
|
||||
cards === 4 &&
|
||||
disabledCard === 1 &&
|
||||
linkHrefs.includes('/methodology') &&
|
||||
linkHrefs.includes('/team') &&
|
||||
linkHrefs.includes('/about/brand') &&
|
||||
noticeCount === 1 &&
|
||||
(noticeText || '').includes('首批客户共创中');
|
||||
|
||||
results.push({
|
||||
theme,
|
||||
pass,
|
||||
hydrated,
|
||||
headingOpacity,
|
||||
signalCards: cards,
|
||||
disabledCard,
|
||||
linkHrefs,
|
||||
noticeCount,
|
||||
noticeText: (noticeText || '').slice(0, 20),
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
// --no-proxy-server:本机系统代理会 403 掉 _next/static chunk(JS 加载失败 → hydration 不发生)
|
||||
const browser = await chromium.launch({ args: ['--no-proxy-server'] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
await probe(page, 'light');
|
||||
|
||||
await page.evaluate(() => localStorage.setItem('novalon-theme', 'dark'));
|
||||
await probe(page, 'dark');
|
||||
|
||||
await browser.close();
|
||||
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
const allPass = results.every((r) => r.pass);
|
||||
console.log(`\n[result] ${allPass ? 'PASS' : 'FAIL'} (light: ${results[0].pass}, dark: ${results[1]?.pass})`);
|
||||
if (!allPass) process.exit(1);
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user