chore(qa): 验收台账与证据入库 + 构建/部署配置同步
- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。 - 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。 - 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径; next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐 standalone 产物装配与部署形态。
@@ -42,7 +42,7 @@
|
|||||||
## 质量门禁
|
## 质量门禁
|
||||||
|
|
||||||
- [ ] `npm run test:unit` 全部通过
|
- [ ] `npm run test:unit` 全部通过
|
||||||
- [ ] `npm run test:coverage` 通过 `jest.config.js` 的 `coverageThreshold`(global:branches 30% / functions 25% / lines 32% / statements 30%),且**未下调阈值**
|
- [ ] `npm run test:coverage` 通过 `config/test/jest.config.js` 的 `coverageThreshold`(**该文件是阈值唯一真源**,根 `jest.config.js` 仅转发;global:branches 82 / functions 75 / lines 75 / statements 75),且**未下调阈值**
|
||||||
- [ ] `npm run test`(Playwright E2E)全部通过
|
- [ ] `npm run test`(Playwright E2E)全部通过
|
||||||
- [ ] `npm run check:contrast` 与 `npm run check:headings` 通过(WCAG 2.1 AA)
|
- [ ] `npm run check:contrast` 与 `npm run check:headings` 通过(WCAG 2.1 AA)
|
||||||
- [ ] `npm run lighthouse` 满足 `lhci` 断言(性能/CSP/可访问性预算未回退)
|
- [ ] `npm run lighthouse` 满足 `lhci` 断言(性能/CSP/可访问性预算未回退)
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ e2e-tests/reports/
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
*.db-journal
|
||||||
|
# SQLite WAL 边车文件(WAL 模式下与 *.db 同目录生成;此前只挡了 *.db 与 *.db-journal,
|
||||||
|
# 导致 prisma/dev.db-shm / dev.db-wal 处于未跟踪且未忽略状态,`git add -A` 会把它们带进提交)
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
data.db
|
data.db
|
||||||
data/
|
data/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
# 系统性 Code Review / 全面测试 / 验收报告 — 2026-09-21
|
||||||
|
|
||||||
|
- 分支:`refactor/optimize-ui`(领先 `origin/main` 30 个提交;`src/` 改动 120 文件,+3248 / −3959)
|
||||||
|
- 环境:Next.js 16.3.0 (Turbopack) / React 18.3 / TypeScript 5 strict / Prisma 6.19 + SQLite / Tailwind 3.4
|
||||||
|
- 方法:4 路并行模块审查(API+鉴权 / lib+hooks / 组件+页面 / 数据+配置+测试基建),所有结论由主代理逐条复核证据后定级;无法证实的假设已撤回(见 §7)
|
||||||
|
- 实测门禁:type-check / lint / 单测+覆盖率 / build / **E2E 三浏览器全量** / check:contrast / check:headings / 安全响应头 / **Lighthouse 7 URL** / **变异测试(quick 作用域)** —— 全部由本次亲跑取回原始输出,未引用任何历史报告(详见 §2)
|
||||||
|
|
||||||
|
## 1. 验收结论
|
||||||
|
|
||||||
|
**不通过验收(REJECTED)。**
|
||||||
|
|
||||||
|
阻断项 5 类:①生产级安全漏洞(权限提升 ×2、存储型 XSS ×2);②E2E 92 例失败(3 浏览器一致复现,含 6 例 `@critical`);③CI 用 `|| echo` 吞掉全部功能性测试,绿色徽章不证明任何行为正确性;④本分支引入 2 处用户可见 UI 回归;⑤**可访问性门禁双重失效**(A-11)导致一个**全站 62 页**的 WCAG AA 对比度失败(A-12 Cookie 条隐私政策链接 3.31:1)在 `check:contrast` 与 Lighthouse 两道门禁下同时报绿。
|
||||||
|
|
||||||
|
> 关键判据不是「失败数」而是「门禁是否可信」。当前 `test:all` 与 Jenkins 绿灯所覆盖的范围,与 AGENTS.md §5 声称的质量门禁之间存在实质落差;本次实测中**两道 a11y 门禁双双通过,却被两个独立引擎各测出 critical 级失败**,即为该落差的最强证据。
|
||||||
|
|
||||||
|
## 2. 门禁执行结果(本次实测,非引用)
|
||||||
|
|
||||||
|
| 门禁 | 命令 | 结果 | 判定 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 类型检查 | `npm run type-check` | 0 error,EXIT=0 | 通过 |
|
||||||
|
| Lint | `npm run lint` | 0 error / **128 warning** | **不达 AGENTS.md「无警告」** |
|
||||||
|
| 单元测试 | `npm run test:unit` | **132 套件 / 1594 例全通过**,23.9s | 通过 |
|
||||||
|
| 覆盖率 | `npm run test:coverage` | stmts 75.9 / branch 84.28 / func 75.46 / line 75.9 | 通过(但门禁形同虚设,见 §3.3) |
|
||||||
|
| 生产构建 | `npm run build` | EXIT=0,62 静态页生成 | 通过 |
|
||||||
|
| E2E 三浏览器 | `playwright test --project=chromium,firefox,webkit` | **710 通过 / 92 失败 / 8 跳过**,17.2min,EXIT=1 | **失败** |
|
||||||
|
| 色彩对比度 | `npm run check:contrast` | 7/7 通过 | 通过(**但仅 7 组硬编码浅色对,盲区见 §3.4**) |
|
||||||
|
| 标题层级 | `npm run check:headings` | 10/10 页 0 问题 | 通过 |
|
||||||
|
| 安全响应头 | `npm run test:security:headers` | 6 通过 / 2 警告 | 通过(**但打的是线上 novalon.cn,不验证本分支**) |
|
||||||
|
| Lighthouse | `npx lighthouse@13`(对 `lighthouserc.json` 全部 7 个 URL,desktop preset) | **性能全绿**(perf 99-100 / FCP 246-250ms / LCP 790-896ms / CLS **0.000** / TBT 0ms / SI 248-414ms);**a11y 92-97、7 页全部含 contrast 失败节点,共 10 节点** | **断言 0 违规 → 门禁"通过",但门禁太松看不见真实 a11y 缺陷,见 §3.4** |
|
||||||
|
| 变异测试 | `npx stryker run --inPlace --mutate 'src/lib/utils.ts'`(`test:mutation:quick`) | **91.18%**(31 killed / 3 survived / 0 no-cov / 0 error),≥ `break:50`;**全量作用域未跑**(外推需数小时) | 通过(阈值达标),但暴露 2 处真实断言缺陷 + 作用域排除「零编造」核心,见 §3.5 |
|
||||||
|
| 压测 | `npm run test:performance` | **不可运行**:`k6` 是 v0.0.0 占位包(`node_modules/k6/package.json`:"Dummy package for autocompleting k6 scripts"),无 `node_modules/.bin/k6` | **失效** |
|
||||||
|
|
||||||
|
Lint 128 warning 构成:`no-explicit-any` 49 · `react-hooks/set-state-in-effect` 12 · `next/no-img-element` 10 · 其余 57。
|
||||||
|
|
||||||
|
## 3. 阻断级问题(P0)
|
||||||
|
|
||||||
|
### 3.1 权限提升与账号接管(已逐行复核)
|
||||||
|
|
||||||
|
**A-1 `content_admin` → `super_admin` 自主提权** — `src/app/api/admin/users/route.ts:90`(POST)/ `:166`(PUT)放行 `content_admin`;`:135` `const rolesToAssign = body.roleCodes?.length ? body.roleCodes : ['readonly']` 直接取请求体,`:139`/`:214` 仅校验「角色是否存在于 DB」,不校验「调用者是否有权授予」。`:201-208` 只阻止移除**自己**的 super_admin,授予方向无任何 allowlist。
|
||||||
|
攻击:`POST /api/admin/users {username,password,roleCodes:["super_admin"]}`。
|
||||||
|
修复:两个 handler 均将 `body.roleCodes` 与调用者角色集求交,非 `super_admin` 不得授予 `super_admin`。
|
||||||
|
|
||||||
|
**A-2 `content_admin` 可重置任意用户密码 → 接管超管** — `users/route.ts:189-191` `if (body.password) updateData.password = await hashPassword(body.password)` → `:193` `prisma.user.update({ where: { id: userId } })`,`userId` 来自 `:172` 攻击者可控的 query 参数,未校验目标是否持有 `super_admin`。同理 `:188` 可将超管 `status` 置 0 致其失联。
|
||||||
|
攻击:`PUT /api/admin/users?id=<superadmin_cuid> {password:"x"}` 后登录。
|
||||||
|
修复:改密/停用他人须 `super_admin`;目标持有 `super_admin` 时同样要求调用者为 `super_admin`。
|
||||||
|
|
||||||
|
架构成因:`src/proxy.ts:104` `matcher: ['/admin/:path*']` 且 `:81` 显式 `!pathname.startsWith('/api/')` —— **proxy 完全不覆盖 `/api/*`**,每个 route 必须自守卫。(Next 16 已将 `middleware.ts` 更名 `proxy.ts`,此处用法正确。)
|
||||||
|
|
||||||
|
### 3.2 存储型 XSS ×2(已验证数据通路)
|
||||||
|
|
||||||
|
**A-3 CMS 富文本未净化直出公开页** — `src/app/terms/page.tsx:212` 与 `src/app/privacy/page.tsx:267`:`dangerouslySetInnerHTML={{ __html: cmsContent }}`,`cmsContent` 来自 `getPublishedItems('legal-page')` → `item.data.content`(`terms/page.tsx:181`)。字段类型 `richtext`,描述即「支持 HTML 标签」(`content-types.ts:1563`)。**全仓无净化器**(`DOMPurify` / `sanitize-html` 检索 0 命中);`RichTextEditor.tsx:38-46` 的 TipTap `Link` 未配置 `protocols`。
|
||||||
|
影响:任何可编辑 legal-page 的账号(或经 A-1 提权者)即可在 `/terms`、`/privacy` 对**全部访客**注入脚本。
|
||||||
|
修复:渲染端统一 `sanitize-html`(白名单标签/属性/协议),并在 `uploadMedia`/items 写入侧再校验一次。
|
||||||
|
|
||||||
|
**A-4 上传文件同源分发且安全头丢失** — `src/app/api/admin/media/route.ts:79-84` 将客户端可控的 `file.type`/`file.name` 原样传入;`src/lib/media/media-service.ts:36-87` `uploadMedia` **无扩展名/MIME 白名单**(`:44` `isImage()` 只门控缩略图,不门控落盘),`generateUniqueFileName` 保留原扩展名 → `evil.html` 存入 `public/uploads/`。`nginx-static-production.conf:163-168` 的 `location /uploads/` 自带 `add_header`,按 nginx 语义**不再继承** server 级 `X-Content-Type-Options: nosniff`(`:42`)与 CSP(`:128`)→ 以 `text/html` 同源渲染。叠加 CSP 含 `script-src 'unsafe-inline' 'unsafe-eval'`(`next.config.mjs:40`),失去兜底。
|
||||||
|
修复:`uploadMedia` 增白名单 + magic-byte 校验;`/uploads/` 内重新 `add_header nosniff/CSP` 并对非图片强制 `Content-Disposition: attachment`;移除 `unsafe-eval`。
|
||||||
|
|
||||||
|
### 3.3 测试基建不可信(最高价值发现)
|
||||||
|
|
||||||
|
**A-5 CI 功能性测试全部被 `|| echo` 吞掉** — `Jenkinsfile:179` `playwright test --grep "@smoke|@critical" || echo "⚠️ …继续执行"`;`:181` journey、`:214` 视觉回归、`:236` `npm audit`、`:238` 安全头 同法吞没。真门禁仅 lint(:129)/type-check(:136)/coverage(:148)/build(:174,254)。**AGENTS.md §5 列为必须的 lighthouse、`check:contrast`、`check:headings`、mutation、k6、integration 在流水线中完全缺席。**
|
||||||
|
结论:「CI 绿」当前只证明 lint + tsc + 「覆盖率≥30%」+ build 成功,不证明任何用户行为。
|
||||||
|
修复:去掉功能性 stage 的 `|| echo`,把缺失门禁纳入流水线。
|
||||||
|
|
||||||
|
**A-6 `@critical` GA4 测试是自证式空测** — `e2e/ga4-event-tracking.spec.ts:47,75,165,235`(4 例,标签 `@critical`):`beforeEach:24-31` 注入 mock `window.gtag`,测试体 `:56-62` **自行调用** `gtag('config','G-TEST123',…)`,再于 `:65-70` 断言 `__gtagCalls` 中存在该调用。TC-GA4-003 `:184-205` 甚至不点击按钮,只判可见性后自调用;且 `if (isCtaVisible) {} else {}` 两分支均自调用 → **CTA 不存在也通过**。断言的 `G-TEST123` 由测试自己提供,与应用无关。
|
||||||
|
影响:`npm run test:critical` 对分析埋点的绿灯为 0 信息量。
|
||||||
|
修复:改为拦截真实网络请求(`gtag/js` 的 `page_path`/事件参数),或让 `trackEvent` 走可注入 sink 并断言应用调用。
|
||||||
|
|
||||||
|
**A-7 覆盖率门禁形同虚设 + 三处文档口径互斥** — 实际生效门禁用 `jest.config.js` global `branches 30 / functions 25 / lines 32 / statements 30`,而实测为 `84.28 / 75.46 / 75.9 / 75.9`,**低于实测约 45 个百分点**;目录级阈值多数不可约束:`seo` branches 门限 0(实测 100)、`content` branches 门限 4(实测 100)、`ui` functions 门限 10(实测 81)。文档互斥:`CLAUDE.md:39,196` 称阈值 80% 且路径写作 `config/test/jest.config.js`(**路径错误**,真实配置在仓库根;`config/test/jest.config.js` 仅 Stryker 使用);`docs/development/quality-gates.md:82-85` 称四项均 ≥70%。
|
||||||
|
修复:阈值上调至「实测 −5pp」的棘轮值;统一三处文档并修正配置路径。
|
||||||
|
|
||||||
|
**A-8 单测不接触真实数据层** — `jest.setup.js:12-27` 全局 mock `PrismaClient`(`findMany → []`),`:29-50` 整体 mock `@/lib/cms/data-server`。因此 §4 所有数据层缺陷(无事务、TOCTOU、JSON.parse 崩溃)**没有任何测试能发现**。`npm run test:integration`(`--testPathPatterns='src/app/api/'`)跑的仍是 mock 版 `route.test.ts`(如 `items/route.test.ts:14` mock `@/lib/db`),命名误导。
|
||||||
|
另有空洞断言:`src/lib/db.test.ts:6-9` `expect(prisma).toBeDefined()` 对全局 mock 永真;`colors.test.ts`(≈18)、`constants.test.ts`(≈24)、`design-system.test.ts`(21) 大量 `toBeDefined()` 静态常量。
|
||||||
|
|
||||||
|
**A-9 视觉基线已「追认现状」,且 9 张近空白** — 提交 `f543e47` 自述「87 例失败…均为尺寸级不匹配」后重生成 86/115 基线 ⇒ 重生时点已存在的回归**被固化为参照**,该套件此后无法再发现它。基线总数 107(5 project × 21,齐全),但 `visual-{chromium,firefox,webkit}-desktop/…/button-{default,hover,focus}-*.png` 为 **912 / 1111 / 1961–2030 字节**,日期 Jul 26 与 Jul 6(**在 9 月重生成之外**)⇒ 近空白区域仍算「比对通过」。容差偏松:`playwright.config.ts:38-41` `maxDiffPixels:200`、`maxDiffPixelRatio:0.005`、`threshold:0.3`。部分断言条件化(`visual-regression.spec.ts:73,91,101,111,130,182` 元素缺失即 0 断言通过)或为永真(`:199` `color||fontFamily`、`:217` `brand||ink||bg` `toBeTruthy()`)。
|
||||||
|
|
||||||
|
**A-10 E2E 从不验证交付物** — `playwright.config.ts:130` `command: 'npm run dev'` + `:132 reuseExistingServer: true`,全部规格跑在 **dev server**(甚至可能是上一轮残留进程)而非 `output:'standalone'` 构建产物。构建期才暴露的问题(预渲染、ISR、production header)永不被测。
|
||||||
|
修复:新增 `--project=production` 指向 `npm run start` 的 webServer。
|
||||||
|
|
||||||
|
### 3.4 可访问性门禁的双重失效与已证实的全站缺陷
|
||||||
|
|
||||||
|
**A-11 两个对比度门禁同时看不见同一个真实违规** — 两条独立通路各自漏检:
|
||||||
|
|
||||||
|
1. **`check:contrast` 静态漏检** — `scripts/utils/check-color-contrast.ts:10-18` 的 `criticalColorPairs` 是 **7 组硬编码十六进制**(全部 `#FFFFFF` 底),既不读 `tailwind.config.js`/`globals.css`,也**不含任何暗色模式配对**,更不覆盖 alpha 修饰类。而本分支主题提交(`4b8500a`、`846585a`)恰好把暗色模式与 `--color-brand` 双通道拆分作为主战场 —— 门禁与其要保护的对象完全脱节,令牌一旦改动它仍对着陈旧色值报绿。
|
||||||
|
2. **Lighthouse 阈值漏检** — `lighthouserc.json` 对 accessibility 断言 **≥0.9**,而含 axe critical 失败的实际得分是 **92-97**:`products` 92 分(7 个 `aria-required-parent` critical 节点)、其余 6 页 96-97 分(每页 1-4 个 `color-contrast` 节点)。**critical 级 WCAG 失败被折算成分数后落在门禁线之上**,故 `npm run lighthouse` 会显示全绿。
|
||||||
|
|
||||||
|
**A-12 Cookie 同意条的「隐私政策」链接在全部 62 页对比度不达标(已双引擎证实)** — `src/components/analytics/CookieConsent.tsx:152` `text-[var(--color-brand)]`(#C41E3A)落在同文件 `:141` 的 `bg-[var(--color-bg-primary)]`(暗色 #0A0E14)上:
|
||||||
|
|
||||||
|
- Lighthouse/axe 实测 **3.3:1**(15.75px normal,要求 4.5:1);我按 `globals.css:23/416` 令牌值独立算得 **3.31:1**,两法吻合。
|
||||||
|
- 该组件挂在**根布局** `src/app/layout.tsx:223` ⇒ 7/7 被测页各命中 1 次,即**全站每一页**都失败(10 个失败节点中的 7 个来自此处,余 3 个见 §4.3)。
|
||||||
|
- 根因是**违反已写明的设计契约**:DESIGN.md:154「**The Two-Channel Red Rule.** 底色用 `--color-brand`(暗黑不翻),文字用 `--color-brand-ink`(暗黑翻至 #F87171)。合并成一条是 bug 的源头」、DESIGN.md:232「用 `text-brand-ink` 写红色文字…**永不混用**」。此处的合规写法应为 `text-brand-ink`。
|
||||||
|
- 讽刺点:这是**隐私/Cookie 同意 UI**里指向隐私政策的链接,属合规可见路径。
|
||||||
|
|
||||||
|
**同类面**:全仓 **67 处** `text-[var(--color-brand)]`(23 个文件,含 `CookieConsent`、`error.tsx`、`not-found-content.tsx`、`cta-section`、`hero-section-v2`、`product-card`、`service-card` 等)对比 233 处合规的 `text-brand-ink`。这些站点在浅色面(#FFFFFF 上 **5.84:1**)偶然达标,一旦位于暗色面即跌到 **3.31:1**(`--color-brand-bg` #2A1418 上更仅 **2.97:1**)—— 而本分支暗色为默认。**修复应整族收敛而非逐点打补丁**:以 `text-brand-ink` 替换全部 67 处,并加一条 grep 门禁禁止 `text-[var(--color-brand)]`。
|
||||||
|
|
||||||
|
> 本项由 Playwright+axe(移动)与 Lighthouse+axe(桌面)**两个独立引擎**分别复现,非源码推断 —— 这是本次验收中证据强度最高的一类结论。
|
||||||
|
|
||||||
|
### 3.5 变异测试(本次实跑,唯一真正量化「测试有没有断言」的门禁)
|
||||||
|
|
||||||
|
`npm run test:mutation:quick`(作用域 `src/lib/utils.ts`):**91.18%**(31 killed / 3 survived / 0 no-coverage / 0 error,均值 14.97 tests/mutant,74s)≥ `stryker.config.json` 的 `break: 50` ⇒ 阈值达标。但 3 个存活变异体经逐个复核后,**2 个是本项目的真实测试缺陷**:
|
||||||
|
|
||||||
|
**A-13 `lerp` 的全部测试用例都以 `start = 0` 输入 ⇒ `start` 偏移量从未被检验** — `src/lib/utils.ts:49` `start + (end - start) * t` 被改为 `start + (end + start) * t` 后仍全绿。原因(`src/lib/utils.test.ts:120-128` 四例逐一验算):`lerp(0,10,0.5)`、`lerp(0,100,0.25)`、`lerp(0,10,0)`、`lerp(0,10,1)` —— **`start` 恒为 0**,而 `end - 0` 与 `end + 0` 数值相同,故该变异在数学上不可观测。这是**测试数据选择缺陷**:函数唯一独有的参数(`start`)恰好是唯一没被非零值覆盖的那个。
|
||||||
|
|
||||||
|
**A-14 `randomBetween` 只断言边界,检不出算子错误** — `utils.ts:45` `Math.random() * (max - min) + min` 改为 `/` 后仍全绿。`test.ts:106-115` 只做 `toBeGreaterThanOrEqual(1)` / `toBeLessThanOrEqual(10)`;变异实现给出 `rand/9 + 1 ∈ [1, 1.89]`,负数例给出 `∈ [-1, -0.89]` —— 均落在断言区间内。分布被压到区间一端 11% 的长度而测试无法察觉,因为**没有任何一例检验取值是否覆盖全区或分布是否均匀**。
|
||||||
|
|
||||||
|
**(反向校准)第 3 个存活体不是缺陷** — `utils.ts:25` 的 `if (timeout)` → `if (true)`:`clearTimeout(null)` 在 Node/浏览器均为合法 no-op,故该变异体与原实现**语义等价**,存活属正常,不计入测试质量问题。列出以示本次定级未把噪声当发现。
|
||||||
|
|
||||||
|
**A-15 变异门禁的作用域把「零编造」核心排除在外** — `stryker.config.json:20` 的 `mutate` 含排除项 `"!src/lib/constants/**"`,而 `src/lib/constants/metrics-basis.ts`(`resolveMetricBasis` / `weakestBasis` / `FORBIDDEN_PROOF_PHRASES`,即项目 AGENTS.md §3「零编造」原则的唯一机械载体)**正在该目录内**。后果:那个「未声明口径必须保守回落到 `target`」的回落逻辑,若被改坏(例如回落到 `verified`)**不会有任何变异测试发现** —— 与 B-3「`basis` 仅类型约定、写入侧无校验」构成同一处治理缺口的两半。
|
||||||
|
|
||||||
|
**门禁自身的两点风险(实测记录)**:
|
||||||
|
1. `npm run test:mutation` 与 `:quick` 均带 `--inPlace`,Stryker 会**直接改写工作树**(其日志自述 "In place mode is enabled, Stryker will be overriding YOUR files")。本次运行期间 `git status` 一度显示 10+ 个文件为 ` M`(Stryker 为注入覆盖率而临时修补 jest/babel 配置),结束后由 `.stryker-tmp/backup-*` 复原,**我已核实工作树恢复到运行前状态**(仅本报告与 `deliverables/` 两个未跟踪项)。但这意味着 CI 中一旦该 job 中途崩溃,仓库将留下**被变异过的源码**且无 `git checkout` 提示 —— 建议 CI 改用沙箱模式(去掉 `--inPlace`)。
|
||||||
|
2. 顺带证伪了一个可疑点:我曾怀疑 Stryker 用的 `config/test/jest.config.js` 与根 `jest.config.js` 存在测试发现差异(其 dry-run 报 "Ran 668 tests" 而 `test:unit` 为 1594)。实测 `npx jest --config config/test/jest.config.js` ⇒ **132 套件 / 1594 例全通过**,两配置的 `testMatch`/`roots`/`setupFilesAfterEnv` 一致,无结果分歧,故**不列为缺陷**(668 与 ✘ 标记是 Stryker perTest 覆盖分析的呈现方式,✘ = 该测试未覆盖当前变异体,**不是失败**)。唯一真实差异仍是 A-7 已记的**阈值不同**(此配置 global 为 70/55/55/55,根配置为 30/25/32/30)—— 即两份 jest 配置近重复却配着互不相同的门禁值。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 4. 高危问题(P1)
|
||||||
|
|
||||||
|
### 4.1 本分支引入的 UI 回归(验收主要风险)
|
||||||
|
|
||||||
|
**R-1 双重移动端安全区留白 ≈190px** — `globals.css:1321-1323`(本分支审计修复 `152eef9` 新增)`footer { padding-bottom: calc(64px + env(safe-area-inset-bottom,0px)) }` 叠加 `src/components/layout/footer.tsx:171` 的 `pb-[calc(8rem+env(safe-area-inset-bottom,0px))]` ⇒ iPhone SE/15 上备案行下方 128+64+2×inset ≈ **192–226px** 空深色带,**全部 31 页**。
|
||||||
|
修复:二选一(建议只保留 CSS 侧,并删除组件 `pb-[…]`)。
|
||||||
|
|
||||||
|
**R-2 动效时长收敛到错误基准** — `CONTEXT.md:76` 与 `DESIGN.md:233` 规定入场 **180–280ms**,`--transition-normal: 280ms`(`globals.css:252`);分支却收敛到 300ms:`src/components/ui/scroll-reveal.tsx:73` `duration = 0.3` 且注释**错误引用契约为「200-300ms」**;另有 `duration-300` ×108、`duration: 0.3` ×179 未令牌化。
|
||||||
|
修复:以 `duration-normal/fast` 令牌替换字面量,修正注释;或正式修订 CONTEXT.md。同类:`--stagger-*` 令牌(`globals.css:272-276`)**零采用**(`var(--stagger` 检索 0 命中),实散为 `detail-trust-section.tsx:108` `index*0.06`、`header.tsx:230` `index*0.05`、`contact-content-v3.tsx:262` `delay: 1.2`(**1200ms**,远超 150ms 段间上限)。
|
||||||
|
|
||||||
|
**R-3 暗色模式新闻页对比度不达标** — `src/app/(marketing)/news/[slug]/NewsDetailClient.tsx:47` `bg-[var(--color-brand-bg)] text-[var(--color-brand)]`(另 `:34`、`:124`)误用**设计上不翻转**的 `--color-brand` #C41E3A 作文字色,违反 DESIGN.md:154 双通道红规则。暗色实测 **3.31:1**(`--color-bg-primary` #0A0E14 上)与 **2.97:1**(`--color-brand-bg` #2A1418 上)。`news-detail-content-v3.tsx:29,105`、`news-content-v3.tsx:37` 同病。同处 `hover:bg-[var(--color-brand)]/20`、`text-[var(--color-brand)]/30` 在构建产物中**不生成任何 CSS**(对照 `border-brand/30` 可正常编译)。
|
||||||
|
**本项是 §3.4 A-12 全站缺陷的一个局部实例** —— 同一契约违反在全仓共 67 处,故修复须按 A-12 整族收敛,只改新闻页会留下 Cookie 条等仍在失败。
|
||||||
|
修复:文字改用 `text-brand-ink`。同族缺陷另见 `content-unavailable.tsx:38`(本分支新增,暗色 3.31:1)。
|
||||||
|
|
||||||
|
**R-4 审计 §10.1 修复 #4 只落一半** — `src/components/detail/solution-service-card.tsx:128` `shadow-blue-500/20 → shadow-brand` 已做,同行保留 `bg-gradient-to-br from-[var(--color-accent-blue)] to-[#1d4ed8]`(硬编码 hex + 双色渐变),`:127` `hover:border-blue-300`、`:134` `group-hover:text-blue-600` 绕过 `accent-blue` 令牌且暗色不翻转 ⇒ **蓝色块配品牌红光晕**,违反 DESIGN.md:150/153「One Voice」与 CONTEXT.md:45「红与强调色不同卡」。
|
||||||
|
|
||||||
|
**R-5 `prefers-reduced-motion` 未被 framer-motion 尊重** — 全仓 **0 处 `MotionConfig`**(`grep MotionConfig src` 无命中),`globals.css:755-764` 的 CSS 守卫(`animation/transition-duration: 0.01ms !important`)**管不到 framer-motion 的 JS-rAF 内联样式**。仅部分文件单独调用 `useReducedMotion()`,未接线者(如 `detail-trust-section.tsx:80-83` 的 `y:24→0`、`detail-hero.tsx`、`product-detail-content-v3.tsx`)在暗色+动效默认开启的本分支上,对前庭敏感用户仍产生大位移。与 DESIGN.md:122「reduced-motion 全链路守卫」不符。
|
||||||
|
修复:根布局包 `<MotionConfig reducedMotion="user">`,一处收口。
|
||||||
|
|
||||||
|
### 4.2 逻辑与安全(非本分支引入,但在验收范围内)
|
||||||
|
|
||||||
|
**B-1 创建接口绕过发布工作流** — `src/app/api/admin/items/route.ts:130` `status: status || 'draft'` + `:135` `publishedAt: status==='published' ? new Date() : null`,仅 `:107` `requirePermission(…,'create')` 守卫;PUT 侧 `:184-186` 明确拒改 status 并要求走 workflow 接口 —— 即 `POST {status:'published'}` 可跳过 submit/approve 与 `publish` 权限直接上线。且任意 status 字符串可入库,之后 `workflow.ts:42` 对所有动作返回 `false`,条目永久卡死且无反馈。
|
||||||
|
|
||||||
|
**B-2 登录接口用户枚举 + 零限流** — `src/app/api/auth/login/route.ts:20-22` 在 `:24` `verifyPassword` **之前**返回 `'账号已被禁用,请联系管理员'`,未知用户则为 `:17` `'用户名或密码错误'`;即便消息相同,`user` 不存在时不跑 bcrypt 也留下可靠的时间侧信道。全仓唯一限流在 `src/app/api/contact/route.ts:14`,登录裸奔(`nginx-static-production.conf:177` `rate=100r/s` 仍允许约 860 万次/日)。修复:禁用检查移到验密之后;对 `/api/auth/login` 加 per-IP+per-username 计数或上游独立 `limit_req`。
|
||||||
|
|
||||||
|
**B-3 `basis` 仅类型约定,非结构强制**(直接对应「零编造」原则)— 类型侧全部可选:`products.ts:15`、`services.ts:40`、`solutions.ts:27`、`sections.tsx:323`、`about-content-v4.tsx:39` 皆 `basis?: MetricBasis`;运行侧 `items/route.ts:131` `JSON.stringify(data || {})` **完全不按 `FieldDefinition` 校验**(`cms/types.ts:50-55` 的 `validation {min,max,pattern}` 全仓从未执行,zod 只在 `api/contact/route.ts` 使用)。故 `POST {data:{metrics:[{value:'99.9%',basis:'verified'}]}}` 会渲染「已有可核验的实测出处」。机械门禁 `metrics-basis.test.ts:53-64,170-182` 读的是**导入的 seed 字面量**、只扫 `src/app`+`src/components`,`prisma/` 与 `/admin` 编辑后的库内容从不复检。
|
||||||
|
缓解事实:`resolveMetricBasis`/`weakestBasis`(`metrics-basis.ts:34-47`)保守回落 + `content-types.ts:8-18` 的 `metricBasisField` 明示「留空按目标口径处理」—— 兜底方向正确,`FORBIDDEN_PROOF_PHRASES` 扫描也确属严格(含 `length>40` 防空跑)。真正的漏洞在**写入侧无校验**,以及 `home-content-v15.tsx:510,552,555,562` 用 `Record<string, any>` / `as any` 把 CMS 载荷从类型系统里放行。
|
||||||
|
|
||||||
|
**B-4 `data:null` 写库即打挂整页** — `items/route.ts:197` `if (data !== undefined) updateData.data = JSON.stringify(data)`(PUT 缺 POST 那样的 `|| {}` 兜底)→ `data-server.ts:20` `JSON.parse(item.data)` 得 `null` → `terms/page.tsx:180` `items.find(i => i.data.pageType === 'terms')` 抛 `TypeError`。同类:`data-server.ts:20,75-77,89-91,106` 的 `JSON.parse` **无 try/catch**(对照 `media-service.ts:133-139 parseDerivatives` 已正确守卫),单个脏列即可中断页面/SSG。
|
||||||
|
|
||||||
|
**B-5 角色权限重写无事务** — `src/app/api/admin/roles/route.ts:81` 先 `deleteMany` 全部权限再 `:85-97` 循环 `create`,**全仓应用代码零 `$transaction`**。中途失败即留下**权限为空/半权限**的角色(提权/降权双向风险)。同因:`cms/workflow.ts:59→66→80` `version: item.version+1` 读-改-写在事务外(`items/route.ts:192` 却用了正确的 `{ increment: 1 }`,同规则两实现)→ 并发审批丢更新 + status TOCTOU;`workflow.ts:80→90→102` update/audit/notify 三连 await 无原子性,通知失败会把成功审批变成 500。SQLite 侧 `src/lib/db.ts:7` 未配 `journal_mode=WAL`/`busy_timeout`(`DATABASE_URL` 无查询参数),默认 rollback journal + `busy_timeout=0` 下并发写直接 `SQLITE_BUSY`。
|
||||||
|
|
||||||
|
**B-6 `/api/cms/draft/enable` 密钥缺失即放行 + 开放重定向** — `route.ts:15` `if (expectedSecret && secret !== expectedSecret)` 为 **fail-open**;`CMS_PREVIEW_SECRET` 经全仓检索**不在 `.env` / `.env.local` / `.env.production` / `.env.example` 任何一处**,故线上恒为未配置,任意请求可 `draft.enable()`,并直达 `:25-27` `NextResponse.redirect(new URL(redirect, request.url))`,`redirect` 取请求体、协议相对形式 `//evil.com` 即跳出站外。(内容面影响当前为零:`draftMode()` 除这两个路由外无人读取,`data-server.ts:38,49,195,219` 硬过滤 `status:'published'`。对照 `/api/cms/revalidate/route.ts:40-45` 未配置即 500,写法正确 —— 应统一为 fail-closed。)
|
||||||
|
|
||||||
|
**B-7 刷新令牌无轮换/吊销** — `auth/refresh/route.ts:16` 仅验签名,`:21-25` 直接用 `payload` 重签,不加载用户 ⇒ 被禁用/删除的用户 7 天内持续换取访问令牌,绕过 `login/route.ts:20` 的禁用拦截;`:24` `role: payload.role` 沿用旧 claim,降权用户保留高权标识(`permissions.ts:38-56` 按 `UserRole` 实查故服务端不破,但 `admin-layout.tsx:241`、`auth/me/route.ts:15` 会显示陈旧角色)。`logout/route.ts:5-10` 只清 cookie,`prisma/schema.prisma` 无 jti/tokenVersion 表 ⇒ 失窃 refresh token 登出后仍有效。
|
||||||
|
|
||||||
|
**B-8 Bento 网格 ARIA 角色无父(3 页 × 3 浏览器 = 9 例失败的真实根因)** — `src/components/sections/bento-grid.tsx:23` 把 `role="list"` 放在 `BentoGrid`、`:47` 子项 `role="listitem"`;但 `src/app/(marketing)/products/products-content-v3.tsx:9` **只 import 了 `BentoItem`**,卡片直接落在无 `role="list"` 的 `div.grid lg:grid-cols-2 gap-px`(`:~193`)上 ⇒ axe `aria-required-parent`(critical)。
|
||||||
|
**已由 Lighthouse 桌面端独立复现并逐节点确认**:`/products` 命中 **7 个** critical 节点,`data-testid` 分别为 `bento-product-card-{erp,crm,cms,bi,sds,oa,novavis}` —— 与我按源码推断的「7 张 BentoItem 卡」精确一致,故根因不是假设。该页 a11y 得分因此降至 **92**(仍高于 0.9 门禁,见 A-11)。
|
||||||
|
修复:改用 `BentoGrid` 包裹,或去掉 `BentoItem` 的 `role="listitem"`。
|
||||||
|
|
||||||
|
**B-9 `tracking-tightest` 是空类名,10 个 H1 丢失展示级字距** — `tailwind.config.js:157-165` 的 `letterSpacing` 仅 `tighter/tight/normal/wide/wider/widest/eyebrow`,**无 `tightest`**;构建产物 `dist/static/chunks/0o3c09ni2y1ao.css` 内只有 `tracking-tighter`/`tracking-tight` 两条规则,**不存在 `.tracking-tightest`** ⇒ 该类在 10 处标题(`contact-content-v3.tsx:249`、`about-content-v4.tsx:70`、`cases-content-v3.tsx:146`、`team-content-v3.tsx:168` 等)**静默失效**,全部大字标题以 0em 字距呈现,与 DESIGN.md:28 的 −0.03em 相悖。与 config 自述(`:130-133`)已发生过的 `font-calligraphy` 同族缺陷。
|
||||||
|
|
||||||
|
**B-10 mega 下拉键盘不可关闭(WCAG 1.4.13 / 2.1.2)** — `src/components/layout/mega-dropdown.tsx:37` `onMouseEnter` 开、`:28-30` 仅 `onMouseLeave` 关,无 Escape 路径;`header.tsx:28-32` 的全局 Escape 只看 `isOpen`(移动抽屉),从不看 `openDropdown`。键盘用户在「产品」上回车后必须动鼠标才能关闭。
|
||||||
|
|
||||||
|
### 4.3 E2E 真实失败分类(92 = 约 30 例 × 3 浏览器,非抖动)
|
||||||
|
|
||||||
|
| 失败簇 | 例数 | 定性 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ga4-event-tracking` TC-GA4-001..004 `@critical` | 12 | **测试自身缺陷**(A-6 空测)+ 断言环境不成立 |
|
||||||
|
| `mobile-accessibility` axe `aria-required-parent` `/products` | 3 | **真实缺陷** B-8(Lighthouse 桌面端独立复现同样 7 节点,见 B-8) |
|
||||||
|
| `mobile-accessibility` axe `color-contrast` `/about`、`/team` | 6 | **需产品裁决**:节点为 `about-content-v4.tsx:217`、`team-content-v3.tsx:85,118` 的 `text-text-muted/30|/10` **`aria-hidden="true"` 装饰性序号**(Lighthouse 桌面端在 `/about` 复现同样 3 个 `aria-hidden` 节点,类名一致);axe 按视觉可见性仍会报。建议按 WCAG「incidental/decorative」显式豁免,或提高 alpha。真正该修的是**正文** `text-text-secondary/80`(`brand-content.tsx:129`、`team-content-v3.tsx:205`)。**注意:Lighthouse 另在 7/7 页各报 1 例装饰节点之外的真缺陷,即 A-12 Cookie 条链接 —— 移动套件反而没抓到它** |
|
||||||
|
| 触摸目标 ≥44px(首页,`@accessibility`+`@performance`) | 6 | 真实移动可用性风险,需按元素定位 |
|
||||||
|
| `p1-brand-visual-audit`「不应出现可见 novalon」× 首页/全站 | 6 | **测试过期**(见 §7),`CONTEXT.md:194` 已批准该文案 |
|
||||||
|
| `uj-11b` 共创旅程 `@critical` | 6 | **真实可测性缺陷**:`uj-11-home-conversion.spec.ts:117` 依赖 `data-testid="early-access-banner"`,而首页 HTML 中该 testid **0 命中**;`:131` 期望文案「成为首批共创客户」HTML 亦 0 命中(「首批客户共创中」「共创进行时」均存在)。实现未落测试钩子 |
|
||||||
|
|
||||||
|
## 5. 中危(P2)摘要
|
||||||
|
|
||||||
|
- `header.tsx:187` `aria-controls="mobile-menu"` 指向 `{isOpen && …}` 内才存在的 `id`(`:219`)——关闭时 AT 解析落空。
|
||||||
|
- `contact-content-v3.tsx:111` `setErrors(prev => ({…prev,[field]:undefined}))` 不删键 ⇒ `:376` `Object.keys(errors).length` 与 `:379`「请修正以下 N 项」长期错误,字段全修完后仍残留带空 `<li>` 的 `role="alert"` 红框。
|
||||||
|
- `header.tsx:167` `<div className="hidden md:flex">` 使 `ThemeToggle` **桌面专属**,移动抽屉(`:224-261`)从不渲染 ⇒ 系统深色下的手机用户无法切浅色。本分支暗色默认开启,影响放大。
|
||||||
|
- `header.tsx:87` 高度 80/64 动画 vs `layout.tsx:18` 固定 `pt-16`(64px) ⇒ `scrollY===0` 时首屏顶部 16px 内容压在固定头下方。
|
||||||
|
- `api-crypto.ts:49-50` 以 `content-length` 判定有无请求体,chunked/H2 下**静默跳过解密**并把 `{data:"<base64>"}` 交给 handler(`:93`),管理端保存即写入密文;`admin-api.ts:44-49` `catch {}` 在非安全上下文(`crypto.subtle` 不可用)**降级明文**;`:85` 将 `e.message` 回显客户端。密钥为 `NEXT_PUBLIC_*` 且盐硬编码 ⇒ 属混淆而非加密,无任何逻辑把它当鉴权用(此点正确)。
|
||||||
|
- `analytics.ts:178-188` `trackOutboundLink` 同时发 `outbound_click` 与 `click`,外跳统计**双计**;`:172` `value || 1` 吞掉 0;`:18-23` 默认 `analytics: true` 与 `:29-42` 无形状合并的 `JSON.parse` 可能违背用户实际同意。
|
||||||
|
- `use-focus-trap.ts:43-46` Escape 恒 `preventDefault` 并归还焦点却不通知持有者(焦点可逃出仍开启的 trap);`:13` 的 `[tabindex]:not([tabindex="-1"])` 只约束末项,`<button tabIndex={-1}>`/`<input type=hidden>` 被当可聚焦;`:64` 无条件 `overflow='unset'`,层叠弹窗互相解锁滚动。`use-keyboard-shortcuts.ts:34-36` `preventDefault()` 后调可能未接线的 `onSkipToContent` ⇒ **Tab 键对键盘用户死路**。
|
||||||
|
- `media-service.ts:113-117` 先删文件后删 DB 记录,DB 失败即留下指向已删文件的资产;`:109-111` 空 `catch {}` 使畸形 derivatives 的文件永不被删。`media/storage.ts:29` `fs.unlink` 未做 `resolve`+前缀校验(当前 `deleteMedia` 只接受库内路径)。
|
||||||
|
- 死代码(grep 复核零引用):`ui/loading-skeleton.tsx`、`cms/RichTextEditor.tsx`、`examples/ContactFormAnalyticsExample.tsx`、`content/testimonials.tsx`、`lib/gradients.ts`(+仅被它引用的 `lib/colors.ts`;且 `getGlowStyle` 把 `primary` 映射为调色板中不存在的蓝 `'0, 94, 184'`)、`ui/metric-card.tsx`、`ui/stats-showcase.tsx`、`sections/stats-bar.tsx`。汇总桶 `ui/index.ts`(201 行)、`sections/index.ts`、`layout/index.ts` **无任何 importer** ⇒ 经其可达的 `metric-card/stats-showcase/milestone-timeline/flip-clock/page-nav/list-page-hero/accordion/tabs/dialog/select` 全为死接线。这解释了 §4 中若干「渲染缺陷」实为潜伏缺陷。
|
||||||
|
- `products/[id]/page.tsx:9-14`(及 `solutions/[id]`、`services/[id]`、`news/[slug]`)同时声明 `generateStaticParams` 与 `export const dynamic='force-dynamic'` —— 后者胜出 ⇒ 前者是死码,且这 4 类详情页退出全站 `revalidate=3600` ISR 策略。`next.config.mjs:4` 实为 `output:'standalone'`,而 `:10` 注释仍以「静态导出限制」为 `unoptimized:true` 辩护,AGENTS.md/CLAUDE.md 亦仍称静态导出。
|
||||||
|
- 配置/文档矛盾:`Dockerfile` 与 `docker-compose.yml` 把 `dist` 当静态 HTML 根拷贝、`Dockerfile.static` 拷贝**不存在的 `html/`** ⇒ 用基础 Dockerfile 部署即白屏,仅 `Dockerfile.prod`(`COPY dist/standalone` + `node server.js`)与配置模式相符。`next.config.mjs:36` `X-Frame-Options: DENY` 与 nginx `SAMEORIGIN`(`:40,124,140,182,194`)**双重且互斥**,并产生重复 CSP —— 这正是 `test:security:headers` 报 2 警告、且 `X-Frame-Options` 实际值为 `"DENY, SAMEORIGIN"` 的来由。Sentry 未用 `withSentryConfig` 包裹 ⇒ **不上传 source map**,线上堆栈为压缩态。`tsconfig.json` 仍排除已不存在的 `src/app/(marketing)/_archive`(AGENTS.md/CLAUDE.md 亦仍描述 `_archive/` 约定)。
|
||||||
|
- `data-server.ts:182-210` 只解析 `zi.itemId`、忽略 `ContentZoneItem.item`,且条目全部未发布的 zone 不写 key(返回 `undefined` 而非 `[]`)。`workflow.ts:94` 把 `submit`/`reject` 一律记为 `action:'update'`(驳回与编辑在审计日志中不可分),且 `'approve'|'archive'` 越出 `cms/types.ts:196` 联合类型,仅靠 `:50` 的 `as unknown as` 通过编译。
|
||||||
|
|
||||||
|
## 6. 已核实为正确(避免无谓返工)
|
||||||
|
|
||||||
|
`$queryRaw/$executeRaw` 应用层零使用(仅 Prisma 生成类型含);Prisma 写入无 `...body` 质量赋值;`media-service.ts:9` `path.basename` 先于 `storage.ts:19` `path.join`,遍历已消;通知路由按会话 `userId` 收口(`notifications.ts:80,90`),无 IDOR;`roles/route.ts:18,59` 正确限 `super_admin` 且 `:73` 保护该角色自身;`revalidate` fail-closed;cookie `HttpOnly; SameSite=Lax` + 条件 `Secure`;`auth.ts:10-15` 缺密钥即抛(无硬编码兜底);三个 `.env*` 均已 gitignore(`git ls-files` 仅 `.env.example` 占位)。
|
||||||
|
迁移无漂移(5 个 migration 与 `schema.prisma` 对齐,RBAC 表已在 `prisma/dev.db` 落地);seed 中 55 处 `basis` 一律取最弱 `'target'`、无一处声称 `'verified'`。
|
||||||
|
`verifyAccessTokenEdge`(`proxy.ts:34-67`)以 HMAC 重算比对,`alg:none` 无法伪造;`color-contrast.ts:21-28` WCAG sRGB 亮度与 0.03928 阈值正确;`use-reduced-motion.ts`、`animated-counter.tsx:59`、`stats-showcase.tsx:48` 等 observer/listener 清理齐备;`theme-toggle.tsx` 是 SSR 安全实现范本;`static-link.tsx` 外链 `rel="noopener noreferrer"` 正确;`footer.tsx:195` 暗色对比度实测 **7.54:1**(审计 P1-2 确已修复);`webpackBuildWorker` 未出现在 `experimental`(符合项目铁律)。
|
||||||
|
Jest 侧无 `.only` / `.skip` / `.todo` / `xit` / 盲写快照;`playwright.config.ts` `forbidOnly:!!CI`、`retries: CI?2:0` 配置正确,仅 2 处合理的按浏览器条件跳过。
|
||||||
|
**性能预算实测达标且余量充足(7/7 页,desktop preset,`lighthouserc.json` 全部 URL)**:performance **99-100**、FCP **246-250ms**(预算 ≤2000)、LCP **790-896ms**(预算 ≤3000)、CLS **0.000**(预算 ≤0.1)、TBT **0ms**(预算 ≤300)、SI **248-414ms**(预算 ≤3000)、best-practices **100**、SEO **100**。`lighthouserc.json` 的 6 项性能断言 + 4 项分类断言**零违规**,且本分支未引入性能退化 —— 这是少数几个「AGENTS.md §5 声称的门禁经实测确实成立」的项。故上表把 Lighthouse 判为「门禁太松」仅指 **accessibility 子项的 0.9 阈值容下了 critical 失败**(A-11-2),性能维度本身可信。
|
||||||
|
|
||||||
|
## 7. 复核中主动撤回的判断(保持报告可信)
|
||||||
|
|
||||||
|
1. ~~「首页服务区忽略 CMS,恒渲染兜底数据」~~ —— 我据 `content-types.ts:88` 的 `serviceFields` 未声明 `subtitle/highlights/href` 推断该假设,随后以三重证据否证:`prisma/dev.db` 的 service 行实测**含** `subtitle/href/highlights(4)/metrics`,且首页 HTML 中 CMS 文案(「战略咨询」「数字化成熟度评估」)命中 3/2/2 次、兜底专属串(「Strategy Consulting」「数字化转型战略咨询」)命中 **0** 次。CMS 通路正常。
|
||||||
|
2. ~~「seed 指标缺 `basis`,违反结构强制」~~ —— `content-types.ts:8-18` 的 `metricBasisField.description` 明示「留空按目标口径处理并自动标注」,`metrics-basis.ts:33-38` 亦为刻意保守回落。这是设计而非缺陷。
|
||||||
|
3. ~~「`prisma/dev.db` 与 schema 漂移,缺 RBAC 表」~~ —— 我误查了仓库根的**陈旧残留** `./dev.db`(Jul 6) 与 0 字节 `./data.db`;真实库 `.env:8` 指向 `prisma/dev.db`,RBAC 四表与 5 个 migration 俱在。
|
||||||
|
4. ~~「`Novalon 创始团队` 是品牌一致性回归」~~(6 例 E2E 失败的定性)—— `CONTEXT.md:194`(✅ 2026-08-31 确认)在「零编造」条款下**明确批准** FounderQuote 兜底署名「Novalon 创始团队」,`prisma/seed.ts:961`、`home-content-v15.tsx:115` 与单测 `home-content-v15.test.tsx:181` 一致。故 `p1-brand-visual-audit.spec.ts:30` 的 `FORBIDDEN_TEXT=/novalon/i` 前提已过期,应改测试而非改文案。
|
||||||
|
5. `stats-bar.tsx:88` `parseInt("99.9")→99`(99.9% 永久显示 99%)与 `:71` 运行期插值类 `lg:grid-cols-${items.length}`(Tailwind 不生成,布局静默停在 `md:grid-cols-3`)、`animated-counter.tsx:62` 先显终值再跳 0 起算 —— 缺陷成立但**当前不可达**(组件经无 importer 的死汇总桶暴露,§5 死代码清单);列为修复时必须一并删除的死码,而非线上问题。`animated-counter.test.tsx:5-12` mock `useCountUp→500` 且 `value={500}`,两分支同值故永不能观测此问题。
|
||||||
|
6. ~~「两份 jest 配置测试发现不一致,变异门禁只看到 42% 的用例」~~ —— 我据 Stryker dry-run 报 "Ran 668 tests"(而 `test:unit` 为 1594)提出该怀疑,实测 `npx jest --config config/test/jest.config.js` 得 **132 套件 / 1594 例全通过**,两配置 `testMatch`/`roots`/`setupFilesAfterEnv` 完全一致 ⇒ 不成立。同时纠正了对 Stryker 报告的误读:其 "All tests" 树中的 **✘ 标记含义是「该测试未覆盖当前变异体」(后缀 `(covered 0)`),不是测试失败**(✓=killed、~=covered 但未杀)。真实差异只有阈值数值不同,已归入 A-7。
|
||||||
|
7. ~~「Stryker 报告的 3 个存活变异体全为测试缺陷」~~ —— 其中 `utils.ts:25` `if (timeout)`→`if (true)` 因 `clearTimeout(null)` 本身是 no-op 而**语义等价**,存活属正常。仅 A-13、A-14 两项计为缺陷。
|
||||||
|
|
||||||
|
## 8. 修复优先级与放行条件
|
||||||
|
|
||||||
|
| 顺序 | 动作 | 理由 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | A-1 + A-2 角色 allowlist 与改密鉴权;A-3 渲染端净化;A-4 上传白名单 + `/uploads/` 头 | 生产可利用,且 A-1→A-3/A-4 构成完整提权-存储 XSS 链 |
|
||||||
|
| 2 | A-5 摘掉 Jenkins `|| echo` 并补齐缺失 stage;A-10 E2E 改打构建产物 | 先让门禁**可信**,否则后续一切绿灯无意义 |
|
||||||
|
| 3 | **A-12 + R-3 + B-9 一并处理:67 处 `text-[var(--color-brand)]` → `text-brand-ink`,并新增 grep 门禁禁该模式** | 全站 62 页的确定性合规失败,且已有明文契约(DESIGN.md:154/232)背书;逐点修必然漏 |
|
||||||
|
| 4 | R-1 双安全区;R-2 动效回到 280ms;R-5 `MotionConfig`;B-8 BentoGrid;B-10 下拉 Escape | 用户可见 / 本分支核心意图 |
|
||||||
|
| 5 | A-6 GA4 重写为真实拦截;A-9 基线在干净参照点重采 + 删 9 张近空白基线;**A-11 `check:contrast` 改为读令牌表并补暗色/alpha 组,Lighthouse a11y 断言由 0.9 改为「critical 失败数 = 0」** | 恢复测试信号有效性;否则第 3 步修完仍无守卫 |
|
||||||
|
| 6 | A-7 阈值棘轮 + 三文档统一;A-8 加真库集成层;B-1/B-5 写入校验与 `$transaction`+WAL | 结构性 |
|
||||||
|
| 7 | B-2/B-6/B-7、§4.2 其余、§5 P2、死码清理 | 常规 |
|
||||||
|
| 8 | **A-13/A-14 补 `lerp` 非零 `start` 用例与 `randomBetween` 分布断言;A-15 把 `src/lib/constants/**` 从 `stryker.config.json` 的 `mutate` 排除中移出;`test:mutation` 去 `--inPlace`** | 变异门禁是本次唯一直接度量「断言是否有内容」的手段,且已实测可在 74s 内跑完单文件,成本极低 |
|
||||||
|
|
||||||
|
**放行条件(全部满足方可验收通过)**:①§3.1/§3.2 四项 P0 安全关闭并有回归测试;②E2E 92 → 0(若按 §7-4 判定品牌测试过期,须同时修订 `CONTEXT.md` 或测试并在提交信息留痕);③Jenkins 无 `|| echo` 后连续两次全绿;④AGENTS.md §5 列出的 lighthouse / contrast / headings 至少各执行一次并留结果;⑤A-12 全站对比度关闭后,以两个引擎各复跑一次并留 axe 节点计数 = 0 的证据;⑥R-1/R-3 在真机(iPhone SE + 系统深色)截图复核。
|
||||||
|
|
||||||
|
> 注:本次性能门禁的 0 违规**不构成本项豁免**——性能维度已实测达标(§6),但 a11y 维度是「断言通过而实际失败」,二者性质不同。
|
||||||
|
|
||||||
|
## 9. 本次未覆盖(诚实标注)
|
||||||
|
|
||||||
|
- **未做真机/真浏览器人工走查**:R-1 双安全区、触摸目标 <44px 的具体元素,均由构建产物 CSS、axe 节点选择器与 HTML 计数推断,未经肉眼截图确认。(A-12 / B-8 例外 —— 二者已由 axe 引擎在真浏览器中直接报出节点,非推断。)
|
||||||
|
- **`npm run lighthouse`(lhci autorun)未直接执行**,原因有两条,均为**独立缺陷**:
|
||||||
|
1. `config/test/lighthouserc.json` 配 `upload.target: "temporary-public-storage"` ⇒ 一旦运行即把含站点结构的结果上传至第三方公共存储(Lighthouse 官方示例报告库),对未发布站点属外泄风险。故本次改为逐 URL 调 `npx lighthouse@13` 复现同一断言集。建议改为 `upload: {target:'filesystem'}`。
|
||||||
|
2. lighthouserc 的 `startServerCommand: "npm run start"` **与 `output:'standalone'` 不兼容** —— 本次以 `next start` 启动 :3100 时 Next 自身告警 `"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.`。即 `package.json` 的 `start`/`preview` 与 lighthouse 的起服命令在 standalone 模式下**是半失效的**,与 §5 中 Dockerfile 仍假设静态导出同源。这解释了 Jenkins 为何从未跑 lighthouse —— 跑不起来。
|
||||||
|
- **k6 四个性能脚本未执行**(依赖为占位包),仓库内 `tests/performance/*-summary.json`(Aug 12)是手工产物而非 CI 结果。
|
||||||
|
- **变异测试仅跑了 `quick` 作用域(`src/lib/utils.ts`,34 变异体 / 74s)**;`stryker.config.json` 声明的全量作用域(`src/lib/**`、`src/hooks/**` 及 7 个组件目录)**未执行** —— 按单文件外推需数小时,且 `--inPlace` 在全量下会长时间改写整个工作树,不宜在验收轮次内冒险。故 §3.5 的 91.18% **只代表 utils.ts 一个文件**,不可作为全仓变异得分引用。
|
||||||
|
- **渗透测试 / 授权验证矩阵**:仅静态审计 A-1/A-2/B-1,未真实构造请求验证提权链可用性。
|
||||||
|
- **Lighthouse 仅测 desktop preset、每 URL 单次运行**(`lighthouserc.json` 配 `numberOfRuns: 3`);未测移动网络节流下的性能,也未做 3 次取中位以消除抖动。
|
||||||
|
- **运维观察(实测泄漏,已在本报告提交前清理)**:`scripts/utils/check-heading-hierarchy.ts:47` 与 `scripts/accessibility-test.js:31` 均以 `spawn('npm', ['run','preview'])` 起服,而 `accessibility-test.js:7` 的自述是「扫描完成后关闭服务」。实测本次跑完 `check:headings` 后,:3000 上仍残留一个 PPID=**1** 的 `next-server (v16.3.0)`(父为已 orphan 的 `npm run preview`)—— 因为脚本终止的是 `npm` 包装进程,真正 LISTEN 的 `next-server` 孙进程被 init 收养而继续占端口,且存活 **39 分钟**。这不是环境噪声而是**门禁脚本的进程回收缺陷**:它与 A-10 的 `reuseExistingServer: true` 叠加后,会让后续 E2E / Lighthouse 静默复用一个陈旧构建的服务,从而使「测的是当前代码」这一前提失效。
|
||||||
|
- 附带发现:`CLAUDE.md:15` 称 `npm run preview` 是「Serve dist/ on port 3000 (npx serve)」,实际 `package.json` 为 `next start -p 3000`;`CLAUDE.md:197` 称 Playwright「Auto-starts `npm run preview`」,实际 `playwright.config.ts:130` 用 `npm run dev`。两处文档与实现相反,属 §5 配置/文档矛盾族。
|
||||||
|
- 仍有 2 处歧义未能闭合:`mega-dropdown` 在 `md:`(768px) 的 `w-[640px]` 是否于真实平板宽度溢出;`useFocusTrap` 的 `offsetParent` 过滤对 `position:fixed` 抽屉是否如预期工作。
|
||||||
@@ -12,7 +12,25 @@ npm run dev:clean # Clean .next/dist then start dev server
|
|||||||
# Build & Preview
|
# Build & Preview
|
||||||
npm run build # Build production files to dist/
|
npm run build # Build production files to dist/
|
||||||
npm run build:clean # Clean then build
|
npm run build:clean # Clean then build
|
||||||
npm run preview # Serve dist/ on port 3000 (npx serve)
|
npm run start # next start -p 3000 — serve the built output
|
||||||
|
npm run preview # ALIAS of `start` (identical `next start -p 3000`); it is NOT `npx serve`
|
||||||
|
|
||||||
|
# 说明(2026-09-23 文档同步轮核实):`next start` 与 `output: 'standalone'` 组合下 Next 16 会打印
|
||||||
|
# `"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js"
|
||||||
|
# instead.`(实测见 `docs/acceptance/2026-09-21-gates/ga4-production-run.txt`;抛出点
|
||||||
|
# `node_modules/next/dist/server/next.js` 的 `getServer()`)。它是 **warn 而非 throw**——服务照常起来,
|
||||||
|
# 但走的是不受支持的降级路径,所以门禁脚本正逐步脱离 `next start`:`test:e2e:prod` 已改为直接
|
||||||
|
# `node dist/standalone/server.js`(`e2e/playwright.config.ts:132-135`);`check:headings` 也已脱离
|
||||||
|
# `next start` —— `scripts/utils/check-heading-hierarchy.ts:55-66` 现为「存在 dist/standalone/server.js 就直接
|
||||||
|
# 起 standalone(注入 HOSTNAME/PORT/NODE_ENV),仅当产物缺失才回退 `npm run preview` 并打印告警」,
|
||||||
|
# 见验收报告 N-24③。至此门禁脚本不再依赖不受支持的 `next start` 路径。
|
||||||
|
# standalone 的受支持启动方式是 `node dist/standalone/server.js`,且服务启动前 `public/`
|
||||||
|
# 与 `dist/static` 必须已在 standalone 根目录下(server.js 启动时缓存静态索引,缺资源会让
|
||||||
|
# /_next/static/** 全 404)。**这一步由 `package.json:9` 的 `postbuild` 自动完成**(`npm run build`
|
||||||
|
# 结尾 `rm -rf` + `cp -R dist/static → dist/standalone/dist/static`、`cp -R public → dist/standalone/public`),
|
||||||
|
# 所以正常路径下"先 build 再起 standalone"即可,不必手工拷;仅当产物来自别处或要复用旧 dist 时才参照
|
||||||
|
# `Jenkinsfile:353-355`(axe 阶段复用上一阶段产物时的同套拷贝)与 `scripts/deploy.sh:162-175`
|
||||||
|
# (发布侧把 `public/` 与 `dist/static → dist/_next/static` 同步进 Nginx 站点根)。npm 侧没有包一层的启动脚本。
|
||||||
|
|
||||||
# Deploy (统一发布脚本)
|
# Deploy (统一发布脚本)
|
||||||
./scripts/deploy.sh build # 构建静态产物
|
./scripts/deploy.sh build # 构建静态产物
|
||||||
@@ -22,27 +40,61 @@ npm run preview # Serve dist/ on port 3000 (npx serve)
|
|||||||
./scripts/deploy.sh status # 查看生产环境发布状态
|
./scripts/deploy.sh status # 查看生产环境发布状态
|
||||||
|
|
||||||
# Linting & Type Checking
|
# Linting & Type Checking
|
||||||
npm run lint # ESLint (configured in config/lint/.eslintrc.json)
|
npm run lint # ESLint flat config at repo root (`eslint.config.mjs`; `config/lint/.eslintrc.json` no longer exists)
|
||||||
npm run type-check # tsc --noEmit
|
npm run type-check # tsc --noEmit
|
||||||
|
|
||||||
# E2E Testing (Playwright)
|
# E2E Testing (Playwright)
|
||||||
npm run test # Run all E2E tests
|
npm run test # 全链路 E2E:先 test:functional(4 功能 project),再 test:visual:all(5 视觉 project),串行以免写库用例与截图并发污染基线
|
||||||
npm run test:e2e # Same as above
|
npm run test:functional # 仅功能 project(chromium / chromium-mobile / firefox / webkit)
|
||||||
|
npm run test:e2e # Same as test:functional
|
||||||
npm run test:smoke # Only @smoke-tagged tests
|
npm run test:smoke # Only @smoke-tagged tests
|
||||||
|
npm run test:e2e:prod # @smoke+@critical+@journey 打构建产物(E2E_TARGET=production → node dist/standalone/server.js,CI 门禁)
|
||||||
npm run test:visual # Visual regression (Desktop Chrome)
|
npm run test:visual # Visual regression (Desktop Chrome)
|
||||||
npm run test:visual:all # Visual regression (all projects)
|
npm run test:visual:all # Visual regression (all projects)
|
||||||
npm run test:visual:update # Update visual snapshots
|
npm run test:visual:update # Update visual snapshots
|
||||||
npx playwright test --grep "test name" # Run a single E2E test by name
|
npx playwright test --grep "test name" # Run a single E2E test by name
|
||||||
|
|
||||||
|
# ⚠ E2E 目标口径(默认跑的是 dev server,不是产物)
|
||||||
|
# `npm run test` / `test:functional` 的 webServer 是 `npm run dev`(`e2e/playwright.config.ts:132-135`),
|
||||||
|
# 因此 **16 个 @critical GA4 用例(TC-GA4-001..004 × 4 project)在这一轮里是 skipped,不是 passed**:
|
||||||
|
# `NEXT_PUBLIC_GA_MEASUREMENT_ID` 只写在 `.env.production`,dev 读不到 → `GoogleAnalytics.tsx:81,119`
|
||||||
|
# 提前 return null → 用例内 `test.skip(measurementId === null, …)` 触发。证据:
|
||||||
|
# `docs/acceptance/2026-09-21-gates/skipped-tests-final-tree.json`(28 skipped 中该组恰为 16)。
|
||||||
|
# 它们真正断言的唯一入口是 `npm run test:e2e:prod`(`E2E_TARGET=production` → 直接
|
||||||
|
# `HOSTNAME=localhost PORT=3000 node dist/standalone/server.js`,非 `next start`;须先 `npm run build`
|
||||||
|
# 并把 `dist/static` + `public` 并入 standalone 根目录,见 Build & Preview 段的装配口径),
|
||||||
|
# 见同目录 `ga4-production-run.txt` 的 4 passed。故「`npm run test` 全绿」不等于 GA4 覆盖已验证。
|
||||||
|
|
||||||
# Unit Testing (Jest)
|
# Unit Testing (Jest)
|
||||||
npm run test:unit # Run all unit tests
|
npm run test:unit # Run all unit tests
|
||||||
npm run test:coverage # Coverage report (thresholds: 80% branches/functions/lines)
|
npm run test:coverage # Coverage report(阈值以 jest.config.js 的 coverageThreshold 为准;根配置 re-export config/test/jest.config.js 以免双份漂移)
|
||||||
npx jest --testPathPattern="button" # Run a single test file matching pattern
|
npx jest --testPathPattern="button" # Run a single test file matching pattern
|
||||||
|
|
||||||
# Quality Checks
|
# Quality Checks
|
||||||
npm run check:contrast # Color contrast audit
|
npm run check:a11y # 可访问性静态门禁伞(= check:contrast + check:headings + check:brand-token,已并入 test:all)
|
||||||
npm run check:headings # Heading hierarchy audit
|
npm run check:contrast # 令牌对比度审计(读 globals.css 令牌表,浅色+暗色双主题、含 alpha 组,缺令牌即红)
|
||||||
npm run lighthouse # Lighthouse CI performance audit
|
npm run check:headings # 标题层级审计(有 dist/standalone/server.js 时直起 standalone;产物缺失才回退 `npm run preview` 并打印告警 —— scripts/utils/check-heading-hierarchy.ts:52-67)
|
||||||
|
npm run check:brand-token # 品牌红文字必须走 text-brand-ink* 令牌(DESIGN.md 双通道红规则)
|
||||||
|
npm run check:motion # 动效契约门禁(N-29 收口):R1 令牌档位(instant 100ms / fast·normal 180–280ms,CONTEXT.md:76)、
|
||||||
|
# R2 时长 ≤700ms(CSS 声明、framer transition 对象的 duration、Tailwind duration-* 类、tailwind.config 的 animation;
|
||||||
|
# infinite 循环与 *-delay 豁免)、R3 transition 时长必须走 var(--transition-*)、
|
||||||
|
# R4 写死的 cubic-bezier 必须属于令牌层 --ease-*(允许集合从 CSS 解析,不硬编码)且 --ease-ink==[0.22,1,0.36,1]。
|
||||||
|
# 退出码 0 干净 / 1 违规 / 2 未能度量(空扫描、缺令牌文件)—— 空扫描不读作干净。
|
||||||
|
# ⚠ 当前树 23 处违规(EXIT=1),故**尚未**并入 check:a11y,详见 docs/development/quality-gates.md §5.1
|
||||||
|
npm run check:motion:test # 上述门禁的自证伪测试(19 例,双向:合规夹具判 0 + 违规夹具判对应规则红);
|
||||||
|
# 测试在 scripts/ 下,jest 默认 roots 只含 src,故该脚本显式传 --roots
|
||||||
|
npm run check:axe:routes # 全站路由清单(sitemap ∪ 预渲染产物 ∪ 站内链接 BFS)→ /tmp/axe-routes.xml
|
||||||
|
# 同样需要 :3100 的生产服务(SEED 默认取 ${BASE}/sitemap.xml)
|
||||||
|
npm run check:axe # 双引擎(chromium/firefox)×双主题(light/dark)逐页 axe 节点计数 + 三条规则级
|
||||||
|
# 规则(autocomplete-valid/presentation-role-conflict/svg-img-alt)的分母断言;
|
||||||
|
# 退出码 0 通过 / 1 判红 / 2 清单缺失或 0 条(假绿熔断)。证据落
|
||||||
|
# docs/acceptance/2026-09-21-axe/axe-evidence.json。需一个生产服务在 :3100:
|
||||||
|
# PORT=3100 HOSTNAME=127.0.0.1 node dist/standalone/server.js(先拷 dist/static 与 public,见上文)
|
||||||
|
# CI 侧由 Jenkinsfile「♿♿ 全站 axe 节点计数」阶段(仅 main)自动串起
|
||||||
|
npm run test:all # type-check + lint + test:coverage + test:integration:real + check:a11y + test:e2e:fast + test:security:headers
|
||||||
|
npm run test:e2e:prod # E2E_TARGET=production(构建产物目标);harness **不代跑构建**,须先 `npm run build`
|
||||||
|
# (standalone 根目录的 dist/static + public 由 postbuild 自动装配,见上文)
|
||||||
|
npm run lighthouse # Lighthouse CI(配置 config/test/lighthouserc.json;axe critical/serious 逐条 minScore:1;inspector-issues 亦判红——CSP/弃用类问题只走 DevTools issue 通道,errors-in-console 与 0.9 分类阈值都看不见它;报告落 lighthouse-reports/)
|
||||||
|
|
||||||
# Database (Prisma + SQLite)
|
# Database (Prisma + SQLite)
|
||||||
npm run db:seed # Seed the dev database
|
npm run db:seed # Seed the dev database
|
||||||
@@ -52,7 +104,8 @@ npm run db:reset # Reset database with migrations
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Tech Stack
|
### Tech Stack
|
||||||
- **Next.js 14** (App Router) with **hybrid rendering** — static pages + API routes (Note: `output: 'export'` was recently removed; the project is transitioning away from pure static export)
|
- **Next.js 16.3**(App Router)混合渲染 — SSG/ISR 静态页 + API routes;`output: 'standalone'`(`next.config.mjs:44`)、`distDir: 'dist'`(`:46`)。standalone 入口是 `node dist/standalone/server.js`,需另拷 `dist/static` 与 `public`;`npm run start`(= `next start`)在 standalone 下只算不受支持的降级路径,见下文 Build Output
|
||||||
|
- **中间件命名为 `proxy.ts`**(Next 16 起 `middleware.ts` 更名);其 matcher 排除 `/api/*`,因此每个 API 路由必须自行做鉴权
|
||||||
- **React 18**, **TypeScript 5** (strict mode with `noUncheckedIndexedAccess`)
|
- **React 18**, **TypeScript 5** (strict mode with `noUncheckedIndexedAccess`)
|
||||||
- **Tailwind CSS 3** with design tokens exposed as CSS custom properties (all tokenized via `var()` references in `tailwind.config.js`)
|
- **Tailwind CSS 3** with design tokens exposed as CSS custom properties (all tokenized via `var()` references in `tailwind.config.js`)
|
||||||
- **Framer Motion** for animations, **Lucide React** for icons, **Zod** for validation
|
- **Framer Motion** for animations, **Lucide React** for icons, **Zod** for validation
|
||||||
@@ -73,7 +126,7 @@ src/app/
|
|||||||
├── layout.tsx # Root layout: fonts, metadata, theme, analytics, SEO schemas
|
├── layout.tsx # Root layout: fonts, metadata, theme, analytics, SEO schemas
|
||||||
├── (marketing)/ # Route group — all public marketing pages
|
├── (marketing)/ # Route group — all public marketing pages
|
||||||
│ ├── layout.tsx # Shared: Header + Footer + PageTransition + ErrorBoundary
|
│ ├── layout.tsx # Shared: Header + Footer + PageTransition + ErrorBoundary
|
||||||
│ ├── page.tsx # Home (delegates to home-content-cms.tsx)
|
│ ├── page.tsx # Home: server component, CMS zones via `lib/cms/data-server` → `home-content-v15.tsx`
|
||||||
│ ├── about/ # About page (client.tsx)
|
│ ├── about/ # About page (client.tsx)
|
||||||
│ ├── news/ # News list + [slug] detail
|
│ ├── news/ # News list + [slug] detail
|
||||||
│ ├── contact/ # Contact form
|
│ ├── contact/ # Contact form
|
||||||
@@ -132,13 +185,14 @@ The detail components implementing this live in `src/components/detail/`:
|
|||||||
- `detail-cross-recommend.tsx` (cross-links between products↔solutions↔services)
|
- `detail-cross-recommend.tsx` (cross-links between products↔solutions↔services)
|
||||||
|
|
||||||
### CMS Content Architecture
|
### CMS Content Architecture
|
||||||
The app has a **mock CMS layer** at `src/lib/cms/` that simulates a headless CMS (no real backend — designed to be swapped with real API calls later):
|
`src/lib/cms/` is a **real, server-side CMS layer over Prisma** — there is no mock layer and no client-side CMS SDK (the historical names `mock-home.ts`, `ContentZoneRenderer.tsx`, `component-registry.ts`, `src/components/cms/` do not exist in the tree; do not look for them):
|
||||||
- `types.ts` — ContentModel, ContentItem, ContentZone, ThemeConfig definitions
|
- `types.ts` — `FieldType`/`FieldDefinition`, `ContentModel`, `ContentStatus`, `ContentItem`, `ContentZone`, `ThemeConfig` definitions
|
||||||
- `ContentZoneRenderer.tsx` — Renders a zone's items in grid/list/carousel layouts, delegating each item to `getItemRenderer(modelCode)` from the component registry
|
- `content-types.ts` — `CONTENT_TYPE_CONFIGS` + `registerAllContentTypes()`: the per-model field schemas the admin form and the validator both read
|
||||||
- `mock-home.ts` — Mock data for the homepage zones
|
- `data-server.ts` — the read path: `getPublishedItems` (`:37`), `getPublishedItemBySlug` (`:48`), `getPageZones` (`:70`), `getZone` (`:85`), `getHomePageCopy` (`:171`), each wrapped in React `cache` so a render pass hits the DB once
|
||||||
- `component-registry.ts` — Maps model codes to React renderer components
|
- `workflow.ts` / `validate-content-data.ts` / `notifications.ts` — status state machine, field validation, in-app notifications (status changes only through `workflow.ts`; see `docs/cms/api-contract.md` §10.5)
|
||||||
|
- `index.ts` — re-exports the types plus `CONTENT_TYPE_CONFIGS`
|
||||||
|
|
||||||
The homepage (`home-content-cms.tsx`) initializes the CMS and fetches mock content zones (hero, stats, services, solutions, cases, news).
|
The homepage is `src/app/(marketing)/page.tsx` (server component, `export const revalidate = 3600` at `:5`): it resolves the home zones with `getResolvedHomeZones()` (`:8`) and, when the CMS has nothing configured, falls back to per-model `getPublishedItems(...)` calls (`:23-29`); the markup lives in `home-content-v15.tsx`. Earlier documents calling it `home-content-cms.tsx` are stale.
|
||||||
|
|
||||||
### Design Token System
|
### Design Token System
|
||||||
All visual tokens are defined as **CSS custom properties** in `src/app/globals.css` (`:root` block) and mapped into Tailwind's config via `var()` references:
|
All visual tokens are defined as **CSS custom properties** in `src/app/globals.css` (`:root` block) and mapped into Tailwind's config via `var()` references:
|
||||||
@@ -147,6 +201,11 @@ All visual tokens are defined as **CSS custom properties** in `src/app/globals.c
|
|||||||
- **Spacing, radius, shadows** all tokenized through CSS variables
|
- **Spacing, radius, shadows** all tokenized through CSS variables
|
||||||
- **Transitions**: `--transition-fast/normal/slow`, `--ease-ink` (cubic-bezier), `--ease-sharp`(`--ease-spring*` 死令牌已于 2026-09-19 polish 删除)
|
- **Transitions**: `--transition-fast/normal/slow`, `--ease-ink` (cubic-bezier), `--ease-sharp`(`--ease-spring*` 死令牌已于 2026-09-19 polish 删除)
|
||||||
- **Dark mode**: `data-theme="dark"` attribute (set via inline script before paint to prevent flash); flipped by the `[data-theme='dark']` override block in `globals.css`, which only re-maps CSS variables — components do not restyle per element
|
- **Dark mode**: `data-theme="dark"` attribute (set via inline script before paint to prevent flash); flipped by the `[data-theme='dark']` override block in `globals.css`, which only re-maps CSS variables — components do not restyle per element
|
||||||
|
- **Opacity**: use theme tokens (`bg-brand/20`, `border-ink/30`) — they resolve through `rgb(var(--x-rgb) / <alpha-value>)`. The arbitrary-value form `bg-[var(--color-brand)]/20` is **accepted by the parser but emits no CSS** (Tailwind v3 cannot apply an alpha modifier to a plain-hex custom property; see `tailwind.config.js:12-15`), so it fails silently. Verify any "is this style live?" claim against the compiled stylesheet, not against the source class list.
|
||||||
|
- **Two-channel red**: `--color-brand` (#C41E3A) is for surfaces only and does not flip in dark mode; text must use `text-brand-ink` (flips to #F87171). Enforced by `scripts/utils/check-brand-text-token.ts` on the text channel.
|
||||||
|
- **Hover / translucent states are unguarded**: axe skips `:hover` and `check:contrast` only asserts declared token *pairs*, so a hover background nobody noticed was dead becomes a live AA failure the moment you fix it. Composite it yourself — a semi-transparent layer **replaces** the element's own base color: `alpha*brand + (1-alpha)*pageBg`; at 20% that yields 4.18:1 against `text-brand-ink` in light theme (fail), at the `brand-soft` 12% token 4.80:1 (pass).
|
||||||
|
|
||||||
|
- **Motion tokens are gated, and the gate is currently RED**: `scripts/utils/check-motion-constraints.ts` (`npm run check:motion`, 配套 `npm run check:motion:test`) enforces the `CONTEXT.md` §动效设计四原则 bands (entrance 180–280ms, ≤700ms ceiling, durations must flow through `--transition-*` tokens, bezier allow-list **parsed from the CSS `--ease-*` tokens** — `--ease-out` is an alias of `ease-ink`, and the token layer deliberately carries 5 further curves). It is deliberately **not** part of `check:a11y`: the tree measures 23 violations / 13 files, so wiring it in would make the aggregate permanently red. `scripts/**` is eslint-ignored, so adding it did not move the lint baseline.
|
||||||
|
|
||||||
### Design DNA Framework
|
### Design DNA Framework
|
||||||
The site follows a three-dimensional design system (see `CONTEXT.md`):
|
The site follows a three-dimensional design system (see `CONTEXT.md`):
|
||||||
@@ -158,10 +217,10 @@ The **Consulting Professional** aesthetic (inspired by Accenture + Bain) is the
|
|||||||
|
|
||||||
**Brand red (#C41E3A) usage rule**: Every page must have ≥3 brand-red touchpoints. It must never be used as paragraph text color, as a large background, or alongside accent colors in the same card. Area ≤10%. Exception: full-bleed dark-red statement/CTA blocks use the separate `crimson-veil` (#8B1530) token — see DESIGN.md「Crimson Veil」(rule clarified 2026-09-19 critique to resolve this apparent contradiction).
|
**Brand red (#C41E3A) usage rule**: Every page must have ≥3 brand-red touchpoints. It must never be used as paragraph text color, as a large background, or alongside accent colors in the same card. Area ≤10%. Exception: full-bleed dark-red statement/CTA blocks use the separate `crimson-veil` (#8B1530) token — see DESIGN.md「Crimson Veil」(rule clarified 2026-09-19 critique to resolve this apparent contradiction).
|
||||||
|
|
||||||
**Motion design**: Animations are purposeful (not decorative), fast (150-300ms), use `ease-ink` [0.22, 1, 0.36, 1] as default easing, and stagger children by 30-60ms. No continuous looping animations except pulse-soft for skeletons. No spring easings for content entry. No animations >700ms.
|
**Motion design**: Animations are purposeful (not decorative). Durations come from tokens, **not** from a free-form "150-300ms" range — that phrasing conflicted with `CONTEXT.md`「动效设计四原则」and is replaced by its three tiers: **入场 180–280ms**(`--transition-fast: 180ms` … `--transition-normal: 280ms`,`src/app/globals.css:264-265`)· **hover 150ms** · **反馈 100ms**;`--transition-slow: 450ms` / `--transition-slower: 700ms` 只用于揭示型动效。`ease-ink` `[0.22, 1, 0.36, 1]`(`globals.css:280`)为默认缓动,children stagger 30-60ms、Section 间 stagger 100-150ms。No continuous looping animations except `pulse-soft` for skeletons. No spring easings for content entry(spring 仅用于按钮按压反馈). **No entry animations >700ms.** Note that `duration-300` 一类的 Tailwind 预设时长绕过令牌、且 300ms 不属任何档位(残留清点见 `CONTEXT.md`「duration-300 → 动效令牌」行,该项 in flight,勿引用其计数)。
|
||||||
|
|
||||||
### Data Layer
|
### Data Layer
|
||||||
All structured content data is in `src/lib/constants/` as TypeScript constants — no API/database for marketing content:
|
Most structured marketing content is still `src/lib/constants/` TypeScript constants, and the pages render from them; on top of that, published CMS items now override specific slots — the home page's hero/services/cases/stats/news data comes from `lib/cms/data-server` (Prisma) and only falls back to constants when the CMS has nothing configured (`src/app/(marketing)/page.tsx:8-36`), and per-section copy comes from `getHomePageCopy()` with in-component fallbacks. "Marketing content has no database" is therefore no longer true:
|
||||||
- `products.ts` — 6 enterprise products (ERP, CRM, CMS, BI, SDS, OA) + standalone products (NovaVis)
|
- `products.ts` — 6 enterprise products (ERP, CRM, CMS, BI, SDS, OA) + standalone products (NovaVis)
|
||||||
- `services.ts`, `solutions.ts` — Service and solution definitions
|
- `services.ts`, `solutions.ts` — Service and solution definitions
|
||||||
- `cases.ts` — Case studies with industry filtering
|
- `cases.ts` — Case studies with industry filtering
|
||||||
@@ -176,7 +235,10 @@ Each product/solution/service implements the `Product`/`Solution`/`Service` inte
|
|||||||
The project has a backend layer for admin/auth/CMS functionality:
|
The project has a backend layer for admin/auth/CMS functionality:
|
||||||
- **Prisma** with SQLite (`prisma/dev.db`) for admin user data, auth, and CMS content management
|
- **Prisma** with SQLite (`prisma/dev.db`) for admin user data, auth, and CMS content management
|
||||||
- API routes at `src/app/api/admin/`, `auth/`, `cms/`, `contact/`
|
- API routes at `src/app/api/admin/`, `auth/`, `cms/`, `contact/`
|
||||||
- The marketing pages use mock data from `src/lib/cms/mock-home.ts`, but the CMS API routes suggest a real CMS backend is planned
|
- The marketing read path and the admin write path share this backend: pages fetch published items through `src/lib/cms/data-server.ts`, while `api/admin/*` writes drafts and `api/cms/*` (`draft/enable`, `draft/disable`, `revalidate`) handles preview cache invalidation
|
||||||
|
|
||||||
|
### Error Monitoring (Sentry)
|
||||||
|
`@sentry/nextjs@10` is wired through `src/instrumentation.ts` (server/edge, incl. `onRequestError`) and `src/instrumentation-client.ts` (client init + `onRouterTransitionStart`); `next.config.mjs` exports `withSentryConfig(nextConfig)`. **This Next 16 build defaults to Turbopack, which never loads the root `sentry.client.config.ts`** — client init therefore lives in `instrumentation-client.ts`, and the old file is kept as an empty stub solely to prevent a double `Sentry.init` on the `--webpack` path. Everything is gated on `NEXT_PUBLIC_SENTRY_DSN`: absent it, `Sentry.init` is skipped, both hooks no-op, **and the CSP is byte-identical** (`connect-src`/`worker-src` only gain the DSN's `protocol//host` when a valid DSN is present, `next.config.mjs:17-26`). Verify "is this inert?" by building without a DSN and diffing the emitted header, not by reading the source.
|
||||||
|
|
||||||
### Component Organization
|
### Component Organization
|
||||||
```
|
```
|
||||||
@@ -185,19 +247,26 @@ src/components/
|
|||||||
├── layout/ # Header, Footer, MobileTabBar, Breadcrumb, MegaDropdown(MobileMenu 已删除,抽屉内置于 Header)
|
├── layout/ # Header, Footer, MobileTabBar, Breadcrumb, MegaDropdown(MobileMenu 已删除,抽屉内置于 Header)
|
||||||
├── sections/ # Page section components: HeroSectionV2, ServiceGrid, CTASection, etc.
|
├── sections/ # Page section components: HeroSectionV2, ServiceGrid, CTASection, etc.
|
||||||
├── detail/ # Four-layer narrative: DetailHero, ProductValueSection, DetailTrustSection, etc.
|
├── detail/ # Four-layer narrative: DetailHero, ProductValueSection, DetailTrustSection, etc.
|
||||||
|
├── content/ # 仅 sections.tsx(+ 同名 .test.tsx)
|
||||||
|
├── admin/ # admin-layout.tsx、auth-context.tsx(React Context 在此,不在 src/contexts/)
|
||||||
|
├── theme/ # theme-toggle.tsx
|
||||||
├── seo/ # Structured data (OrganizationSchema, WebsiteSchema, etc.)
|
├── seo/ # Structured data (OrganizationSchema, WebsiteSchema, etc.)
|
||||||
├── analytics/ # GA4, error tracking, cookie consent, scroll depth, outbound links
|
└── analytics/ # GA4, error tracking, cookie consent, scroll depth, outbound links
|
||||||
├── cms/ # CMS renderers (ContentRenderer, SectionRenderer, FieldRenderer)
|
|
||||||
├── content/ # sections.tsx, testimonials.tsx
|
|
||||||
└── providers/ # (currently empty)
|
|
||||||
```
|
```
|
||||||
|
> 口径核对(2026-09-23,验收 N-33「不存在的目录」一项):`cms/`、`providers/`、`effects/` 三个目录在工作树与 `git ls-tree -r HEAD` 中**均不存在**,旧图里的对应条目是残留。`ContentRenderer` / `SectionRenderer` / `FieldRenderer` 三个标识符在 `src/**/*.{ts,tsx}` 中零命中,CMS 区块渲染实际由 `src/components/content/sections.tsx` + `src/lib/cms/` 承担;`effects/` 一族已在提交 `37296b5`(2026-05-10)删除,见 `CONTEXT.md`「特效组件」。
|
||||||
|
|
||||||
### Testing Setup
|
### Testing Setup
|
||||||
- **Jest** (unit): Tests alongside source in `src/**/*.test.{ts,tsx}`, coverage threshold 80%. Config in `config/test/jest.config.js`. Uses `@/` path alias and ts-jest with `jsx: 'react-jsx'` transform (since tsconfig uses `'preserve'`). Run single tests with `npx jest --testPathPattern="component-name"`.
|
- **Jest** (unit): Tests alongside source in `src/**/*.test.{ts,tsx}`, coverage **thresholds** live in `config/test/jest.config.js` (global 75% stmts/lines/funcs, 82% branches) and are the single source of truth — root `jest.config.js` only re-exports it, never duplicate thresholds. **Measured values are recorded in exactly one place: `docs/development/quality-gates.md` §3** (2026-09-23 本树复跑:134 suites / 1697 tests / EXIT=0);该文件顶部注释里的百分比是滞后快照,引用前先复跑。Uses `@/` path alias and ts-jest with `jsx: 'react-jsx'` transform (since tsconfig uses `'preserve'`). Run single tests with `npx jest --testPathPattern="component-name"`.
|
||||||
- **Playwright** (E2E): Tests in `e2e/`, config in `e2e/playwright.config.ts`. Auto-starts `npm run preview` as web server. Visual regression snapshots in `e2e/visual-snapshots/`. Three browser projects (chromium, firefox, webkit) plus dedicated visual regression projects at desktop/tablet/mobile breakpoints. Run single tests with `npx playwright test --grep "test name"`.
|
- **Playwright** (E2E): Tests in `e2e/`, config in `e2e/playwright.config.ts` — run from `cd e2e` (`npm run test` does this). webServer is `HOSTNAME=localhost PORT=3000 node dist/standalone/server.js` when `E2E_TARGET=production` (`e2e/playwright.config.ts:132-135` — build artifacts, real security headers; CI gate uses `npm run test:e2e:prod`, and `npm run build` + the `dist/static` / `public` copies must already exist because the harness never builds) and `npm run dev` otherwise; `reuseExistingServer` is disabled for production targets, so a busy :3000 aborts the run. Functional projects: chromium, chromium-mobile (iPhone 14, 390×844), firefox, webkit — plus 5 visual-regression projects at desktop/tablet/mobile breakpoints; snapshots in `e2e/visual-snapshots/`. `mobile-*.spec.ts` must pin their own viewport because they also execute under the desktop projects. Touch-target gate lives in `e2e/touch-targets.ts` (AA SC 2.5.8 ≥24px hard, AAA SC 2.5.5 44px advisory). Visual snapshots are server-target independent — every spec imports `test` from `e2e/fixtures.ts`, whose `context` fixture injects an init script that removes the `next dev`-only `<nextjs-portal>` devtools indicator (MutationObserver on `document`, because `documentElement` is still null at document start). The indicator's shadow-DOM `<footer class="error-overlay-footer">` is pierced by Playwright's CSS engine and made `locator('footer')` resolve to 2 elements (56 failures). `devIndicators: false` is *not* a fix here — measured: the CSP-blocked-`eval` dev error keeps the overlay mounted, so the portal survives; don't reintroduce that env gate. `visual-regression.spec.ts` additionally pins the cookie-consent state via `addInitScript` (constant timestamp) because the banner renders on a 2 s timer and the `visual-firefox-desktop` / `visual-webkit-desktop` projects use an empty storage state — without the pin, slower engines photograph the banner into the first viewport and faster ones don't (34 engine-specific failures). Client-component interactions must call `expectHydrated()` from `e2e/hydrated.ts` before hover/click — SSR HTML is visible ~1.7 s before React attaches handlers, and pointer events in that window are dropped without replay. Hover-driven UI additionally needs `expect(...).toPass()` re-driving the pointer, because a single `hover()` can land mid-layout-transition and `mouseenter` never re-fires. Run single tests with `npx playwright test --grep "test name"`.
|
||||||
|
|
||||||
### Build Output
|
### Build Output
|
||||||
The project builds to `dist/` (configured via `distDir` in `next.config.mjs`). It is served via Nginx (see `nginx-static-production.conf`) with CDN support (`assetPrefix` respects `CDN_DOMAIN` env var). Images are unoptimized (static export limitation). Note: `output: 'export'` was recently removed from the config — the project may be moving toward a hybrid SSR + static model.
|
The project builds to `dist/` (`distDir` in `next.config.mjs:46`) with **`output: 'standalone'`** (`next.config.mjs:44`) — this is a hybrid render model, *not* a static export (`output: 'export'` was dropped and the project has since moved on to standalone; `CONTEXT.md`「生产部署模式」records the 2026-08 switch). Concretely:
|
||||||
|
|
||||||
|
- **Prerendered/ISR**: the marketing pages, `privacy`, `terms`, `sitemap.ts`, `robots.ts`.
|
||||||
|
- **Runtime-backed**: `src/app/api/**` (20 route files — `admin/*`, `auth/*`, `cms/*`, `contact`), the `admin/` pages, and any ISR revalidation source. `proxy.ts` matchers exclude `/api/*`, so each API route authenticates itself.
|
||||||
|
- **Serving**: Nginx keeps a *disk copy* of the client static assets (`/_next/static/` with `try_files … @nextjs`, `nginx-static-production.conf:151-157`) plus `/uploads/` hardening, and proxies `/api/` (`:213`), `/admin` (`:226`) and everything else (`location /` `:238-240`, `@nextjs` `:249`) to the `nextjs_app` upstream running `dist/standalone/server.js`. Page HTML no longer has a static-file fast path — it was removed on purpose (`nginx-static-production.conf:20-25`), so "nginx serves the pages from `dist/`" is no longer true. `assetPrefix` is driven by `CDN_DOMAIN`. Container build is `Dockerfile.prod` + `docker-compose.server.yml`.
|
||||||
|
- **Images are unoptimized** (`next.config.mjs:48-61`, value at `:58`) because distribution goes through Nginx + CDN, and flipping it to `false` would require every environment to have a Node runtime taking over `/_next/image` (otherwise every image 404s). The reason is *deployment topology*, not a static-export limitation.
|
||||||
|
- **Boot command**: `npm run start` / `npm run preview` both run `next start -p 3000`, which Next 16 warns "does not work with output: standalone" (warning only — see the Build & Preview note above). The supported server is `dist/standalone/server.js`; `public/` and `dist/static` are copied into the standalone root **automatically by `package.json:9` 的 `postbuild`**,所以 `npm run build` 之后直接 `node dist/standalone/server.js` 即可。该启动方式仍没有包成 npm script —— CI 在 `Jenkinsfile:353-355` 复用产物时重做同套拷贝,发布侧由 `scripts/deploy.sh:162-175` 把 `public/` 与 `dist/static → dist/_next/static` 同步进 Nginx 站点根。
|
||||||
|
|
||||||
### Commit Convention
|
### Commit Convention
|
||||||
Uses Conventional Commits with commitlint (`@commitlint/config-conventional`). Husky + lint-staged enforces linting on pre-commit.
|
Uses Conventional Commits with commitlint (`@commitlint/config-conventional`). Husky + lint-staged enforces linting on pre-commit.
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ Novalon 的 6 个核心产品,互为互补关系,常以组合形式出现在
|
|||||||
当前状态:近期有规划,需在本次重构中预留完整的模板体系
|
当前状态:近期有规划,需在本次重构中预留完整的模板体系
|
||||||
|
|
||||||
### 特效组件 (Effects)
|
### 特效组件 (Effects)
|
||||||
`src/components/effects/` 目录下的 24 个视觉特效组件。当前状态:**大部分未被首页使用**,属于技术债务。
|
**该目录已不存在**(口径核对 2026-09-23):`src/components/effects/` 在提交 `37296b5`(2026-05-10「三轮视觉改造与页面过渡动画」)中被**整体删除 25 个文件**,`git ls-tree -r HEAD` 与该目录的 `ls` 均为空。本节原文("24 个视觉特效组件……大部分未被首页使用,属于技术债务")是**重构前的现状盘点**,只作历史保留,不得再当作可寻址的目录引用。动效能力现在的落点是 `src/components/ui/`(`scroll-reveal.tsx`、`animated-counter.tsx`、`brand-visuals.tsx`)与 `src/components/sections/`(`hero-particle-field.tsx` + 其引擎)。
|
||||||
|
|
||||||
### Design Tokens
|
### Design Tokens
|
||||||
`.impeccable.md` 中定义的设计令牌系统,包含颜色、排版、间距、卡片系统、Section 背景交替规则。当前状态:**文档已定义,代码中 globals.css 有对应 CSS 变量,但组件层未完全落地**。
|
`.impeccable.md` 中定义的设计令牌系统,包含颜色、排版、间距、卡片系统、Section 背景交替规则。当前状态:**文档已定义,代码中 globals.css 有对应 CSS 变量,但组件层未完全落地**。
|
||||||
@@ -191,7 +191,35 @@ Novalon 的 6 个核心产品,互为互补关系,常以组合形式出现在
|
|||||||
| 品牌主口号 | **智连未来 · 成长伙伴 —— 您的数字化转型同行者**(整句全站统一采用)。品牌叙事内核(定位声明 / 承诺 / L0 价值主张 / L1 三支柱 / 语气语调)见 `docs/brand-narrative-core.md`,作为全站文案统一唯一锚点 | ✅ 2026-08-19 确认 |
|
| 品牌主口号 | **智连未来 · 成长伙伴 —— 您的数字化转型同行者**(整句全站统一采用)。品牌叙事内核(定位声明 / 承诺 / L0 价值主张 / L1 三支柱 / 语气语调)见 `docs/brand-narrative-core.md`,作为全站文案统一唯一锚点 | ✅ 2026-08-19 确认 |
|
||||||
| 品牌叙事 CMS 化 | 品牌叙事内核全部下沉到 CMS `site-config` 模型(`slogan` / `valueProposition` / `positioningStatement` / `brandPromise` / `toneOfVoice` / `pillars`),后台可编辑;代码侧 `COMPANY_INFO` 与 `BRAND_NARRATIVE`(`src/lib/constants/company.ts`)作为未配置 CMS 时的兜底唯一真源,`layout.tsx` 读取后经 `SiteConfigProvider` 注入全站 | ✅ 2026-08-19 确认 |
|
| 品牌叙事 CMS 化 | 品牌叙事内核全部下沉到 CMS `site-config` 模型(`slogan` / `valueProposition` / `positioningStatement` / `brandPromise` / `toneOfVoice` / `pillars`),后台可编辑;代码侧 `COMPANY_INFO` 与 `BRAND_NARRATIVE`(`src/lib/constants/company.ts`)作为未配置 CMS 时的兜底唯一真源,`layout.tsx` 读取后经 `SiteConfigProvider` 注入全站 | ✅ 2026-08-19 确认 |
|
||||||
| 结构性文案 CMS 化 | 新增 `page-copy` 内容模型承载各业务页面章节标题/眉标/描述/CTA/空状态,覆盖首页 + 服务/方案/产品/案例/新闻列表页,seed 写入 6 条(home/services/solutions/products/cases/news)。实现为「CMS 优先 + 硬编码兜底」:CMS 未配置时回退 `TRUST_SIGNALS`/`EARLY_ACCESS`/`NARRATIVE_ACTS` 等常量,保证任何情况不白屏。数据层新增 `getPageCopy(pageCode)` / `getHomePageCopy()`,各 `page.tsx` 读取后以 `pageCopy` props 传入组件。详情页小节标题、页面内嵌业务常量(服务流程/合作模式/产品组合)、Header/Footer 等 UI 外壳按 UI 职责保留代码。数据实体(服务/产品/方案/案例/新闻/指标/Hero)仍由 `data-server` 从 Prisma 读取 | ✅ 2026-08-19 确认 |
|
| 结构性文案 CMS 化 | 新增 `page-copy` 内容模型承载各业务页面章节标题/眉标/描述/CTA/空状态,覆盖首页 + 服务/方案/产品/案例/新闻列表页,seed 写入 6 条(home/services/solutions/products/cases/news)。实现为「CMS 优先 + 硬编码兜底」:CMS 未配置时回退 `TRUST_SIGNALS`/`EARLY_ACCESS`/`NARRATIVE_ACTS` 等常量,保证任何情况不白屏。数据层新增 `getPageCopy(pageCode)` / `getHomePageCopy()`,各 `page.tsx` 读取后以 `pageCopy` props 传入组件。详情页小节标题、页面内嵌业务常量(服务流程/合作模式/产品组合)、Header/Footer 等 UI 外壳按 UI 职责保留代码。数据实体(服务/产品/方案/案例/新闻/指标/Hero)仍由 `data-server` 从 Prisma 读取 | ✅ 2026-08-19 确认 |
|
||||||
| 首页对标埃森哲重构(scene#16) | **视觉基调:局部深色 Hero(推荐项)**——保持浅色咨询风为底,仅 Hero 升级为全出血深色画布(近黑底 `bg-ink` + 白色 Logo `/logo-white.svg` + 品牌红 #C41E3A 单电压 ≤10%),制造埃森哲式戏剧感;**实施范围:Phase A+B 全量**——①内容支柱补齐:新增 Insights 行业洞察(2×2 gap-px 卡片网格 + 方法论/观点/共创类型标签)、FounderQuote 创始人观点(深红区块 + 白色点阵 + 大引言)、News 新闻动态(最近 3 条 + 查看全部)三个对标埃森哲思想领导力支柱的区块;②Hero 深色化;③CTA 签名符号:全站 Button 组件 `rounded-md` → `rounded-full`(Pill 胶囊化)+ ArrowRight 签名箭头。零编造内容原则:公司 2026-01-15 成立,Insights 兜底 4 条均为可验证方法论内容(不虚构研究报告/人名),FounderQuote 署名兜底「Novalon 创始团队」不虚构具体人名。实现:`home-content-v15.tsx` 替换 v14(9 区块顺序 Hero→Trust→Narrative→Insights→Services→FounderQuote→Cases→News→CTA),CMS 优先 + FALLBACK_INSIGHTS/NEWS 常量兜底;seed page-copy 新增 insights*/founderQuote*/news* 字段(upsert 幂等)。验证:tsc 0 errors / eslint 0 errors / Jest 128 套件 1621 通过 / Playwright 视觉回归 22 passed(macOS 11 限制下用 Chromium 117 兼容配置生成快照) | ✅ 2026-08-31 确认 |
|
| 首页对标埃森哲重构(scene#16) | **视觉基调:局部深色 Hero(推荐项)**——保持浅色咨询风为底,仅 Hero 升级为全出血深色画布(近黑底 `bg-ink` + 白色 Logo `/logo-white.svg` + 品牌红 #C41E3A 单电压 ≤10%),制造埃森哲式戏剧感;**实施范围:Phase A+B 全量**——①内容支柱补齐:新增 Insights 行业洞察(2×2 gap-px 卡片网格 + 方法论/观点/共创类型标签)、FounderQuote 创始人观点(深红区块 + 白色点阵 + 大引言)、News 新闻动态(最近 3 条 + 查看全部)三个对标埃森哲思想领导力支柱的区块;②Hero 深色化;③CTA 签名符号:全站 Button 组件 `rounded-md` → `rounded-full`(Pill 胶囊化)+ ArrowRight 签名箭头。零编造内容原则:公司 2026-01-15 成立,Insights 兜底 4 条均为可验证方法论内容(不虚构研究报告/人名),FounderQuote 署名兜底「Novalon 创始团队」不虚构具体人名。实现:`home-content-v15.tsx` 替换 v14(9 区块顺序 Hero→Trust→Narrative→Insights→Services→FounderQuote→Cases→News→CTA),CMS 优先 + FALLBACK_INSIGHTS/NEWS 常量兜底;seed page-copy 新增 insights*/founderQuote*/news* 字段(upsert 幂等)。验证:tsc 0 errors / eslint 0 errors / Jest 128 套件 1621 通过 / Playwright 视觉回归 22 passed(macOS 11 限制下用 Chromium 117 兼容配置生成快照) | ✅ 2026-08-31 确认(署名口径已被 2026-09-21 行修订)|
|
||||||
|
| FounderQuote 兜底署名改回法定主体名(修订上行) | 上行的「Novalon 创始团队」与 `p1-brand-visual-audit.spec.ts:30` 的 `FORBIDDEN_TEXT=/novalon/i` 冲突,且对外可见文案的署名应是法定主体「四川睿新致远科技有限公司」的简称而非英文品牌名。裁定:**改文案不改测试**——`FALLBACK_FOUNDER_QUOTE.name`(`home-content-v15.tsx:115`)与 `prisma/seed.ts:961` 同步为「睿新致远创始团队」,单测 `home-content-v15.test.tsx:181` 随动。DB 侧 `page-copy/home` 的 `founderQuoteName` 实测为空,页面走兜底分支,无需授权写库;`curl localhost:3000/` 复核渲染值为「睿新致远创始团队」且 0 处 "Novalon 创始团队"。零编造原则不变:仍不虚构具体人名 | ✅ 2026-09-21 确认(验收 §7-4 / 放行条件② 留痕)|
|
||||||
|
| 装饰大字对比度口径 | 装饰性大字号文字(序号、客户名首字占位)不享有对比度豁免:axe-core 4.11.4 的 `color-contrast` 只看 DOM 可见文字颜色,`aria-hidden` / `role="none"` / CSS `opacity` 均不豁免(变体矩阵实测,仅 SVG `<text>`、CSS mask、`::before` 等非文本编码可绕)。全站统一用大字档令牌 `text-text-hint`(#7C8CA5 / 深色 #94A3B8,对各级卡片底色 ≥3:1,满足 WCAG 1.4.3 大字 AA),不再用 `text-text-muted/10` 之类低 alpha 淡化;口径由 `scripts/utils/check-color-contrast.ts` 的「大号装饰文本 × 各级卡片底色」契约组固化 | ✅ 2026-09-21 确认(验收 A-12 清零)|
|
||||||
|
| 触摸目标口径 | 硬门禁 = WCAG 2.2 **AA** SC 2.5.8 **≥24×24px**(含 Spacing/Equivalent/Inline 例外,正文内联链接按 `display:inline` 排除);SC 2.5.5 的 44×44px 属 **AAA**,在 `e2e/touch-targets.ts` 中只作为建议清单打印、不使构建失败。此前测试把 44px 标为 AA 是标准档位错用 | ✅ 2026-09-21 确认 |
|
||||||
|
| GA4 SPA pageview 标题时序 | Next.js App Router 的 `<title>` 在路由 commit 之后 2–5 帧才写入(生产实测 chromium-mobile 第 2 帧、桌面 chromium 第 3–5 帧),因此 `GoogleAnalytics.tsx` 必须带帧预算(`MAX_TITLE_SETTLE_FRAMES = 10`)轮询等待标题变化后再 `gtag('config')`;超预算兜底发送(不丢计数),新导航前补发未送出的上一条 pageview(不送上一页标题)| ✅ 2026-09-21 确认(验收 A-17)|
|
||||||
|
| iPhone SE 复核口径(放行条件⑥) | 375×667 @2x + `colorScheme` 强制深/浅 × 同意条 pending/dismissed × 首页/新闻详情/新闻列表/联系页 = 16 组,证据(32 图 + `measurements.json` + 探针脚本)落在 `docs/acceptance/2026-09-21-iphone-se/`。实测:16/16 `scrollWidth == clientWidth == 375`(无横向溢出)、`footerBottomGap = 64`(验收 R-1 记录为 192–226 的双安全区空带)、滚到底后 `footerBottomVsViewport = -128` 一致;同意条在 pending 态以 `bannerTop=461` 覆盖页脚——这是 `fixed bottom-16` 浮层的设计行为,可关闭后消失。**边界**:这是浏览器仿真,不等同真机走查,真机确认仍需人工 | ✅ 2026-09-22 确认(验收 §8-⑥)|
|
||||||
|
| `-[var(--color-*)]/<alpha>` 死样式族的收口边界 | 编译产物实测:src 下 22 处 / 17 个唯一类在 CSS 中 0 产出(Tailwind v3 不能对纯 hex 变量套 alpha,见 `tailwind.config.js:12-15`)。按**渲染证据**分级后只修 2 处「活组件 + 仅 hover」:`NewsDetailClient.tsx:49` → `hover:bg-brand-soft`、`service-card.tsx:33` → `hover:border-brand/20`(hover 不入基线,零像素影响,已确认三条规则在编译 CSS 中产出)。注意点亮后的合成底色须重算对比度:`hover:bg-brand/20` 在浅色主题下使 `text-brand-ink` 降到 4.18:1(破 AA 4.5:1),且 axe 忽略 `:hover`、`check:contrast` 只断言令牌配对,两道门禁均测不到,故取 12% 的 `brand-soft`(浅色 4.80:1 / 深色 6.60:1)。**不做**:`/privacy`、`/terms` 首屏 `via-[var(--color-brand)]/80` 一旦被"点亮"会在已审批基线里画出半透明暗坑,属设计判断;3 处需先在 `globals.css` + config 补 `--color-bg-section-rgb` / `--color-brand-bg-rgb` 通道;其余 14 处位于无任何路由渲染的 barrel-only 死代码。是否扩 `check-brand-text-token.ts` 到全族由用户定范围 | ⏳ 2026-09-22 待裁定(残留项,不属验收 §8 六条放行条件)|
|
||||||
|
| 英文 wordmark 的受控口径(续上上行 §7-4) | 全仓唯一的字面 `NOVALON` 位于 `public/logo.svg:72`、`public/logo-white.svg:72`、`public/logo-calligraphy.svg:72` 的 `<text>` 元素,且各自行前注释 `<!-- NOVALON - 英文 -->` 自述为设计意图,三处 `<text>` 实测内容恰为 `NOVALON`(`letter-spacing="4"`,仅 `fill` 随底色不同:#0A0E14 / #FFFFFF)。提交 `8d3bd72` 另把书法人名统一到 header/footer/品牌页(新增 `BrandCalligraphyName`,改 `public/logo.svg`)。裁定:**wordmark 属图形资产内的受控内容,不是文案回归**;对外可见**文本**署名一律「睿新致远」(:195),`src` 下其余 `novalon` 串为技术上下文(`novalon.cn` 域名、`novalon_admin_token` 等存储键、PBKDF2 盐、管理端 `Novalon CMS` 界面)。据此把 `e2e/p1-brand-visual-audit.spec.ts` 从"零断言"改为三条真不变量:①两张 logo 资产经 `page.request.get` 取回后,每个 `<text>` 内容须恰为 `NOVALON`(改名、夹带或漏字即失败);②header/footer/首页的可见文本、`alt`、`title`、hover 后文本零 `novalon`(技术上下文除外);③凡"遍历后无命中即通过"的用例补 `expect(checked/hovered/withAlt).toBeGreaterThan(0)`,避免选择器失配伪装成绿灯。因 wordmark 以 `<img>` 加载、不成 DOM 文本,`:173-177` 的首页 Hero 豁免分支在生产 HTML 上当前不触发(改为内联 SVG 时会生效)——保留但不依赖。放行条件② 的「修订 `CONTEXT.md` 或测试」两条路径此处**同时**满足 | ✅ 2026-09-22 确认(验收 §7-4 / 放行条件②)|
|
||||||
|
| 边界页(404 / 根错误兜底)的可测口径 | ⑤ 的扫描分母从 `sitemap.xml`(29 条)扩为 **sitemap ∪ 预渲染产物 ∪ 站内链接 BFS = 35 条**后,首轮即 `passed=false`(`docs/acceptance/2026-09-21-axe/axe-evidence.json`,双引擎双主题共 8 条 bad rows):`/_not-found` 的 h1「404」用 `text-brand-ink` + **`opacity-20`** ⇒ 有效 alpha 20%,`color-contrast` 必破;`/_global-error` 因项目无 `global-error.tsx`,由 Next 内置文档壳顶替 root layout ⇒ `<html id="__next_error__">` 无 `lang`,且本站主题是 `html[data-theme='dark']` 属性驱动(`globals.css:396` 明示非 `.dark` class),内置壳读不到令牌,深色落到浏览器默认 `rgb(10,10,10)`。裁定:**边界页属全站范围,不豁免**——404 与根错误页是每个访客都可能落到的页面,且是唯一的"无 CMS 数据兜底路径"。修复:404 标题改 `text-brand-ink/80`(120px 属大号文本,AA 需 3:1;沿用 §5.22 `/products/erp-upgrade` 水印数字的同一口径与同一档位),新增 `src/app/global-error.tsx` 自带 `<html lang="zh-CN">` + `<title>`(错误边界是 Client Component,不支持 `metadata` 导出)+ 与 `globals.css` 同值的 OS 主题内联样式(CSP `style-src 'self' 'unsafe-inline'` 允许)。**约束**:①可见文本一律不得用 `opacity-*` 暗化,装饰性大字用可编译的主题令牌 + `/80` 档;②新增 `app/` 顶层边界文件须自带文档壳与主题样式,且本版本 Next 的 `global-error` props 是 **`retry`**(`node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/error.md`),不是旧 API 的 `reset`;③任何"全站 = 0"型门禁的分母必须是三源并集清单,`sitemap.xml` 只覆盖"希望被收录的页",单独使用即为口径错误。配套回归:`src/app/global-error.test.tsx`(3 例:`lang`、`retry` 接线、digest 展示) | ✅ 2026-09-22 修复并复扫通过:chain5/chain6 最终树 34 路由 × chromium/firefox × 深/浅 的 `contrastNodes / violationNodes / themeMismatch / bgMismatch` 全为 0、`rows=136`、`passed=true`(`docs/acceptance/2026-09-21-axe/axe-evidence.json`,详见 §5.24 与 `docs/acceptance/2026-09-21-gates/final-tree-results.md` ⑤)|
|
||||||
|
| refresh token 的轮换/吊销:本轮**有意延后**(需 schema 变更 = 需授权动作) | `src/app/api/auth/refresh/route.ts` 只验签名即用旧 payload 重签,意味着失窃的 refresh token 在到期前可无限换取 access token。**闭合它需要 `jti` / `tokenVersion` 一类的服务端状态,即 Prisma schema 迁移**——按 `AGENTS.md` §5.1「破坏性/共享状态动作须单独取得授权」,DB 迁移不在常规变更里夹带,故本轮明确延后并留痕(源码注释同处 `:33-34` 已写明 backlog)。**已随车交付的缓解**(不需迁移):①按 `payload.userId` 实查 `User`,账号不存在或 `status === 0` 一律 401,堵住「被禁用/删除账号仍能在 7 天有效期内换新令牌」(B-7);②角色不按令牌里的旧 claim 沿用,而是走 `permissions` 层的同源查询 `UserRole` 关联表后**重签**,无关联角色的历史账号回落到与 login 同源的 `User.role` 标量,避免降权后仍持旧角色。边界:这两条只保证「账号状态实时」,**不**构成令牌吊销——已签发且账号仍启用的旧 refresh token 依旧可用到过期 | ✅ 2026-09-23 确认(延后为有意决策,非遗漏;轮换/吊销待授权迁移)|
|
||||||
|
| CMS 写入侧字段校验的边界(承接「数字口径 basis 结构强制」行) | 出路由 `src/lib/cms/validate-content-data.ts` 承载,挂在 `POST /api/admin/items`(`:132`)与 `PATCH`(`:215`)上,**只执行 `ContentModel.fields` 已声明的约束**:`options` 即视为枚举(`select`/`dropdown` 与带 `options` 的 `text` 在 `content-types.ts` 同义)、`min`/`max`/`pattern`;违规返回 `validationError('内容字段校验未通过', { fields })` 并附每条 `path`。刻意保持宽松的两处:**未声明的键一律放行、未填的键不判必填**,以免改变既有载荷形态(seed 与页面组件依赖宽松结构);`loadDeclaredFields` 在模型缺失或 `fields` 不可解析时回落 `[]`,即**fail-open**(无声明 = 无约束),因此「后台有校验」不等于「值一定合法」,新增约束的正解是补 `ContentModel.fields` 而非在路由里加特例。`basis` 的强制仍在那一行的口径里:渲染端 `metrics-basis-note.tsx:11` 缺省回落 `target`,`metrics-basis.test.ts` 机械校验 seed 全量条目显式声明并禁止无据自称 `verified`;字段定义改动须重跑 seed 同步 `ContentModel.fields` 后 admin 才出现输入项 | ✅ 2026-09-23 确认(写入侧只判已声明约束 + fail-open 为有意取舍)|
|
||||||
|
| 边界页(`/_not-found` / `/_global-error`)**不带站点 Header/Footer** | 这是结构结果而非疏漏,本轮核实并固化为口径:`src/app/not-found.tsx` 位于 `app/` 根、**不在 `(marketing)` 路由组内**,而 Header/Footer 只挂在 `(marketing)/layout.tsx:14,22`,故 404 页由 root layout 提供公共外壳(主题内联脚本、GA、CookieConsent、MobileTabBar)但**无导航与页脚**;`NotFoundContent` 自带恢复动线(返回首页 CTA + `history.back()` + 产品/方案/关于/联系四枚入口)。`src/app/global-error.tsx` 更彻底:按 Next 契约它**自带 `<html lang="zh-CN">` + `<title>` + 与 `globals.css` 同值的 OS 主题内联样式并顶替 root layout**(`node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/error.md:163,165` 明写"必须自带文档壳、不含全局样式、`data-theme` 到不了它"),因此两页都**不能**指望复用 Header/Footer 或品牌红规则;错误边界是 Client Component,`metadata` 导出无效,标题只能用 React `<title>`。**约束**:新增 `app/` 顶层边界文件须自带文档壳与主题样式,且本版本 props 是 `retry` 不是 `reset`;给这两页补导航/页脚需先决定是否把 root layout 变成可复用外壳,属设计判断而非机械改动 | ✅ 2026-09-23 确认(口径固化,配套回归 `src/app/global-error.test.tsx`)|
|
||||||
|
| lint warning 的处置口径:**0 error 判门禁,warning 不判红也不清零** | 现行裁定:`npm run lint` 通过标准是 **0 error**;warning 属**已知存量**并只登记分布、不阻塞合入(数量口径的**版本库内权威记录位**是 `docs/development/quality-gates.md` §1:2026-09-23 **第二周期**最终树复跑 `npm run lint` ⇒ **104 problems / 0 errors / EXIT=0**,分项 55 `no-console` + 25 `no-explicit-any` + **11** `set-state-in-effect` + 10 `no-img-element` + 各 1 `no-sync-scripts`/`no-html-link-for-pages`/死抑制(同日第一周期为 105,差值唯一来自后台编辑器"挂载期消费守卫"那个效果被删)。⚠ 不要再指向 `AGENTS.md` §5——该文件被 `.gitignore` 有意排除、由 `next dev` 再生,不可审也不可回滚,见验收 N-16)。分级由 `eslint.config.mjs` 声明而非随手 `--fix`:`@typescript-eslint/no-explicit-any`(`:52`)、`no-console`(`:54`,`allow: ['warn','error']`)、`react-hooks/set-state-in-effect`(`:61`)为 `warn`;`react/no-unescaped-entities`、`eqeqeq`、`prefer-const` 等是 `error`。按路径降档只有三处 override(`e2e/**/*.ts` 关 `no-console`、`**/*.test.{ts,tsx}` 与 `**/__tests__/**` 关 `no-explicit-any`、`tests/performance/*.js`),**`prisma/seed.ts` 不在其中**——它的进度日志是"有意容忍的 warning",不是配置豁免。**禁止的处置方式**:为凑绿把规则调成 `off`、加行内 `eslint-disable` 而不写理由、把 `Unused eslint-disable directive`(死抑制)当噪声忽略——死抑制本身按 §「门禁断言的有效性口径」第 1 条归入"什么都没测"。需要静音时须附来源注释 | ✅ 2026-09-23 确认(warning 存量制,error 零容忍)|
|
||||||
|
| `duration-300` → 动效令牌 与 死样式收口:**本轮为 in flight,不得当作已完成** | 两条已知残留:①`duration-300` 一类 Tailwind 预设时长绕过 `--transition-fast/normal/slow`(`globals.css:255-258` 的 180/280/450/700ms)且 300ms 超出动效四原则「入场 180–280ms」的档位;②`-[var(--color-*)]/<alpha>` 静默不产出 CSS 的家族(见上方 §5.20 与「死样式族的收口边界」行)。**2026-09-23 文档同步时不写结论**:另一执行体正在同一批 `src/**/*.tsx` 上作业(写入瞬间 `src` 下 `duration-300` 命中 13 处、死样式族命中 8 处,均在实时变动中),任何计数此刻都不可引用。**约束(替换时才成立)**:动效时长走令牌、点亮死样式属设计判断、hover 合成底色须重算对比度(axe 与 `check:contrast` 都测不到 `:hover`)。复算命令:`grep -rn "duration-300" src` 与 `grep -rno "\-\[var(--color-[a-z-]*)\]/[0-9]*" src` | ⏳ in flight(2026-09-23 观察:他人正在编辑相关文件;未落地,不作为已确认决策引用)|
|
||||||
|
| E2E 目标与 skipped 口径 / `check:axe*` 的落地状态(2026-09-23) | ①**口径**:`npm run test`(含 `test:all` 里的 `test:e2e:fast`)的 webServer 是 `npm run dev`(`e2e/playwright.config.ts:130`),GA4 的 4 个 `@critical` 用例 × 4 project = **16 个 skipped**(`.env.production` 独占 GA ID → dev 下不注入 gtag → 用例内 `test.skip`);产物口径只有 `npm run test:e2e:prod`(CI 在 `Jenkinsfile`「🌐 E2E 测试」仅 main 分支执行,且须先 build)。裁定:**引用 GA4/生产头覆盖必须指名 prod 目标**,"全绿"不等于"全断言"。②**静态 a11y 门禁已成型**:`check:a11y` = `check:contrast` + `check:headings` + `check:brand-token`,并已并入 `test:all`。③**已闭合(2026-09-23 同日落地)**:`package.json` 的 `check:axe` / `check:axe:routes` 指向的 `scripts/accessibility/{axe-node-count,crawl-routes}.mjs` 现已存在(由 `docs/acceptance/2026-09-21-axe/` 的一次性 harness 平移而来,最终树 34 路由 × 4 组合 `passed=true`,证据 JSON 仍留原目录),判定改为退出码(0 通过 / 1 判红 / 2 清单缺失或 0 条),并由 `Jenkinsfile`「♿♿ 全站 axe 节点计数」阶段(仅 main,服务用 `node dist/standalone/server.js`,按记录 PID 收服)自动执行 —— axe 节点计数自此**是**自动化门禁;引用其结果时须同时给出 `axe-evidence.json` 的 `routeCount` 与分母断言,只报 `passed=true` 不报分母即为口径错误。④文档同步另纠正三处:`npm run preview` 实为 `next start -p 3000`(非 `npx serve`,standalone 下会打警告)、渲染模式措辞由「静态导出」改「standalone 混合渲染」、`docs/testing.md` 的 dev 指示器机制改回 `e2e/fixtures.ts` 的真实实现 | ✅ 2026-09-23 确认(③已于同日落地为常驻门禁,④为该轮文档纠正;③的漂移记录见 `docs/lessons-learned.md` §5.26,其修复标注在同一行)|
|
||||||
|
|
||||||
|
#### 门禁断言的有效性口径(chain6/chain7 新增)
|
||||||
|
|
||||||
|
验收期间发现 `config/test/lighthouserc.json` 有 3 条 `warn` 级断言(`autocomplete-valid` / `presentation-role-conflict` / `svg-img-alt`)**在 Lighthouse 里根本没有对应审计项**:把 chain6 全部 21 份报告的 `audits` 键取并集(175 个 id)比对 62 个非 `categories:` 断言键,只有这 3 条缺席;`@lhci/cli@0.15.1` 用的是它自带的 `lighthouse@12.6.1`(不是顶层的 13.4.1),其 `core/audits/accessibility/` 64 个审计文件里没有这三个。它们每轮稳定打印 `"…" is not a known audit. found: 0`,因为是 warn 级永不判红 —— 于是"57 项 a11y 断言全过"里混进了 3 项什么都没测的条目。
|
||||||
|
|
||||||
|
**约束(今后一律适用)**:
|
||||||
|
|
||||||
|
1. **门禁里的每一条断言必须映射到真实存在的测量项**。删除或新增断言都要留下"这项由谁兜底"的说明;`warn` 级、恒 0、无对照的断言按"假绿"处理,不得当作覆盖。
|
||||||
|
2. **拆门之前先证明覆盖还在别处**。用与 ⑤ 门禁同款的 `runOnly: {type:'tag', values:['wcag2a','wcag2aa','wcag21a','wcag21aa']}` 做正/负对照(`docs/acceptance/2026-09-21-axe/probe-axe-rule-coverage.mjs`,每条规则各造一个必然违规的最小节点):`autocomplete-valid`、`svg-img-alt` 确在 axe 通道内(各抓到 1 node);但 `presentation-role-conflict` 属 `best-practice` tag,**axe 门禁的 tag 集合根本不跑它**。因此处理不是"删了事",而是删死断言 + 在 `axe-contrast-evidence.mjs`(现 `scripts/accessibility/axe-node-count.mjs`)增加第二次 `runOnly: {type:'rules', values:EXTRA_RULES}` 运行。
|
||||||
|
3. **规则级通道也要断言分母**:每页 `extraRulesChecked === EXTRA_RULES.length`(计 `extraRulePagesUnderCovered`)+ 违规节点 0。axe 对未知 rule id 会抛错,这一条同时防"将来某条规则被 axe 改名后静默消失"——与本节第 1 条是同一种病。
|
||||||
|
4. **异常快的绿灯必须自证**。chain6 `type-check` 只花 2s,用注入 `const probe: number = "not a number"` 的正对照确认 `tsc` 能报错(冷缓存 11.1s)、删除后 2.9s 干净,成因是 `tsconfig.compilerOptions.incremental=true` 命中 `.tsbuildinfo` ⇒ 门禁有效。**"很快/很慢"只是怀疑的起点,能造正对照就不要靠推断背书**(与记忆「绿灯≠达标」同源)。
|
||||||
|
5. **文档里的数字要被复算,否则就从证据退化成传说**:该口径本身成立,但**它的示范当时写歪了**——原文称「`AGENTS.md` §5 记的 lint『实测 105 条 warning』在 chain4/chain6 实测均为 **106**,已按实测改文并注明分布(56 `no-console`/25/12/10/其余 3 条零散)」,而 2026-09-23 本树复跑 `npm run lint` 实为 **105 problems / 0 errors / EXIT=0**、`no-console` **55** 条,且"改文"落在未被 git 跟踪的 `AGENTS.md` 上(验收 N-16:由 `next dev` 再生,随时可丢)。裁定:**warning 计数按轮次漂移,不作为常量引用**;权威分布记录位改到 `docs/development/quality-gates.md` §1,并在其后附可复制的复算命令(含分项管道)。本条教训从"文档数字落后于实测"升级为"**文档数字被写进了不可审的载体**"。
|
||||||
|
|
||||||
|
来源:`docs/acceptance/2026-09-21-gates/final-tree-results.md` ④ 与「Lighthouse 的 3 条死断言」小节、`docs/lessons-learned.md §5.25`。
|
||||||
|
|
||||||
## 页面类型与四层映射
|
## 页面类型与四层映射
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ Novalon 的界面是一份可以逐条审计的经营宣言。它模仿顶级咨
|
|||||||
- **Do** 让每个 section 以可验证的陈述开场,数据、来源角注跟上(The Answer-First Rule)。
|
- **Do** 让每个 section 以可验证的陈述开场,数据、来源角注跟上(The Answer-First Rule)。
|
||||||
- **Do** 新文字令牌落地前先在代码注释写实测对比度比值(沿现制度:如 5.74:1、AA 判定)。
|
- **Do** 新文字令牌落地前先在代码注释写实测对比度比值(沿现制度:如 5.74:1、AA 判定)。
|
||||||
- **Do** 用 `text-brand-ink` 写红色文字、`bg-brand` 做红色填充,永不混用(双通道)。
|
- **Do** 用 `text-brand-ink` 写红色文字、`bg-brand` 做红色填充,永不混用(双通道)。
|
||||||
- **Do** 入场动效统一 ease-ink `cubic-bezier(0.22,1,0.36,1)`、180–280ms,stagger 用 `--stagger-*` 令牌;reduced-motion 时退为静态。
|
- **Do** 入场动效统一 ease-ink `cubic-bezier(0.22,1,0.36,1)`、180–280ms;stagger 在 JS 侧以秒数值表达(Section 间 100–150ms、子元素卡片 30–60ms;原 `--stagger-*` CSS 令牌因 framer-motion 读不到自定义属性且零消费已删除);`delay` 属调度不属时长,>150ms 的字面 delay 为已批准保留值;reduced-motion 时退为静态。
|
||||||
- **Do** 未发布内容使用 `ContentUnavailableState`(带导航出口),空/错/成功态永远给出下一步。
|
- **Do** 未发布内容使用 `ContentUnavailableState`(带导航出口),空/错/成功态永远给出下一步。
|
||||||
|
|
||||||
### Don't:
|
### Don't:
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
|
# 构建 + 运行一体镜像。
|
||||||
|
#
|
||||||
|
# 服务契约来源(交叉验证):
|
||||||
|
# - node_modules/next/dist/docs/01-app/03-api-reference/05-config/01-next-config-js/output.md:
|
||||||
|
# output: 'standalone' 产出可独立部署的目录 + 精简 server.js;该 server **默认不含** public
|
||||||
|
# 与 static,需手工拷到 <standalone>/public 与 <standalone>/.next/static(本项目 distDir=dist,
|
||||||
|
# 实测为 <standalone>/dist/static,见下);监听地址/端口由 HOSTNAME / PORT 环境变量决定。
|
||||||
|
# - 本仓库 dist/ 实测:dist/standalone/server.js、dist/standalone/dist/{BUILD_ID,server,...}、
|
||||||
|
# dist/static/{chunks,media,<buildId>}。
|
||||||
|
#
|
||||||
|
# 与 Dockerfile.prod 的分工(避免第三种变体):
|
||||||
|
# - Dockerfile.prod:直接复用**宿主机已构建**的 dist/standalone,宿主为 darwin,
|
||||||
|
# 因此需要 sharp-deps 阶段补 linux-musl 的 @img 二进制。
|
||||||
|
# - 本文件:镜像内完成 npm ci + next build(依赖原生装到 linux-musl),无需覆盖 @img。
|
||||||
|
#
|
||||||
|
# 历史缺陷(ACCEPTANCE_REVIEW_2026-09-21 §5):旧版把 /app/dist 当静态 HTML 根
|
||||||
|
# `COPY --from=builder /app/dist /usr/share/nginx/html` 交给 nginx,standalone 产物里没有
|
||||||
|
# 可直服的站点根目录(HTML 在 dist/standalone/dist/server/app/*.html,且需运行时渲染),
|
||||||
|
# 用本镜像部署即白屏/404。
|
||||||
|
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -9,13 +29,39 @@ COPY . .
|
|||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
RUN npm run build
|
# 先删除构建上下文里带进来的宿主产物:.dockerignore 为 Dockerfile.prod 放行了
|
||||||
|
# dist/standalone 与 dist/static,而 next build 不会清空目标目录,残留的
|
||||||
|
# dist/standalone/node_modules/@img/*-darwin-* 会被下面的 runner 原样拷进 linux 镜像。
|
||||||
|
RUN rm -rf "${NEXT_DIST_DIR:-dist}" && npm run build
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
WORKDIR /app
|
||||||
COPY nginx-static.conf /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
EXPOSE 80
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV PORT=3000
|
||||||
|
# standalone server 只绑 HOSTNAME,默认 localhost 在容器外不可达
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
RUN addgroup --system --gid 1001 nodejs && \
|
||||||
|
adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Next.js 16 standalone 输出:server.js 位于 dist/standalone 根目录(与 Dockerfile.prod 一致)
|
||||||
|
# 注:以下三个路径按 next.config.mjs 的默认 distDir=dist 硬编码;若 CI 用 NEXT_DIST_DIR 改目录,
|
||||||
|
# 需同步这里的 --from 路径(当前 Jenkinsfile 未设置该变量)。
|
||||||
|
COPY --from=builder /app/dist/standalone ./
|
||||||
|
# 客户端静态资源,standalone 内的 distDir 影子目录为 dist/
|
||||||
|
COPY --from=builder /app/dist/static ./dist/static
|
||||||
|
# 公共静态资源(图片、字体、favicon、uploads)
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
RUN chown -R nextjs:nodejs /app
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# 应用层安全响应头由 next.config.mjs 的 headers() 发出,本镜像不再叠加 nginx 层,
|
||||||
|
# 因此容器直连(无 nginx)时头部依然完整。
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
FROM nginx:alpine
|
|
||||||
|
|
||||||
COPY html /var/www/novalon
|
|
||||||
COPY nginx-internal.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@@ -162,6 +162,8 @@ pipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ====== L3: E2E + 用户旅程测试 ======
|
// ====== L3: E2E + 用户旅程测试 ======
|
||||||
|
// 历史上这些命令带 `|| echo`,失败被吞掉、绿灯不代表任何行为正确(验收 A-5)。
|
||||||
|
// 现统一 set -e:任一例失败即整个 stage 失败。
|
||||||
stage('🌐 E2E 测试') {
|
stage('🌐 E2E 测试') {
|
||||||
when {
|
when {
|
||||||
branch 'main'
|
branch 'main'
|
||||||
@@ -170,21 +172,16 @@ pipeline {
|
|||||||
steps {
|
steps {
|
||||||
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
||||||
sh '''
|
sh '''
|
||||||
echo "🔨 构建用于 E2E 测试..."
|
set -e
|
||||||
|
echo "🔨 构建生产产物(验收 A-10:E2E 打构建产物,dev server 下预渲染/ISR/生产响应头永不被测)..."
|
||||||
npm run build
|
npm run build
|
||||||
echo "🚀 启动预览服务器..."
|
echo "🚀 运行 E2E(@smoke + @critical + @journey × 三浏览器,next start 由 Playwright webServer 起)..."
|
||||||
npx serve dist -l 3000 &
|
npm run test:e2e:prod
|
||||||
sleep 5
|
|
||||||
echo "🧪 运行 E2E 快速测试(@smoke + @critical)..."
|
|
||||||
cd e2e && npx playwright test --grep "@smoke|@critical" || echo "⚠️ E2E 测试部分失败,继续执行"
|
|
||||||
echo "🧪 运行用户旅程测试..."
|
|
||||||
npx playwright test --grep @journey || echo "⚠️ 用户旅程测试部分失败,继续执行"
|
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
always {
|
always {
|
||||||
sh 'kill $(lsof -t -i:3000) 2>/dev/null || true'
|
|
||||||
publishHTML(target: [
|
publishHTML(target: [
|
||||||
allowMissing: true,
|
allowMissing: true,
|
||||||
reportDir: 'e2e/playwright-report',
|
reportDir: 'e2e/playwright-report',
|
||||||
@@ -207,22 +204,80 @@ pipeline {
|
|||||||
steps {
|
steps {
|
||||||
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
||||||
sh '''
|
sh '''
|
||||||
echo "🚀 启动预览服务器..."
|
set -e
|
||||||
npx serve dist -l 3000 &
|
|
||||||
sleep 5
|
|
||||||
echo "🧪 运行视觉回归测试..."
|
echo "🧪 运行视觉回归测试..."
|
||||||
cd e2e && npx playwright test visual-regression.spec.ts --project=visual-chromium-desktop || echo "⚠️ 视觉回归测试失败,请检查基线是否需要更新"
|
npm run test:visual
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
always {
|
always {
|
||||||
sh 'kill $(lsof -t -i:3000) 2>/dev/null || true'
|
|
||||||
archiveArtifacts artifacts: 'e2e/test-results/**/*.png', allowEmptyArchive: true
|
archiveArtifacts artifacts: 'e2e/test-results/**/*.png', allowEmptyArchive: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== L4.5: 可访问性与设计契约门禁 ======
|
||||||
|
stage('♿ 可访问性门禁') {
|
||||||
|
steps {
|
||||||
|
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
echo "🎨 令牌对比度(读令牌表,含暗色与 alpha 组)..."
|
||||||
|
npm run check:contrast
|
||||||
|
echo "🔴 双通道红契约(禁 text-[var(--color-brand)])..."
|
||||||
|
npm run check:brand-token
|
||||||
|
echo "🧱 标题层级..."
|
||||||
|
npm run check:headings
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('⚡ Lighthouse 性能与无障碍') {
|
||||||
|
when {
|
||||||
|
branch 'main'
|
||||||
|
beforeAgent true
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
echo "🔨 构建用于 Lighthouse..."
|
||||||
|
npm run build
|
||||||
|
echo "🚦 运行 lhci(断言含 axe critical 失败数 = 0)..."
|
||||||
|
npm run lighthouse
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
archiveArtifacts artifacts: 'lighthouse-reports/**/*.report.html, lighthouse-reports/**/*.report.json', allowEmptyArchive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('🧬 变异测试') {
|
||||||
|
when {
|
||||||
|
branch 'main'
|
||||||
|
beforeAgent true
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
npm run test:mutation:quick
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// 沙箱模式下 Stryker 不改写工作树;此清理仅兜底残留临时目录
|
||||||
|
sh 'rm -rf .stryker-tmp || true'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ====== L5: 安全扫描 ======
|
// ====== L5: 安全扫描 ======
|
||||||
stage('🔒 安全扫描') {
|
stage('🔒 安全扫描') {
|
||||||
when {
|
when {
|
||||||
@@ -232,10 +287,11 @@ pipeline {
|
|||||||
steps {
|
steps {
|
||||||
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
||||||
sh '''
|
sh '''
|
||||||
|
set -e
|
||||||
echo "🔒 运行依赖安全审计..."
|
echo "🔒 运行依赖安全审计..."
|
||||||
npm audit --audit-level=high || echo "⚠️ 存在高危依赖漏洞,请检查"
|
npm audit --audit-level=high
|
||||||
echo "🔒 检查安全响应头..."
|
echo "🔒 检查本分支构建产物的安全响应头..."
|
||||||
npm run test:security:headers -- --url https://novalon.cn || echo "⚠️ 安全头检查未通过,请检查 Nginx 配置"
|
npm run test:security:headers
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,6 +330,80 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== 验收 §8-⑤:全站 axe 节点计数(三规则级覆盖 + 分母闭合的唯一真实来源) ======
|
||||||
|
// 复用上一个阶段刚产出的 standalone 产物,不重复构建。跑在 main 分支(与 E2E / Lighthouse 同档:
|
||||||
|
// 34 路由 × 双引擎 × 双主题 = 136 页扫描,约 14 分钟);令牌级 a11y 门禁仍在每个分支跑。
|
||||||
|
// 服务用 `node dist/standalone/server.js`,不用 `npm run start`——Next 对 output:'standalone' 下
|
||||||
|
// 的 next start 会直接告警不支持(佐证见 docs/acceptance/2026-09-21-gates/ga4-production-run.txt)。
|
||||||
|
stage('♿♿ 全站 axe 节点计数') {
|
||||||
|
when {
|
||||||
|
branch 'main'
|
||||||
|
beforeAgent true
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
AXE_PORT="${AXE_PORT:-3100}" # 专用端口:不复用 :3000(chain2 事故就是端口被占 + 静默复用)
|
||||||
|
ROUTES=/tmp/axe-routes.xml
|
||||||
|
|
||||||
|
echo "📦 把浏览器静态资源并入 standalone 根目录(与 Dockerfile.prod / scripts/deploy.sh 同一套装配)..."
|
||||||
|
echo " standalone 产物不含 dist/static 与 public,缺了它们 /_next/static/** 全 404 ⇒"
|
||||||
|
echo " 页面落在默认黑白底上,对比度会「意外达标」;harness 的 bgMismatch 判据会抓到,但别拿它当门禁目的。"
|
||||||
|
mkdir -p dist/standalone/dist/static dist/standalone/public
|
||||||
|
cp -R dist/static/. dist/standalone/dist/static/
|
||||||
|
cp -R public/. dist/standalone/public/
|
||||||
|
|
||||||
|
echo "🚀 启动 standalone 服务 :$AXE_PORT ..."
|
||||||
|
PORT="$AXE_PORT" HOSTNAME=127.0.0.1 node dist/standalone/server.js > /tmp/axe-server.log 2>&1 &
|
||||||
|
AXE_PID=$!
|
||||||
|
# 只收服本轮自己起的进程(记录 PID)。历史事故:只 kill 包装进程会留下 next-server
|
||||||
|
# 孤儿继续占端口,下一轮 E2E / Lighthouse 通过 reuseExistingServer 静默复用它 —— 整轮验证被毒化。
|
||||||
|
cleanup() {
|
||||||
|
if kill -0 "$AXE_PID" 2>/dev/null; then
|
||||||
|
kill "$AXE_PID" 2>/dev/null || true
|
||||||
|
i=0
|
||||||
|
while kill -0 "$AXE_PID" 2>/dev/null && [ "$i" -lt 20 ]; do i=$((i+1)); sleep 1; done
|
||||||
|
kill -0 "$AXE_PID" 2>/dev/null && kill -9 "$AXE_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# rc=$? 先行捕获:EXIT/INT/TERM 处理器必须原样带回门禁的退出码,
|
||||||
|
# 否则「trap 里最后一条命令的状态」会把判红变成判绿(POSIX trap 语义的坑)。
|
||||||
|
on_exit() { rc=$?; cleanup; exit "$rc"; }
|
||||||
|
trap on_exit EXIT INT TERM
|
||||||
|
|
||||||
|
CODE=000
|
||||||
|
i=0
|
||||||
|
while [ "$CODE" != "200" ] && [ "$i" -lt 40 ]; do
|
||||||
|
i=$((i+1))
|
||||||
|
CODE=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$AXE_PORT/" || true)
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
if [ "$CODE" != "200" ]; then
|
||||||
|
echo "❌ standalone 服务未就绪(HTTP $CODE),日志:"
|
||||||
|
tail -40 /tmp/axe-server.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export BASE="http://127.0.0.1:$AXE_PORT"
|
||||||
|
echo "🗺️ 生成全站路由清单(sitemap ∪ 预渲染产物 ∪ 站内链接 BFS)..."
|
||||||
|
OUT="$ROUTES" npm run check:axe:routes
|
||||||
|
echo "🔎 双引擎(chromium/firefox)× 双主题(light/dark)逐页 axe 节点计数 + 规则级通道..."
|
||||||
|
SITEMAP="$ROUTES" npm run check:axe
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// 失败时这份证据就是判红依据(harness 失败也会落盘,含逐条 failures),必须留档
|
||||||
|
archiveArtifacts artifacts: 'docs/acceptance/2026-09-21-axe/axe-evidence.json', allowEmptyArchive: true
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
sh 'tail -40 /tmp/axe-server.log || true'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage('🚀 部署到生产环境') {
|
stage('🚀 部署到生产环境') {
|
||||||
when {
|
when {
|
||||||
allOf {
|
allOf {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
- [x] CMS 全覆盖:法律页、新闻、团队、案例、服务、方案、产品、独立产品、首页运营位(ContentZone)、RBAC/工作流/媒体/通知全部从 CMS 读取并启用 ISR(3600s)
|
- [x] CMS 全覆盖:法律页、新闻、团队、案例、服务、方案、产品、独立产品、首页运营位(ContentZone)、RBAC/工作流/媒体/通知全部从 CMS 读取并启用 ISR(3600s)
|
||||||
- [x] CMS 管理后台:角色权限界面(`/admin/roles`)、内容模型 × 操作权限矩阵、super_admin 锁定
|
- [x] CMS 管理后台:角色权限界面(`/admin/roles`)、内容模型 × 操作权限矩阵、super_admin 锁定
|
||||||
- [x] 全量测试体系:单元 ~1600 / 功能 E2E ~800 / 视觉回归三端 / 用户旅程 UJ-01~UJ-11 / Lighthouse / k6 负载压力 / 安全扫描,全部通过
|
- [x] 全量测试体系:单元 **134 suites / 1697 例** / 功能 E2E **1080 例**(270 例 × chromium + chromium-mobile + firefox + webkit 四个 project)/ 视觉回归 **125 例**(25 例 × 桌面·平板·移动·firefox·webkit 五个 project)/ 用户旅程 UJ-01~UJ-11 / Lighthouse / k6 负载压力 / 安全扫描,全部通过。**计数为 2026-09-23 本树复跑实测**(`npm run test:coverage` ⇒ `Test Suites: 134 passed` / `Tests: 1697 passed` / EXIT=0;`cd e2e && npx playwright test --list` ⇒ `Total: 1205 tests in 22 files` = 1080 + 125),非估算;计数随树漂移,引用请附复算命令
|
||||||
- [x] **官网产品模块定位(IHG/字节式品牌矩阵,2026-08-19)**:官网品牌宣传为主,产品矩阵页为聚合入口;成熟产品 `externalUrl` 外链独立站(NovaVis → novavis.p.novalon.cn);未成熟产品详情页占位,独立站上线后切换外链
|
- [x] **官网产品模块定位(IHG/字节式品牌矩阵,2026-08-19)**:官网品牌宣传为主,产品矩阵页为聚合入口;成熟产品 `externalUrl` 外链独立站(NovaVis → novavis.p.novalon.cn);未成熟产品详情页占位,独立站上线后切换外链
|
||||||
- [x] **结构性文案全站 CMS 化(2026-08-19)**:新增 `page-copy` 内容模型承载全部营销页面章节标题/眉标/描述/CTA/空状态(首页 + 服务/方案/产品/案例/新闻列表页),「CMS 优先 + 硬编码兜底」不白屏;seed 写入 6 条,本地 db:seed 已生效
|
- [x] **结构性文案全站 CMS 化(2026-08-19)**:新增 `page-copy` 内容模型承载全部营销页面章节标题/眉标/描述/CTA/空状态(首页 + 服务/方案/产品/案例/新闻列表页),「CMS 优先 + 硬编码兜底」不白屏;seed 写入 6 条,本地 db:seed 已生效
|
||||||
- [x] **信任证据补齐·阶段 0(成立 <1 年策略,2026-08-20)**:信任策略从「结果证据」转向「可验证的过程 + 能力 + 治理证据」——首页「首批客户共创计划」板块(共创进行中/产品内测中/成果授权公开三档如实状态 + 招募 CTA)+ 关于页资质「建设中」如实空态,零编造
|
- [x] **信任证据补齐·阶段 0(成立 <1 年策略,2026-08-20)**:信任策略从「结果证据」转向「可验证的过程 + 能力 + 治理证据」——首页「首批客户共创计划」板块(共创进行中/产品内测中/成果授权公开三档如实状态 + 招募 CTA)+ 关于页资质「建设中」如实空态,零编造
|
||||||
@@ -87,9 +87,20 @@ npm run build
|
|||||||
### 预览生产版本
|
### 预览生产版本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run preview
|
npm run build # 先产出 dist/
|
||||||
|
npm run preview # == npm run start == `next start -p 3000`(不是 `npx serve`)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`preview` 是 `start` 的同义别名,两者都是 `next start -p 3000`。项目为 `output: 'standalone'`,Next 16 在此组合下会打印
|
||||||
|
`"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.`
|
||||||
|
——这是**警告而非抛错**,服务仍会起来,但走的是**不受支持的降级路径**,因此它已不再被任何门禁当作产物口径:
|
||||||
|
|
||||||
|
- 受支持的启动方式是 `node dist/standalone/server.js`(静态资源由 `package.json:9` 的 `postbuild` 在 `npm run build` 末尾自动拷入 `dist/standalone/`,standalone 产物本身不含 `dist/static` 与 `public`,缺了它们 `/_next/static/**` 全 404)。该启动方式**尚未做成 npm script**,目前由 `check:axe`、`check:headings`、`e2e/playwright.config.ts:134`(`E2E_TARGET=production`)与 `Jenkinsfile` 各自直接 `node` 起。
|
||||||
|
- `npm run test:e2e:prod` 走的是上面这条受支持路径(`e2e/playwright.config.ts:132-135`),**不是** `next start`。
|
||||||
|
- `npm run check:headings` 存在产物时直起 standalone,仅在产物缺失时回退 `npm run preview` 并打印告警(`scripts/utils/check-heading-hierarchy.ts:52-67`,验收 N-24③)。
|
||||||
|
|
||||||
|
详见 `CLAUDE.md`「Build & Preview」。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -98,31 +109,43 @@ novalon-website/
|
|||||||
│ ├── app/ # Next.js App Router
|
│ ├── app/ # Next.js App Router
|
||||||
│ │ ├── (marketing)/ # 营销页面路由组
|
│ │ ├── (marketing)/ # 营销页面路由组
|
||||||
│ │ │ ├── page.tsx # 首页
|
│ │ │ ├── page.tsx # 首页
|
||||||
|
│ │ │ ├── home-content-v15.tsx # 首页内容装配(V1.5)
|
||||||
│ │ │ ├── about/ # 关于我们
|
│ │ │ ├── about/ # 关于我们
|
||||||
|
│ │ │ ├── team/ # 团队
|
||||||
│ │ │ ├── cases/ # 成功案例
|
│ │ │ ├── cases/ # 成功案例
|
||||||
│ │ │ ├── contact/ # 联系我们
|
│ │ │ ├── contact/ # 联系我们
|
||||||
|
│ │ │ ├── methodology/ # 方法论
|
||||||
│ │ │ ├── news/ # 新闻动态
|
│ │ │ ├── news/ # 新闻动态
|
||||||
│ │ │ ├── products/ # 产品服务
|
│ │ │ ├── products/ # 产品服务
|
||||||
│ │ │ ├── services/ # 核心业务
|
│ │ │ ├── services/ # 核心业务
|
||||||
│ │ │ └── solutions/ # 解决方案
|
│ │ │ └── solutions/ # 解决方案
|
||||||
|
│ │ ├── admin/ # CMS 管理后台
|
||||||
|
│ │ ├── api/ # 服务端接口(表单、CMS、鉴权)
|
||||||
│ │ ├── privacy/ # 隐私政策
|
│ │ ├── privacy/ # 隐私政策
|
||||||
│ │ ├── terms/ # 服务条款
|
│ │ ├── terms/ # 服务条款
|
||||||
│ │ ├── layout.tsx # 根布局
|
│ │ ├── layout.tsx # 根布局
|
||||||
│ │ ├── error.tsx # 错误页面
|
│ │ ├── error.tsx # 根段错误页面
|
||||||
|
│ │ ├── global-error.tsx # 全局错误边界
|
||||||
│ │ └── not-found.tsx # 404 页面
|
│ │ └── not-found.tsx # 404 页面
|
||||||
│ ├── components/ # React 组件
|
│ ├── components/ # React 组件
|
||||||
│ │ ├── ui/ # 基础 UI 组件
|
│ │ ├── ui/ # 基础 UI 组件
|
||||||
│ │ ├── layout/ # 布局组件
|
│ │ ├── layout/ # 布局组件
|
||||||
│ │ ├── sections/ # 页面区块组件
|
│ │ ├── sections/ # 页面区块组件
|
||||||
│ │ ├── effects/ # 视觉效果组件
|
│ │ ├── content/ # 内容渲染组件(`sections.tsx`)
|
||||||
|
│ │ ├── detail/ # 详情页组件
|
||||||
|
│ │ ├── admin/ # 后台组件
|
||||||
|
│ │ ├── theme/ # 主题组件
|
||||||
│ │ ├── seo/ # SEO 组件
|
│ │ ├── seo/ # SEO 组件
|
||||||
│ │ └── analytics/ # 分析组件
|
│ │ └── analytics/ # 分析组件
|
||||||
│ ├── hooks/ # 自定义 Hooks
|
│ ├── hooks/ # 自定义 Hooks
|
||||||
│ └── contexts/ # React Context
|
│ ├── lib/ # 工具与领域逻辑(含 `cms/`、`constants/`、`media/`)
|
||||||
├── e2e/ # E2E 测试
|
│ └── types/ # 类型声明
|
||||||
├── tests/ # 测试文件
|
├── e2e/ # Playwright E2E + 视觉回归(**无独立 package.json**,依赖装在根目录)
|
||||||
│ ├── performance/ # 性能测试
|
├── tests/ # 非 Playwright 资产
|
||||||
│ └── styles/ # 样式测试
|
│ ├── performance/ # k6 负载/压力脚本(由 k6 CLI 跑,不在 npm scripts 内)
|
||||||
|
│ ├── lib/ # 集成测试
|
||||||
|
│ ├── styles/ # 样式测试
|
||||||
|
│ └── screenshots/ # 截图基线
|
||||||
├── docs/ # 项目文档
|
├── docs/ # 项目文档
|
||||||
├── scripts/ # 脚本文件
|
├── scripts/ # 脚本文件
|
||||||
├── config/ # 配置文件
|
├── config/ # 配置文件
|
||||||
@@ -157,31 +180,38 @@ novalon-website/
|
|||||||
| `npm start` | 启动生产服务器 |
|
| `npm start` | 启动生产服务器 |
|
||||||
| `npm run lint` | 运行 ESLint 检查 |
|
| `npm run lint` | 运行 ESLint 检查 |
|
||||||
| `npm run type-check` | TypeScript 类型检查 |
|
| `npm run type-check` | TypeScript 类型检查 |
|
||||||
| `npm run test` | 运行 E2E 测试 |
|
| `npm run test` | 全链路 E2E:`test:functional`(4 project ⇒ **1080 实例**)→ `test:visual:all`(5 project ⇒ **125 实例**)串行,`--list` 合计 **1205**。**webServer 默认是 dev server**,见 `test:e2e:prod` |
|
||||||
| `npm run test:unit` | 运行单元测试(Jest,120 suites / 1509 tests) |
|
| `npm run test:unit` | 运行单元测试(Jest,2026-09-23 本树复跑实测 **134 suites / 1697 tests**) |
|
||||||
| `npm run test:coverage` | 运行测试覆盖率(当前 Branches 82.38%, Lines 73.59%) |
|
| `npm run test:coverage` | 运行测试覆盖率。**阈值**单一真源 `config/test/jest.config.js` 的 `coverageThreshold`(全局 statements/lines/functions ≥ 75%、branches ≥ 82%);**实测值**单一记录位见 [docs/development/quality-gates.md](docs/development/quality-gates.md) §3,本文件不再复制百分比以免再次互斥 |
|
||||||
| `npm run test:e2e:fast` | 运行 E2E 快速测试(@smoke + @critical 标签) |
|
| `npm run test:e2e:fast` | 运行 E2E 快速测试(@smoke + @critical 标签) |
|
||||||
| `npm run test:e2e:standard` | 运行 E2E 标准回归测试(@regression 标签) |
|
| `npm run test:e2e:standard` | 运行 E2E 标准回归测试(@regression 标签) |
|
||||||
| `npm run test:smoke` | 运行 E2E 冒烟测试(@smoke 标签) |
|
| `npm run test:smoke` | 运行 E2E 冒烟测试(@smoke 标签) |
|
||||||
| `npm run test:critical` | 运行 E2E 关键路径测试(@critical 标签) |
|
| `npm run test:critical` | 运行 E2E 关键路径测试(@critical 标签) |
|
||||||
| `npm run test:e2e:journey` | 运行用户旅程测试(@journey 标签,UJ-01/UJ-02) |
|
| `npm run test:e2e:journey` | 运行用户旅程测试(@journey 标签,UJ-01/UJ-02) |
|
||||||
| `npm run test:e2e:mobile` | 运行移动端 E2E 测试(@mobile 标签,53 个测试) |
|
| `npm run test:e2e:prod` | **产物目标** E2E(`E2E_TARGET=production` → harness 新起 `node dist/standalone/server.js`,**不是** `npm run start`——standalone 下 `next start` 不受支持;`@smoke\|@critical\|@journey` × 4 project)。**只有 GA4 的 16 例在这里真正断言**;生产响应头不在此 grep 集合内(它属 `@security` 标签与 `test:security:headers`,勿混)(2026-09-23 实测:排除 2 个写库 spec 后 92 例通过、0 skipped);须先 `npm run build`(`postbuild` 负责装配 `dist/static` + `public`) |
|
||||||
|
| `npm run test:e2e:mobile` | 运行移动端 E2E 测试(`@mobile` 标签:**55 个用例**(16 `mobile.spec.ts` + 5 `mobile-user-journeys.spec.ts` + 14 `mobile-accessibility.spec.ts` + 18 `mobile-performance.spec.ts` + 2 `uj-11-home-conversion.spec.ts`),4 个功能 project 全跑 ⇒ **220 个实例**;`cd e2e && npx playwright test --list --grep @mobile` 实测 `Total: 220 tests in 5 files`) |
|
||||||
| `npm run test:e2e:mobile:performance` | 运行移动端性能基线测试(FCP/LCP/加载时间) |
|
| `npm run test:e2e:mobile:performance` | 运行移动端性能基线测试(FCP/LCP/加载时间) |
|
||||||
| `npm run test:e2e:mobile:accessibility` | 运行移动端可访问性测试(axe-core WCAG 2.1 AA) |
|
| `npm run test:e2e:mobile:accessibility` | 运行移动端可访问性测试(axe-core WCAG 2.1 AA) |
|
||||||
| `npm run test:mutation` | 运行变异测试(Stryker,评估测试质量,当前 36.98%) |
|
| `npm run test:mutation` | 运行变异测试(Stryker,评估测试质量)。**分数不在此声明**:全量轮耗时以十分钟计且 `--inPlace` 会改动工作树,本轮未复跑;最近一次落盘记录为整体 **36.98%**(`docs/test-strategy-plan.md` P4.5 / 验收清单条目,2026-08 轮次),引用时须附该出处与 `npm run test:mutation` 复算命令 |
|
||||||
| `npm run test:mutation:quick` | 快速变异测试(仅 utils.ts,91.18%) |
|
| `npm run test:mutation:quick` | 快速变异测试(`--mutate 'src/lib/utils.ts'`,同一落盘记录 **91.18%**,出处同上) |
|
||||||
| `npm run test:security` | 安全扫描(npm audit + 安全响应头检查) |
|
| `npm run test:security` | 安全扫描(npm audit + 安全响应头检查) |
|
||||||
| `npm run test:security:headers` | 安全响应头检查(X-Content-Type-Options, CSP, HSTS 等) |
|
| `npm run test:security:headers` | 安全响应头检查(X-Content-Type-Options, CSP, HSTS 等) |
|
||||||
| `npm run test:all` | 全量门禁检查(type-check + lint + coverage + fast E2E + security headers) |
|
| `npm run check:a11y` | 可访问性静态门禁伞:`check:contrast`(令牌对比度,只读 `globals.css`)+ `check:headings`(标题层级,有产物直起 standalone、缺产物才回退 preview)+ `check:brand-token`(双通道红) |
|
||||||
|
| `npm run check:axe:routes` | 生成全站路由清单:sitemap ∪ 预渲染产物 ∪ 站内链接 BFS → `/tmp/axe-routes.xml`(`scripts/accessibility/crawl-routes.mjs`;退出码 0/2) |
|
||||||
|
| `npm run check:axe` | 双引擎 × 双主题逐页 axe **节点计数** + 三条规则级通道的分母断言(`scripts/accessibility/axe-node-count.mjs`);退出码 0 通过 / 1 判红 / 2 清单缺失。**需先有 `node dist/standalone/server.js` 在 :3100**;不在 `test:all` 内,CI 由 `Jenkinsfile`「♿♿」阶段(仅 main)执行 |
|
||||||
|
| `npm run test:all` | 全量门禁检查,`package.json:48` 原文七段:type-check + lint + coverage + **test:integration:real** + check:a11y + fast E2E + security headers;其 E2E 段仍是 dev 目标,GA4 `@critical` 在其中 skipped,需另跑 `test:e2e:prod` |
|
||||||
| `npm run lighthouse` | 运行 Lighthouse 性能测试 |
|
| `npm run lighthouse` | 运行 Lighthouse 性能测试 |
|
||||||
|
|
||||||
## 代码质量门禁
|
## 代码质量门禁
|
||||||
|
|
||||||
项目配置了自动化质量门禁,确保代码提交前通过所有质量检查:
|
项目配置了自动化质量门禁,确保代码提交前通过所有质量检查:
|
||||||
|
|
||||||
- **ESLint**: 代码风格检查
|
- **ESLint**: 代码风格检查(pre-commit 经 husky + lint-staged 跑 `eslint --fix`,CI 跑全仓 `npm run lint`;判定口径 0 error,warning 不判红)
|
||||||
- **commitlint**: 提交信息规范
|
- **commitlint**: 提交信息规范(`.husky/commit-msg`)
|
||||||
- **Jest**: 代码覆盖率检查
|
- **Jest**: 代码覆盖率检查(阈值单一真源 `config/test/jest.config.js`)
|
||||||
|
- **可访问性**: `npm run check:a11y`(对比度 / 标题层级 / 双通道红),CI 在「♿ 可访问性门禁」阶段逐条执行同一组
|
||||||
|
- **TypeScript**: `npm run type-check` —— **不在 pre-commit**(`.lintstagedrc.json` 只跑 `eslint --fix`),推送前须自行执行
|
||||||
|
- **E2E 目标**: `npm run test` 打 dev server;GA4 的 16 个 `@critical` 实例在该轮为 skipped,产物口径见 `npm run test:e2e:prod`(详见 [docs/development/quality-gates.md](docs/development/quality-gates.md) §5–§7)
|
||||||
|
|
||||||
### 提交规范
|
### 提交规范
|
||||||
|
|
||||||
@@ -209,12 +239,12 @@ novalon-website/
|
|||||||
|
|
||||||
| 工具 | 用途 | 配置 |
|
| 工具 | 用途 | 配置 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **Jest** | 单元测试(120 suites / 1509 tests) | `config/test/jest.config.js` |
|
| **Jest** | 单元测试(2026-09-23 本树复跑:134 suites / 1697 tests) | `config/test/jest.config.js` |
|
||||||
| **Playwright** | E2E / 视觉回归 / 用户旅程测试 | `e2e/playwright.config.ts` |
|
| **Playwright** | E2E / 视觉回归 / 用户旅程测试(`--list` 实测 1205 例 = 功能 1080 + 视觉 125) | `e2e/playwright.config.ts` |
|
||||||
| **Stryker** | 变异测试(评估测试质量) | `stryker.config.json` |
|
| **Stryker** | 变异测试(评估测试质量) | `stryker.config.json` |
|
||||||
| **Allure** | 测试报告可视化 | Allure Playwright reporter |
|
| **Allure** | 测试报告可视化 | Allure Playwright reporter |
|
||||||
| **k6** | 负载/压力/API 性能测试 | `tests/performance/` |
|
| **k6** | 负载/压力/API 性能测试 | `tests/performance/` |
|
||||||
| **Lighthouse CI** | 性能/可访问性/SEO 审计 | `lighthouserc.js` |
|
| **Lighthouse CI** | 性能/可访问性/SEO 审计 | `config/test/lighthouserc.json` |
|
||||||
|
|
||||||
### E2E 测试标签体系
|
### E2E 测试标签体系
|
||||||
|
|
||||||
@@ -225,35 +255,39 @@ novalon-website/
|
|||||||
| `@smoke` + `@critical` | 快速回归(<5min) | `npm run test:e2e:fast` |
|
| `@smoke` + `@critical` | 快速回归(<5min) | `npm run test:e2e:fast` |
|
||||||
| `@regression` | 全量回归(<15min) | `npm run test:e2e:standard` |
|
| `@regression` | 全量回归(<15min) | `npm run test:e2e:standard` |
|
||||||
| `@journey` | 用户旅程(UJ-01/UJ-02) | `npm run test:e2e:journey` |
|
| `@journey` | 用户旅程(UJ-01/UJ-02) | `npm run test:e2e:journey` |
|
||||||
| `@mobile` | 移动端专项测试(53 个:16 基础 + 5 用户旅程 + 14 可访问性 + 18 性能) | `npm run test:e2e:mobile` |
|
| `@mobile` | 移动端专项测试(55 个用例:16 基础 + 5 用户旅程 + 14 可访问性 + 18 性能 + 2 UJ-11c;× 4 project = 220 实例) | `npm run test:e2e:mobile` |
|
||||||
| `@visual` | 视觉回归(105 snapshots × 5 browsers) | `npm run test:visual:all` |
|
| (无标签) | 视觉回归:`@visual` 标签**并不存在**(`e2e/` 全量 grep 为 0 命中),视觉用例由 5 个 `visual-*` project 通过 `testMatch` 选中,且只跑 `visual-regression.spec.ts`(25 例 × 5 project = 125 实例) | `npm run test:visual:all` |
|
||||||
|
|
||||||
### 运行测试
|
### 运行测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 单元测试
|
# 单元测试
|
||||||
npm run test:unit # 1509 tests
|
npm run test:unit # 134 suites / 1697 tests(2026-09-23 本树复跑)
|
||||||
|
|
||||||
# 测试覆盖率
|
# 测试覆盖率
|
||||||
npm run test:coverage # Branches 82.38%, Lines 73.59%
|
npm run test:coverage # 阈值: 全局 statements/lines/functions ≥75%、branches ≥82%
|
||||||
|
# 实测值见 docs/development/quality-gates.md §3(此处不复制百分比)
|
||||||
|
|
||||||
# E2E 测试
|
# E2E 测试
|
||||||
npm run test # 全量 E2E(631 passed)
|
npm run test # 全量 E2E = 功能 1080 + 视觉 125 = 1205 实例
|
||||||
|
# 落盘通过数引自 docs/acceptance/2026-09-21-gates/skipped-tests-final-tree.json
|
||||||
|
# (totalResults 1195 = 1167 passed / 28 skipped,生成于 2026-09-22 的 chain6 最终树);
|
||||||
|
# 本轮未重跑全量 E2E(需 dev server + 4 引擎 + 视觉基线,且 28 条 skip 需按该文件逐条解释)
|
||||||
npm run test:e2e:fast # 快速回归(@smoke + @critical)
|
npm run test:e2e:fast # 快速回归(@smoke + @critical)
|
||||||
npm run test:e2e:standard # 标准回归(@regression)
|
npm run test:e2e:standard # 标准回归(@regression)
|
||||||
npm run test:e2e:journey # 用户旅程(@journey)
|
npm run test:e2e:journey # 用户旅程(@journey)
|
||||||
|
|
||||||
# 移动端测试
|
# 移动端测试
|
||||||
npm run test:e2e:mobile # 全量移动端(53 个 @mobile 测试)
|
npm run test:e2e:mobile # 全量移动端(55 个 @mobile 用例 × 4 project = 220 实例)
|
||||||
npm run test:e2e:mobile:performance # 移动端性能基线
|
npm run test:e2e:mobile:performance # 移动端性能基线
|
||||||
npm run test:e2e:mobile:accessibility # 移动端可访问性
|
npm run test:e2e:mobile:accessibility # 移动端可访问性
|
||||||
|
|
||||||
# 变异测试
|
# 变异测试(分数只引 docs/test-strategy-plan.md 的落盘记录,本仓未复跑)
|
||||||
npm run test:mutation # 全量变异测试
|
npm run test:mutation # 全量变异测试
|
||||||
npm run test:mutation:quick # 快速变异(仅 utils.ts)
|
npm run test:mutation:quick # 快速变异(仅 utils.ts)
|
||||||
|
|
||||||
# 全量门禁检查
|
# 全量门禁检查
|
||||||
npm run test:all # type-check + lint + coverage + fast E2E
|
npm run test:all # type-check + lint + coverage + integration:real + check:a11y + fast E2E(dev 目标) + security headers
|
||||||
```
|
```
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
@@ -263,7 +297,7 @@ npm run test:all # type-check + lint + coverage + fast E2E
|
|||||||
项目发布统一通过 `scripts/deploy.sh` 完成,包含构建、发布、回滚、状态查看:
|
项目发布统一通过 `scripts/deploy.sh` 完成,包含构建、发布、回滚、状态查看:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 构建静态产物
|
# 构建产物(`next build`,output=standalone;脚本再把 public/ 同步进 dist、把 dist/static 复制为 dist/_next/static 供 Nginx 直服)
|
||||||
./scripts/deploy.sh build
|
./scripts/deploy.sh build
|
||||||
|
|
||||||
# 构建并发布到生产服务器(默认命令,等价于 ./scripts/deploy.sh deploy)
|
# 构建并发布到生产服务器(默认命令,等价于 ./scripts/deploy.sh deploy)
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ services:
|
|||||||
- .env.production
|
- .env.production
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./uploads:/app/uploads
|
# 落盘根是 cwd/public/uploads(src/lib/media/storage.ts:141,LOCAL_UPLOAD_DIR 未在任何 .env 中设置),
|
||||||
|
# 挂到 /app/uploads 会让上传写进容器可写层并在重启后丢失,挂载点则一直是空目录。
|
||||||
|
- ./uploads:/app/public/uploads
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
version: "3.8"
|
# 单机应用容器:跑 Next.js 16 standalone 服务(不是静态 HTML 站点)。
|
||||||
|
# 服务契约见 Dockerfile 顶部注释(output: 'standalone' → node server.js + HOSTNAME=0.0.0.0)。
|
||||||
|
# TLS/HTTP→HTTPS 跳转/HSTS/gzip/limit_req 属于边缘,见 nginx-static.conf。
|
||||||
|
#
|
||||||
|
# 历史缺陷:本文件曾用 `Dockerfile`(把 dist/ 当静态 HTML 交给 nginx、暴露 80/443 并挂载
|
||||||
|
# nginx-static.conf + ./ssl),在 standalone 产物下部署即白屏/404(ACCEPTANCE_REVIEW §5)。
|
||||||
|
|
||||||
services:
|
services:
|
||||||
novalon-website:
|
novalon-website:
|
||||||
@@ -9,8 +14,31 @@ services:
|
|||||||
container_name: novalon-website
|
container_name: novalon-website
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
# 容器内 server.js 监听 0.0.0.0:3000;边缘 nginx 的 upstream 默认写 127.0.0.1:3000,
|
||||||
- "443:443"
|
# 若把本服务加入 nginx 所在网络,可改用容器名 novalon-website:3000。
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
- PORT=3000
|
||||||
|
# .dockerignore 排除了 .env*,镜像内没有 env 文件,数据库路径必须显式给出
|
||||||
|
- DATABASE_URL=file:/app/data/prod.db
|
||||||
|
# 无需 Dockerfile.prod 里的 PRISMA_QUERY_ENGINE_LIBRARY 覆盖:那条是为「宿主机(darwin)
|
||||||
|
# 构建的 dist 被直接 COPY」准备的;本镜像在 alpine 内 npm ci + build,query engine
|
||||||
|
# 本身就是 linux-musl 版本并被 standalone 追踪进 /app/src/generated/prisma/。
|
||||||
|
env_file:
|
||||||
|
# 与 docker-compose.server.yml 保持一致(CDN_DOMAIN / NEXT_PUBLIC_* 等)
|
||||||
|
- .env.production
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx-static.conf:/etc/nginx/nginx.conf:ro
|
# SQLite 落盘
|
||||||
- ./ssl:/etc/nginx/ssl:ro
|
- ./data:/app/data
|
||||||
|
# 媒体上传目录:src/lib/media/storage.ts 默认写 <cwd>/public/uploads
|
||||||
|
# (对照:docker-compose.server.yml 挂的是 ./uploads:/app/uploads,与应用实际读取的
|
||||||
|
# public/uploads 不一致,属该文件的问题,本次未改——不在本次授权范围内)
|
||||||
|
- ./uploads:/app/public/uploads
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { chromium } from 'playwright';
|
||||||
|
import path from 'path';
|
||||||
|
const ROOT = '/Users/zhangxiang/Codes/Novalon/novalon-website';
|
||||||
|
const TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];
|
||||||
|
// 每条规则各造一个必然违规的最小节点(正对照)+ 一个正常页(负对照)
|
||||||
|
const cases = {
|
||||||
|
'autocomplete-valid': '<form><label for=x>n</label><input id=x autocomplete="bogus-value"></form>',
|
||||||
|
'presentation-role-conflict': '<div role="button" aria-label="x" tabindex="0" style="display:none">h</div><img alt="" role="presentation" tabindex="0" src="data:,"><h1>t</h1><main>m</main>',
|
||||||
|
'svg-img-alt': '<svg role="img" width="10" height="10"><rect width="10" height="10"></rect></svg><h1>t</h1><main>m</main>',
|
||||||
|
};
|
||||||
|
const b = await chromium.launch();
|
||||||
|
for (const [rule, html] of Object.entries(cases)) {
|
||||||
|
const p = await b.newPage();
|
||||||
|
await p.setContent(`<!doctype html><html lang="zh-CN"><head><title>t</title></head><body><main>${html}</main></body></html>`);
|
||||||
|
await p.addScriptTag({ path: path.join(ROOT, 'node_modules/axe-core/axe.min.js') });
|
||||||
|
const res = await p.evaluate((tags) => window.axe.run(document, { runOnly: { type: 'tag', values: tags } }), TAGS);
|
||||||
|
const v = res.violations.find((x) => x.id === rule);
|
||||||
|
const ran = [...res.violations, ...res.passes, ...res.incomplete].some((x) => x.id === rule);
|
||||||
|
console.log(`${rule.padEnd(28)} tagIncluded=${ran ? 'YES' : 'NO '} violation=${v ? v.nodes.length + ' nodes' : '0'}`);
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
await b.close();
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 第七轮(死断言处理后的复验):
|
||||||
|
# A. axe harness 扩到「tag 通道 + 规则级通道」——先 2 路由 sanity,再全站 34 路由 × 4 组合
|
||||||
|
# B. Lighthouse 去掉 3 条死断言后重跑,证明 EXIT=0 且 0 warning
|
||||||
|
set -u
|
||||||
|
cd /Users/zhangxiang/Codes/Novalon/novalon-website || exit 1
|
||||||
|
LOG=/tmp/final-chain7.log
|
||||||
|
HARNESS=docs/acceptance/2026-09-21-axe/axe-contrast-evidence.mjs
|
||||||
|
CRAWLER=docs/acceptance/2026-09-21-axe/crawl-sitemap.mjs
|
||||||
|
PORTS() { lsof -nP -iTCP:3000 -iTCP:3100 -sTCP:LISTEN 2>/dev/null | grep -c LISTEN; }
|
||||||
|
echo "=== chain7 started $(date -u +%FT%TZ) ===" > "$LOG"
|
||||||
|
echo "PREFLIGHT_LISTENERS=$(PORTS) $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
|
||||||
|
npx next start -p 3100 > /tmp/axe-server8.log 2>&1 &
|
||||||
|
SRV=$!
|
||||||
|
code=000
|
||||||
|
for i in $(seq 1 40); do
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3100/ 2>/dev/null || true)
|
||||||
|
[ "$code" = "200" ] && break
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
echo "AXE_SERVER_UP=$code $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
|
||||||
|
# 全站清单(口径与 chain6 相同:sitemap ∪ 预渲染产物 ∪ 站内链接 BFS)
|
||||||
|
BASE=http://localhost:3100 SEED=/tmp/axe-sitemap2.xml \
|
||||||
|
MANIFEST=dist/standalone/dist/server/app OUT=/tmp/axe-sitemap-all5.xml \
|
||||||
|
node --input-type=module -e "$(cat $CRAWLER)" > /tmp/crawl6.log 2>&1
|
||||||
|
echo "EXIT_CRAWL=$? $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
grep -a "^crawled" /tmp/crawl6.log >> "$LOG"
|
||||||
|
|
||||||
|
# sanity:只用 2 条路由跑新代码路径,避免 14 分钟后才发现探针本身有 bug
|
||||||
|
python3 - <<'PY'
|
||||||
|
import re
|
||||||
|
src = open('/tmp/axe-sitemap-all5.xml', encoding='utf8').read()
|
||||||
|
blocks = re.findall(r'<url>.*?</url>', src, re.S)
|
||||||
|
open('/tmp/axe-sitemap-sanity.xml', 'w', encoding='utf8').write(
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||||
|
+ "\n".join(blocks[:2]) + "\n</urlset>\n")
|
||||||
|
print('sanity urls:', len(blocks[:2]))
|
||||||
|
PY
|
||||||
|
echo "EXIT_SANITY_CROP=$?" >> "$LOG"
|
||||||
|
SITEMAP=/tmp/axe-sitemap-sanity.xml BASE=http://localhost:3100 OUT=/tmp/axe-sanity-out \
|
||||||
|
node --input-type=module -e "$(cat $HARNESS)" > /tmp/axe-sanity.log 2>&1
|
||||||
|
echo "EXIT_SANITY=$? $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
sed -e 's/\x1b\[[0-9;]*m//g' /tmp/axe-sanity.log | grep -aE "^(chromium|firefox)/|^PASSED|^SUMMARY|FATAL|Error" | head -14 >> "$LOG"
|
||||||
|
|
||||||
|
# 保留 chain6 证据,再产出最终树的新证据
|
||||||
|
cp docs/acceptance/2026-09-21-axe/axe-evidence.json \
|
||||||
|
docs/acceptance/2026-09-21-axe/axe-evidence-chain6-tagonly.json 2>/dev/null
|
||||||
|
SITEMAP=/tmp/axe-sitemap-all5.xml BASE=http://localhost:3100 \
|
||||||
|
OUT=docs/acceptance/2026-09-21-axe \
|
||||||
|
node --input-type=module -e "$(cat $HARNESS)" > /tmp/axe-sweep8.log 2>&1
|
||||||
|
echo "EXIT_AXE_FULL=$? $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
node -e "const j=require('./docs/acceptance/2026-09-21-axe/axe-evidence.json');console.log('AXE routeCount='+j.routeCount+' rows='+j.rows.length+' passed='+j.passed+' extraRules='+JSON.stringify(j.extraRules)+' summary='+JSON.stringify(j.summary))" >> "$LOG" 2>&1
|
||||||
|
|
||||||
|
kill "$SRV" 2>/dev/null
|
||||||
|
pkill -f 'next start -p 3100' 2>/dev/null
|
||||||
|
sleep 3
|
||||||
|
echo "POST_AXE_LISTENERS=$(PORTS)" >> "$LOG"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo
|
||||||
|
echo "===== chain7 lighthouse(移除 3 条死断言后)$(date -u +%FT%TZ) ====="
|
||||||
|
npm run lighthouse 2>&1
|
||||||
|
} | sed -e 's/\x1b\[[0-9;]*m//g' >> docs/acceptance/2026-09-21-gates/lighthouse.txt 2>&1
|
||||||
|
echo "EXIT_LIGHTHOUSE=${PIPESTATUS[0]} $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
# 只统计本轮(chain7 段)残留的死断言,历史段保留作对照
|
||||||
|
awk '/chain7 lighthouse/{f=1} f' docs/acceptance/2026-09-21-gates/lighthouse.txt \
|
||||||
|
| grep -ac "not a known audit" | sed 's/^/CHAIN7_DEAD_AUDIT_WARNINGS=/' >> "$LOG"
|
||||||
|
sleep 3
|
||||||
|
echo "POST_FINAL_LISTENERS=$(PORTS)" >> "$LOG"
|
||||||
|
echo "=== chain7 done $(date -u +%FT%TZ) ===" >> "$LOG"
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 第八轮(补 chain6/7 未覆盖的两项证据):
|
||||||
|
# A. npm run test:coverage —— A-7 阈值棘轮在最终树上是否仍成立
|
||||||
|
# B. iPhone SE 口径复跑(含新增的 404 边界页),证据写入新目录,不覆盖 09-21 的旧证据
|
||||||
|
set -u
|
||||||
|
cd /Users/zhangxiang/Codes/Novalon/novalon-website || exit 1
|
||||||
|
LOG=/tmp/final-chain8.log
|
||||||
|
SE_OUT=docs/acceptance/2026-09-22-iphone-se
|
||||||
|
PORTS() { lsof -nP -iTCP:3000 -iTCP:3100 -sTCP:LISTEN 2>/dev/null | grep -c LISTEN; }
|
||||||
|
echo "=== chain8 started $(date -u +%FT%TZ) ===" > "$LOG"
|
||||||
|
while ! grep -q "chain7 done" /tmp/final-chain7.log 2>/dev/null; do sleep 20; done
|
||||||
|
echo "CHAIN7_TAIL: $(grep -aE 'EXIT_AXE_FULL|AXE routeCount|EXIT_LIGHTHOUSE|CHAIN7_DEAD|^crawled|EXIT_SANITY' /tmp/final-chain7.log | tr '\n' '|')" >> "$LOG"
|
||||||
|
echo "PREFLIGHT_LISTENERS=$(PORTS) $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
|
||||||
|
npm run test:coverage > /tmp/coverage-final8.log 2>&1
|
||||||
|
echo "EXIT_COVERAGE=$? $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
{
|
||||||
|
echo "# test:coverage 最终树复跑 $(date -u +%FT%TZ)"
|
||||||
|
sed -e 's/\x1b\[[0-9;]*m//g' /tmp/coverage-final8.log | grep -aE "All files|Jest:|Tests:|Test Suites:|threshold|% (Stmts|Branch)" | head -20
|
||||||
|
} > docs/acceptance/2026-09-21-gates/coverage-final-tree.txt 2>&1
|
||||||
|
sleep 2
|
||||||
|
echo "POST_COVERAGE_LISTENERS=$(PORTS)" >> "$LOG"
|
||||||
|
|
||||||
|
mkdir -p "$SE_OUT"
|
||||||
|
npx next start -p 3000 > /tmp/se-server.log 2>&1 &
|
||||||
|
SRV=$!
|
||||||
|
code=000
|
||||||
|
for i in $(seq 1 40); do
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/ 2>/dev/null || true)
|
||||||
|
[ "$code" = "200" ] && break
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
echo "SE_SERVER_UP=$code $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
|
||||||
|
BASE=http://localhost:3000 OUT="$SE_OUT" \
|
||||||
|
node --input-type=module -e "$(cat docs/acceptance/2026-09-21-iphone-se/iphone-se-audit.mjs)" \
|
||||||
|
> "$SE_OUT/audit.log" 2>&1
|
||||||
|
echo "EXIT_SE_AUDIT=$? $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
|
||||||
|
kill "$SRV" 2>/dev/null
|
||||||
|
pkill -f 'next start -p 3000' 2>/dev/null
|
||||||
|
sleep 3
|
||||||
|
node -e '
|
||||||
|
const j = require("./'"$SE_OUT"'/measurements.json");
|
||||||
|
const pending = j.rows.filter((r) => /pending/.test(r.tag));
|
||||||
|
const overlap = pending.filter((r) => r.bannerOverlapsFooter);
|
||||||
|
const nf = j.rows.filter((r) => /not-found/.test(r.tag));
|
||||||
|
const bad = j.rows.filter((r) => r.docScrollW > r.clientW || r.theme === null);
|
||||||
|
console.log("SE rows=" + j.rows.length
|
||||||
|
+ " overflow=" + j.rows.filter((r) => r.docScrollW > r.clientW).length
|
||||||
|
+ " themeNull=" + j.rows.filter((r) => r.theme === null).length
|
||||||
|
+ " pendingRows=" + pending.length
|
||||||
|
+ " pendingBannerOverlapFooter=" + overlap.length
|
||||||
|
+ " notFoundRows=" + nf.length
|
||||||
|
+ " notFoundGaps=" + JSON.stringify(nf.map((r) => r.footerBottomGap))
|
||||||
|
+ " footerGaps=" + JSON.stringify([...new Set(j.rows.map((r) => r.footerBottomGap))]));
|
||||||
|
console.log("SE PASSED =", j.rows.length === 20 && bad.length === 0);
|
||||||
|
' >> "$LOG" 2>&1
|
||||||
|
echo "POST_FINAL_LISTENERS=$(PORTS)" >> "$LOG"
|
||||||
|
echo "=== chain8 done $(date -u +%FT%TZ) ===" >> "$LOG"
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 第九轮(把 §7 覆盖缺口的 Lighthouse 半边补上后复跑):
|
||||||
|
# lighthouserc 的 collect.url 由 7 条扩到 9 条(补 /products/erp-upgrade、/about/brand),
|
||||||
|
# 同时重跑静态门禁,确认本轮改 config/docs/probe 未触碰 src 行为。
|
||||||
|
set -u
|
||||||
|
cd /Users/zhangxiang/Codes/Novalon/novalon-website || exit 1
|
||||||
|
LOG=/tmp/final-chain9.log
|
||||||
|
PORTS() { lsof -nP -iTCP:3000 -iTCP:3100 -sTCP:LISTEN 2>/dev/null | grep -c LISTEN; }
|
||||||
|
echo "=== chain9 started $(date -u +%FT%TZ) ===" > "$LOG"
|
||||||
|
while ! grep -q "chain8 done" /tmp/final-chain8.log 2>/dev/null; do sleep 20; done
|
||||||
|
echo "CHAIN8_TAIL: $(grep -aE 'EXIT_COVERAGE|SE PASSED|SE rows|EXIT_SE_AUDIT' /tmp/final-chain8.log | tr '\n' '|')" >> "$LOG"
|
||||||
|
echo "PREFLIGHT_LISTENERS=$(PORTS) $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
|
||||||
|
for g in type-check lint test:unit check:contrast check:headings check:brand-token; do
|
||||||
|
npm run "$g" > "/tmp/c9-$(echo "$g" | tr ':' '-').log" 2>&1
|
||||||
|
echo "EXIT_$(echo "$g" | tr 'a-z-' 'A-Z')=$? $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
done
|
||||||
|
sed -e 's/\x1b\[[0-9;]*m//g' /tmp/c9-lint.log | grep -a "problems" | tail -1 >> "$LOG"
|
||||||
|
sleep 2
|
||||||
|
echo "POST_STATIC_LISTENERS=$(PORTS)" >> "$LOG"
|
||||||
|
|
||||||
|
URLS=$(node -e 'const c=require("./config/test/lighthouserc.json");console.log(c.ci.collect.url.length)' 2>/dev/null)
|
||||||
|
echo "LIGHTHOUSE_URLS=$URLS" >> "$LOG"
|
||||||
|
{
|
||||||
|
echo
|
||||||
|
echo "===== chain9 lighthouse(9 URL × 3 次 = 27 运行,补 erp-upgrade / about-brand)$(date -u +%FT%TZ) ====="
|
||||||
|
npm run lighthouse 2>&1
|
||||||
|
} | sed -e 's/\x1b\[[0-9;]*m//g' >> docs/acceptance/2026-09-21-gates/lighthouse.txt 2>&1
|
||||||
|
echo "EXIT_LIGHTHOUSE=${PIPESTATUS[0]} $(date -u +%FT%TZ)" >> "$LOG"
|
||||||
|
awk '/chain9 lighthouse/{f=1} f' docs/acceptance/2026-09-21-gates/lighthouse.txt \
|
||||||
|
| grep -ac "not a known audit" | sed 's/^/CHAIN9_DEAD_AUDIT_WARNINGS=/' >> "$LOG"
|
||||||
|
awk '/chain9 lighthouse/{f=1} f' docs/acceptance/2026-09-21-gates/lighthouse.txt \
|
||||||
|
| grep -aE "✗|✓ \[" | tail -12 >> "$LOG"
|
||||||
|
sleep 3
|
||||||
|
echo "POST_FINAL_LISTENERS=$(PORTS)" >> "$LOG"
|
||||||
|
echo "=== chain9 done $(date -u +%FT%TZ) ===" >> "$LOG"
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
# check:contrast 与 check:headings —— 空闲机器复跑(无并发 E2E/Lighthouse)
|
||||||
|
# 生成于 2026-09-21T21:51:32Z
|
||||||
|
|
||||||
|
## npm run check:contrast
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 check:contrast
|
||||||
|
> tsx scripts/utils/check-color-contrast.ts
|
||||||
|
|
||||||
|
|
||||||
|
🎨 LIGHT 主题(:root,246 个令牌)
|
||||||
|
正文层级 × 页面底色
|
||||||
|
✅ --color-text-primary on --color-bg-primary — 19.34:1
|
||||||
|
✅ --color-text-primary on --color-bg-secondary — 18.48:1
|
||||||
|
✅ --color-text-primary on --color-bg-tertiary — 17.65:1
|
||||||
|
✅ --color-text-secondary on --color-bg-primary — 10.35:1
|
||||||
|
✅ --color-text-secondary on --color-bg-secondary — 9.90:1
|
||||||
|
✅ --color-text-tertiary on --color-bg-primary — 7.58:1
|
||||||
|
✅ --color-text-muted on --color-bg-primary — 4.76:1
|
||||||
|
品牌红文字通道(text-brand-ink 及 hover 档)
|
||||||
|
✅ --color-brand-ink on --color-bg-primary — 5.84:1
|
||||||
|
✅ --color-brand-ink on --color-bg-secondary — 5.58:1
|
||||||
|
✅ --color-brand-ink on --color-brand-bg — 5.35:1
|
||||||
|
✅ --color-brand-ink-hover on --color-bg-primary — 7.87:1
|
||||||
|
✅ --color-brand-ink-hover on --color-brand-bg — 7.20:1
|
||||||
|
✅ #FFFFFF on --color-brand — 5.84:1
|
||||||
|
alpha 修饰正文(带 /80 等修饰类的正文)
|
||||||
|
✅ --color-text-secondary on --color-bg-primary @80% — 5.74:1
|
||||||
|
大号装饰文本(text-text-hint)× 各级卡片底色
|
||||||
|
✅ --color-text-hint on --color-bg-primary — 3.41:1
|
||||||
|
✅ --color-text-hint on --color-bg-secondary — 3.26:1
|
||||||
|
✅ --color-text-hint on --color-bg-tertiary — 3.12:1
|
||||||
|
✅ --color-text-hint on --color-bg-elevated — 3.41:1
|
||||||
|
深色区块(不随主题翻转的独立 token)
|
||||||
|
✅ --color-dark-text-primary on --color-dark-bg — 18.48:1
|
||||||
|
✅ --color-dark-text-secondary on --color-dark-bg — 13.03:1
|
||||||
|
✅ --color-dark-text-muted on --color-dark-bg — 7.54:1
|
||||||
|
✅ --color-brand-section-text on --color-brand-section — 8.93:1
|
||||||
|
|
||||||
|
🎨 DARK 主题(html[data-theme='dark'],246 个令牌)
|
||||||
|
正文层级 × 页面底色
|
||||||
|
✅ --color-text-primary on --color-bg-primary — 18.48:1
|
||||||
|
✅ --color-text-primary on --color-bg-secondary — 17.69:1
|
||||||
|
✅ --color-text-primary on --color-bg-tertiary — 16.55:1
|
||||||
|
✅ --color-text-secondary on --color-bg-primary — 13.03:1
|
||||||
|
✅ --color-text-secondary on --color-bg-secondary — 12.47:1
|
||||||
|
✅ --color-text-tertiary on --color-bg-primary — 7.54:1
|
||||||
|
✅ --color-text-muted on --color-bg-primary — 7.54:1
|
||||||
|
品牌红文字通道(text-brand-ink 及 hover 档)
|
||||||
|
✅ --color-brand-ink on --color-bg-primary — 6.99:1
|
||||||
|
✅ --color-brand-ink on --color-bg-secondary — 6.69:1
|
||||||
|
✅ --color-brand-ink on --color-brand-bg — 6.27:1
|
||||||
|
✅ --color-brand-ink-hover on --color-bg-primary — 10.19:1
|
||||||
|
✅ --color-brand-ink-hover on --color-brand-bg — 9.13:1
|
||||||
|
✅ #FFFFFF on --color-brand — 5.84:1
|
||||||
|
alpha 修饰正文(带 /80 等修饰类的正文)
|
||||||
|
✅ --color-text-secondary on --color-bg-primary @80% — 8.52:1
|
||||||
|
大号装饰文本(text-text-hint)× 各级卡片底色
|
||||||
|
✅ --color-text-hint on --color-bg-primary — 7.54:1
|
||||||
|
✅ --color-text-hint on --color-bg-secondary — 7.22:1
|
||||||
|
✅ --color-text-hint on --color-bg-tertiary — 6.75:1
|
||||||
|
✅ --color-text-hint on --color-bg-elevated — 6.75:1
|
||||||
|
深色区块(不随主题翻转的独立 token)
|
||||||
|
✅ --color-dark-text-primary on --color-dark-bg — 18.48:1
|
||||||
|
✅ --color-dark-text-secondary on --color-dark-bg — 13.03:1
|
||||||
|
✅ --color-dark-text-muted on --color-dark-bg — 7.54:1
|
||||||
|
✅ --color-brand-section-text on --color-brand-section — 8.93:1
|
||||||
|
|
||||||
|
📊 合计 44 组:✅ 44 / ❌ 0 不达标 / ⚠️ 0 令牌缺失
|
||||||
|
|
||||||
|
✅ 全部令牌配对满足 WCAG 2.1 AA
|
||||||
|
|
||||||
|
## npm run check:headings
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 check:headings
|
||||||
|
> tsx scripts/utils/check-heading-hierarchy.ts
|
||||||
|
|
||||||
|
🚀 启动预览服务...
|
||||||
|
(node:52518) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
🔍 检查页面标题层级...
|
||||||
|
|
||||||
|
✅ 首页 (0 个问题)
|
||||||
|
✅ 关于我们 (0 个问题)
|
||||||
|
✅ 产品中心 (0 个问题)
|
||||||
|
✅ 产品详情-ERP (0 个问题)
|
||||||
|
✅ 解决方案 (0 个问题)
|
||||||
|
✅ 解决方案详情-制造 (0 个问题)
|
||||||
|
✅ 服务列表 (0 个问题)
|
||||||
|
✅ 服务详情-软件开发 (0 个问题)
|
||||||
|
✅ 新闻列表 (0 个问题)
|
||||||
|
✅ 联系我们 (0 个问题)
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描页面: 10
|
||||||
|
通过页面: 10
|
||||||
|
失败页面: 0
|
||||||
|
问题总数: 0
|
||||||
|
|
||||||
|
📝 详细报告已保存: /Users/zhangxiang/Codes/Novalon/novalon-website/heading-hierarchy-report.json
|
||||||
|
# check:contrast 与 check:headings —— 最终树空闲复跑
|
||||||
|
# 生成于 2026-09-21T22:47:59Z
|
||||||
|
|
||||||
|
## npm run check:contrast
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 check:contrast
|
||||||
|
> tsx scripts/utils/check-color-contrast.ts
|
||||||
|
|
||||||
|
|
||||||
|
🎨 LIGHT 主题(:root,246 个令牌)
|
||||||
|
正文层级 × 页面底色
|
||||||
|
✅ --color-text-primary on --color-bg-primary — 19.34:1
|
||||||
|
✅ --color-text-primary on --color-bg-secondary — 18.48:1
|
||||||
|
✅ --color-text-primary on --color-bg-tertiary — 17.65:1
|
||||||
|
✅ --color-text-secondary on --color-bg-primary — 10.35:1
|
||||||
|
✅ --color-text-secondary on --color-bg-secondary — 9.90:1
|
||||||
|
✅ --color-text-tertiary on --color-bg-primary — 7.58:1
|
||||||
|
✅ --color-text-muted on --color-bg-primary — 4.76:1
|
||||||
|
品牌红文字通道(text-brand-ink 及 hover 档)
|
||||||
|
✅ --color-brand-ink on --color-bg-primary — 5.84:1
|
||||||
|
✅ --color-brand-ink on --color-bg-secondary — 5.58:1
|
||||||
|
✅ --color-brand-ink on --color-brand-bg — 5.35:1
|
||||||
|
✅ --color-brand-ink-hover on --color-bg-primary — 7.87:1
|
||||||
|
✅ --color-brand-ink-hover on --color-brand-bg — 7.20:1
|
||||||
|
✅ #FFFFFF on --color-brand — 5.84:1
|
||||||
|
alpha 修饰正文(带 /80 等修饰类的正文)
|
||||||
|
✅ --color-text-secondary on --color-bg-primary @80% — 5.74:1
|
||||||
|
大号装饰文本(text-text-hint)× 各级卡片底色
|
||||||
|
✅ --color-text-hint on --color-bg-primary — 3.41:1
|
||||||
|
✅ --color-text-hint on --color-bg-secondary — 3.26:1
|
||||||
|
✅ --color-text-hint on --color-bg-tertiary — 3.12:1
|
||||||
|
✅ --color-text-hint on --color-bg-elevated — 3.41:1
|
||||||
|
深色区块(不随主题翻转的独立 token)
|
||||||
|
✅ --color-dark-text-primary on --color-dark-bg — 18.48:1
|
||||||
|
✅ --color-dark-text-secondary on --color-dark-bg — 13.03:1
|
||||||
|
✅ --color-dark-text-muted on --color-dark-bg — 7.54:1
|
||||||
|
✅ --color-brand-section-text on --color-brand-section — 8.93:1
|
||||||
|
|
||||||
|
🎨 DARK 主题(html[data-theme='dark'],246 个令牌)
|
||||||
|
正文层级 × 页面底色
|
||||||
|
✅ --color-text-primary on --color-bg-primary — 18.48:1
|
||||||
|
✅ --color-text-primary on --color-bg-secondary — 17.69:1
|
||||||
|
✅ --color-text-primary on --color-bg-tertiary — 16.55:1
|
||||||
|
✅ --color-text-secondary on --color-bg-primary — 13.03:1
|
||||||
|
✅ --color-text-secondary on --color-bg-secondary — 12.47:1
|
||||||
|
✅ --color-text-tertiary on --color-bg-primary — 7.54:1
|
||||||
|
✅ --color-text-muted on --color-bg-primary — 7.54:1
|
||||||
|
品牌红文字通道(text-brand-ink 及 hover 档)
|
||||||
|
✅ --color-brand-ink on --color-bg-primary — 6.99:1
|
||||||
|
✅ --color-brand-ink on --color-bg-secondary — 6.69:1
|
||||||
|
✅ --color-brand-ink on --color-brand-bg — 6.27:1
|
||||||
|
✅ --color-brand-ink-hover on --color-bg-primary — 10.19:1
|
||||||
|
✅ --color-brand-ink-hover on --color-brand-bg — 9.13:1
|
||||||
|
✅ #FFFFFF on --color-brand — 5.84:1
|
||||||
|
alpha 修饰正文(带 /80 等修饰类的正文)
|
||||||
|
✅ --color-text-secondary on --color-bg-primary @80% — 8.52:1
|
||||||
|
大号装饰文本(text-text-hint)× 各级卡片底色
|
||||||
|
✅ --color-text-hint on --color-bg-primary — 7.54:1
|
||||||
|
✅ --color-text-hint on --color-bg-secondary — 7.22:1
|
||||||
|
✅ --color-text-hint on --color-bg-tertiary — 6.75:1
|
||||||
|
✅ --color-text-hint on --color-bg-elevated — 6.75:1
|
||||||
|
深色区块(不随主题翻转的独立 token)
|
||||||
|
✅ --color-dark-text-primary on --color-dark-bg — 18.48:1
|
||||||
|
✅ --color-dark-text-secondary on --color-dark-bg — 13.03:1
|
||||||
|
✅ --color-dark-text-muted on --color-dark-bg — 7.54:1
|
||||||
|
✅ --color-brand-section-text on --color-brand-section — 8.93:1
|
||||||
|
|
||||||
|
📊 合计 44 组:✅ 44 / ❌ 0 不达标 / ⚠️ 0 令牌缺失
|
||||||
|
|
||||||
|
✅ 全部令牌配对满足 WCAG 2.1 AA
|
||||||
|
|
||||||
|
## npm run check:headings
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 check:headings
|
||||||
|
> tsx scripts/utils/check-heading-hierarchy.ts
|
||||||
|
|
||||||
|
🚀 启动预览服务...
|
||||||
|
(node:61061) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
🔍 检查页面标题层级...
|
||||||
|
|
||||||
|
✅ 首页 (0 个问题)
|
||||||
|
✅ 关于我们 (0 个问题)
|
||||||
|
✅ 产品中心 (0 个问题)
|
||||||
|
✅ 产品详情-ERP (0 个问题)
|
||||||
|
✅ 解决方案 (0 个问题)
|
||||||
|
✅ 解决方案详情-制造 (0 个问题)
|
||||||
|
✅ 服务列表 (0 个问题)
|
||||||
|
✅ 服务详情-软件开发 (0 个问题)
|
||||||
|
✅ 新闻列表 (0 个问题)
|
||||||
|
✅ 联系我们 (0 个问题)
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描页面: 10
|
||||||
|
通过页面: 10
|
||||||
|
失败页面: 0
|
||||||
|
问题总数: 0
|
||||||
|
|
||||||
|
📝 详细报告已保存: /Users/zhangxiang/Codes/Novalon/novalon-website/heading-hierarchy-report.json
|
||||||
|
|
||||||
|
## npm run check:brand-token
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 check:brand-token
|
||||||
|
> tsx scripts/utils/check-brand-text-token.ts
|
||||||
|
|
||||||
|
|
||||||
|
🔴 Two-Channel Red Rule 检查 — 文字通道禁用 --color-brand
|
||||||
|
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描目录: src
|
||||||
|
扫描文件: 414
|
||||||
|
违规处数: 0
|
||||||
|
|
||||||
|
✅ 文字通道全部使用 --color-brand-ink,双通道契约未被违反!
|
||||||
|
|
||||||
|
# ===== 最终树空闲复跑(chain3,含门禁脚本进程回收修复后)2026-09-22T02:42:16Z =====
|
||||||
|
|
||||||
|
## npm run check:contrast (EXIT=0)
|
||||||
|
✅ --color-brand-ink on --color-bg-primary — 6.99:1
|
||||||
|
✅ --color-brand-ink on --color-bg-secondary — 6.69:1
|
||||||
|
✅ --color-brand-ink on --color-brand-bg — 6.27:1
|
||||||
|
✅ --color-brand-ink-hover on --color-bg-primary — 10.19:1
|
||||||
|
✅ --color-brand-ink-hover on --color-brand-bg — 9.13:1
|
||||||
|
✅ #FFFFFF on --color-brand — 5.84:1
|
||||||
|
alpha 修饰正文(带 /80 等修饰类的正文)
|
||||||
|
✅ --color-text-secondary on --color-bg-primary @80% — 8.52:1
|
||||||
|
大号装饰文本(text-text-hint)× 各级卡片底色
|
||||||
|
✅ --color-text-hint on --color-bg-primary — 7.54:1
|
||||||
|
✅ --color-text-hint on --color-bg-secondary — 7.22:1
|
||||||
|
✅ --color-text-hint on --color-bg-tertiary — 6.75:1
|
||||||
|
✅ --color-text-hint on --color-bg-elevated — 6.75:1
|
||||||
|
深色区块(不随主题翻转的独立 token)
|
||||||
|
✅ --color-dark-text-primary on --color-dark-bg — 18.48:1
|
||||||
|
✅ --color-dark-text-secondary on --color-dark-bg — 13.03:1
|
||||||
|
✅ --color-dark-text-muted on --color-dark-bg — 7.54:1
|
||||||
|
✅ --color-brand-section-text on --color-brand-section — 8.93:1
|
||||||
|
|
||||||
|
📊 合计 44 组:✅ 44 / ❌ 0 不达标 / ⚠️ 0 令牌缺失
|
||||||
|
|
||||||
|
✅ 全部令牌配对满足 WCAG 2.1 AA
|
||||||
|
|
||||||
|
## npm run check:headings (EXIT=0)
|
||||||
|
(node:58761) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
🔍 检查页面标题层级...
|
||||||
|
|
||||||
|
✅ 首页 (0 个问题)
|
||||||
|
✅ 关于我们 (0 个问题)
|
||||||
|
✅ 产品中心 (0 个问题)
|
||||||
|
✅ 产品详情-ERP (0 个问题)
|
||||||
|
✅ 解决方案 (0 个问题)
|
||||||
|
✅ 解决方案详情-制造 (0 个问题)
|
||||||
|
✅ 服务列表 (0 个问题)
|
||||||
|
✅ 服务详情-软件开发 (0 个问题)
|
||||||
|
✅ 新闻列表 (0 个问题)
|
||||||
|
✅ 联系我们 (0 个问题)
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描页面: 10
|
||||||
|
通过页面: 10
|
||||||
|
失败页面: 0
|
||||||
|
问题总数: 0
|
||||||
|
|
||||||
|
📝 详细报告已保存: /Users/zhangxiang/Codes/Novalon/novalon-website/heading-hierarchy-report.json
|
||||||
|
|
||||||
|
## npm run check:brand-token (EXIT=0)
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 check:brand-token
|
||||||
|
> tsx scripts/utils/check-brand-text-token.ts
|
||||||
|
|
||||||
|
|
||||||
|
🔴 Two-Channel Red Rule 检查 — 文字通道禁用 --color-brand
|
||||||
|
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描目录: src
|
||||||
|
扫描文件: 414
|
||||||
|
违规处数: 0
|
||||||
|
|
||||||
|
✅ 文字通道全部使用 --color-brand-ink,双通道契约未被违反!
|
||||||
|
|
||||||
|
# ===== 最终树复跑(chain4:404 水印 /80 + global-error 兜底页修复后)2026-09-22T03:19:50Z =====
|
||||||
|
|
||||||
|
## npm run check:contrast (EXIT=0)
|
||||||
|
✅ --color-brand-ink on --color-bg-primary — 6.99:1
|
||||||
|
✅ --color-brand-ink on --color-bg-secondary — 6.69:1
|
||||||
|
✅ --color-brand-ink on --color-brand-bg — 6.27:1
|
||||||
|
✅ --color-brand-ink-hover on --color-bg-primary — 10.19:1
|
||||||
|
✅ --color-brand-ink-hover on --color-brand-bg — 9.13:1
|
||||||
|
✅ #FFFFFF on --color-brand — 5.84:1
|
||||||
|
alpha 修饰正文(带 /80 等修饰类的正文)
|
||||||
|
✅ --color-text-secondary on --color-bg-primary @80% — 8.52:1
|
||||||
|
大号装饰文本(text-text-hint)× 各级卡片底色
|
||||||
|
✅ --color-text-hint on --color-bg-primary — 7.54:1
|
||||||
|
✅ --color-text-hint on --color-bg-secondary — 7.22:1
|
||||||
|
✅ --color-text-hint on --color-bg-tertiary — 6.75:1
|
||||||
|
✅ --color-text-hint on --color-bg-elevated — 6.75:1
|
||||||
|
深色区块(不随主题翻转的独立 token)
|
||||||
|
✅ --color-dark-text-primary on --color-dark-bg — 18.48:1
|
||||||
|
✅ --color-dark-text-secondary on --color-dark-bg — 13.03:1
|
||||||
|
✅ --color-dark-text-muted on --color-dark-bg — 7.54:1
|
||||||
|
✅ --color-brand-section-text on --color-brand-section — 8.93:1
|
||||||
|
|
||||||
|
📊 合计 44 组:✅ 44 / ❌ 0 不达标 / ⚠️ 0 令牌缺失
|
||||||
|
|
||||||
|
✅ 全部令牌配对满足 WCAG 2.1 AA
|
||||||
|
|
||||||
|
## npm run check:brand-token (EXIT=0)
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 check:brand-token
|
||||||
|
> tsx scripts/utils/check-brand-text-token.ts
|
||||||
|
|
||||||
|
|
||||||
|
🔴 Two-Channel Red Rule 检查 — 文字通道禁用 --color-brand
|
||||||
|
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描目录: src
|
||||||
|
扫描文件: 416
|
||||||
|
违规处数: 0
|
||||||
|
|
||||||
|
✅ 文字通道全部使用 --color-brand-ink,双通道契约未被违反!
|
||||||
|
|
||||||
|
## npm run check:headings (EXIT=0)
|
||||||
|
(node:41979) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
🔍 检查页面标题层级...
|
||||||
|
|
||||||
|
✅ 首页 (0 个问题)
|
||||||
|
✅ 关于我们 (0 个问题)
|
||||||
|
✅ 产品中心 (0 个问题)
|
||||||
|
✅ 产品详情-ERP (0 个问题)
|
||||||
|
✅ 解决方案 (0 个问题)
|
||||||
|
✅ 解决方案详情-制造 (0 个问题)
|
||||||
|
✅ 服务列表 (0 个问题)
|
||||||
|
✅ 服务详情-软件开发 (0 个问题)
|
||||||
|
✅ 新闻列表 (0 个问题)
|
||||||
|
✅ 联系我们 (0 个问题)
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描页面: 10
|
||||||
|
通过页面: 10
|
||||||
|
失败页面: 0
|
||||||
|
问题总数: 0
|
||||||
|
|
||||||
|
📝 详细报告已保存: /Users/zhangxiang/Codes/Novalon/novalon-website/heading-hierarchy-report.json
|
||||||
|
|
||||||
|
# ===== 最终树复跑(chain6:header 函数式 setState + 移动端性能计时修复后)2026-09-22T03:51:41Z =====
|
||||||
|
|
||||||
|
## npm run check:contrast (EXIT=0)
|
||||||
|
✅ --color-brand-ink-hover on --color-brand-bg — 9.13:1
|
||||||
|
✅ #FFFFFF on --color-brand — 5.84:1
|
||||||
|
alpha 修饰正文(带 /80 等修饰类的正文)
|
||||||
|
✅ --color-text-secondary on --color-bg-primary @80% — 8.52:1
|
||||||
|
大号装饰文本(text-text-hint)× 各级卡片底色
|
||||||
|
✅ --color-text-hint on --color-bg-primary — 7.54:1
|
||||||
|
✅ --color-text-hint on --color-bg-secondary — 7.22:1
|
||||||
|
✅ --color-text-hint on --color-bg-tertiary — 6.75:1
|
||||||
|
✅ --color-text-hint on --color-bg-elevated — 6.75:1
|
||||||
|
深色区块(不随主题翻转的独立 token)
|
||||||
|
✅ --color-dark-text-primary on --color-dark-bg — 18.48:1
|
||||||
|
✅ --color-dark-text-secondary on --color-dark-bg — 13.03:1
|
||||||
|
✅ --color-dark-text-muted on --color-dark-bg — 7.54:1
|
||||||
|
✅ --color-brand-section-text on --color-brand-section — 8.93:1
|
||||||
|
|
||||||
|
📊 合计 44 组:✅ 44 / ❌ 0 不达标 / ⚠️ 0 令牌缺失
|
||||||
|
|
||||||
|
✅ 全部令牌配对满足 WCAG 2.1 AA
|
||||||
|
|
||||||
|
## npm run check:brand-token (EXIT=0)
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 check:brand-token
|
||||||
|
> tsx scripts/utils/check-brand-text-token.ts
|
||||||
|
|
||||||
|
|
||||||
|
🔴 Two-Channel Red Rule 检查 — 文字通道禁用 --color-brand
|
||||||
|
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描目录: src
|
||||||
|
扫描文件: 416
|
||||||
|
违规处数: 0
|
||||||
|
|
||||||
|
✅ 文字通道全部使用 --color-brand-ink,双通道契约未被违反!
|
||||||
|
|
||||||
|
## npm run check:headings (EXIT=0)
|
||||||
|
✅ 首页 (0 个问题)
|
||||||
|
✅ 关于我们 (0 个问题)
|
||||||
|
✅ 产品中心 (0 个问题)
|
||||||
|
✅ 产品详情-ERP (0 个问题)
|
||||||
|
✅ 解决方案 (0 个问题)
|
||||||
|
✅ 解决方案详情-制造 (0 个问题)
|
||||||
|
✅ 服务列表 (0 个问题)
|
||||||
|
✅ 服务详情-软件开发 (0 个问题)
|
||||||
|
✅ 新闻列表 (0 个问题)
|
||||||
|
✅ 联系我们 (0 个问题)
|
||||||
|
|
||||||
|
📊 检查摘要
|
||||||
|
扫描页面: 10
|
||||||
|
通过页面: 10
|
||||||
|
失败页面: 0
|
||||||
|
问题总数: 0
|
||||||
|
|
||||||
|
📝 详细报告已保存: /Users/zhangxiang/Codes/Novalon/novalon-website/heading-hierarchy-report.json
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# test:coverage 最终树复跑 2026-09-22T04:50:44Z
|
||||||
|
✓ does not refresh when pull distance is below threshold (2 ms)
|
||||||
|
✓ should call onSwipeLeft when progress exceeds threshold (2 ms)
|
||||||
|
✓ should call onSwipeRight when progress exceeds threshold
|
||||||
|
✓ should not trigger callbacks when progress is below threshold (1 ms)
|
||||||
|
✓ should not call vibrate when progress is below threshold (2 ms)
|
||||||
|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
||||||
|
All files | 77.92 | 84.6 | 77.64 | 77.92 |
|
||||||
|
Test Suites: 137 passed, 137 total
|
||||||
|
Tests: 1659 passed, 1659 total
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// 从 chain6 的 allure-results 里取出「本轮真正跑过的用例」按状态分组,
|
||||||
|
// 目的:给 ② 的 28 skipped 一份**具名**证据(哪些用例、在哪个 project、以什么理由跳过),
|
||||||
|
// 而不是只引用汇总行里的计数。汇总计数只能证明「数量」,证明不了「没有把失败洗成跳过」。
|
||||||
|
import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
const DIR = resolve(process.cwd() ?? '.', 'e2e/allure-results');
|
||||||
|
// chain6 功能阶段:03:52:08Z → 04:20:43Z;视觉阶段紧接其后。留出余量按 UTC 当日 03:50–04:25 过滤。
|
||||||
|
const FROM = Date.parse('2026-09-22T03:50:00Z');
|
||||||
|
const TO = Date.parse('2026-09-22T04:25:00Z');
|
||||||
|
|
||||||
|
const files = readdirSync(DIR).filter((f) => f.endsWith('-result.json'));
|
||||||
|
const rows = [];
|
||||||
|
for (const f of files) {
|
||||||
|
const p = resolve(DIR, f);
|
||||||
|
const m = statSync(p).mtimeMs;
|
||||||
|
if (m < FROM || m > TO) continue;
|
||||||
|
let j;
|
||||||
|
try {
|
||||||
|
j = JSON.parse(readFileSync(p, 'utf8'));
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const labels = {};
|
||||||
|
for (const l of j.labels || []) labels[l.name] = l.value;
|
||||||
|
rows.push({
|
||||||
|
file: f,
|
||||||
|
mtime: new Date(m).toISOString(),
|
||||||
|
status: j.status,
|
||||||
|
reason: j.statusDetails?.message || j.statusDetails?.trace || '',
|
||||||
|
project: labels.device || labels.suiteParent || '',
|
||||||
|
suite: labels.suite || '',
|
||||||
|
fullName: j.fullName || '',
|
||||||
|
name: j.name || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const byStatus = {};
|
||||||
|
for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
||||||
|
const skipped = rows.filter((r) => r.status === 'skipped').sort((a, b) => (a.fullName + a.project).localeCompare(b.fullName + b.project));
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
source: 'e2e/allure-results(chain6 最终代码树 `npm run test` 功能+视觉两阶段的 allure 落盘)',
|
||||||
|
window: { from: new Date(FROM).toISOString(), to: new Date(TO).toISOString() },
|
||||||
|
totalResults: rows.length,
|
||||||
|
byStatus,
|
||||||
|
skippedCount: skipped.length,
|
||||||
|
// 按 (suite, reason) 聚合,便于核对「每处 test.skip 条件命中了多少条」
|
||||||
|
skipTally: Object.entries(
|
||||||
|
skipped.reduce((acc, r) => {
|
||||||
|
const k = `${r.suite} :: ${r.reason.slice(0, 80)}`;
|
||||||
|
acc[k] = (acc[k] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
).map(([k, n]) => ({ group: k, count: n })),
|
||||||
|
skipped,
|
||||||
|
};
|
||||||
|
writeFileSync(
|
||||||
|
resolve(process.cwd() ?? '.', 'docs/acceptance/2026-09-21-gates/skipped-tests-final-tree.json'),
|
||||||
|
JSON.stringify(out, null, 2),
|
||||||
|
);
|
||||||
|
console.log(`window results=${rows.length} byStatus=${JSON.stringify(byStatus)}`);
|
||||||
|
console.log(`skipped=${skipped.length}`);
|
||||||
|
for (const t of out.skipTally) console.log(` x${t.count} ${t.group}`);
|
||||||
|
const nonPass = rows.filter((r) => r.status !== 'passed' && r.status !== 'skipped');
|
||||||
|
console.log(`otherNonPassed=${nonPass.length}`);
|
||||||
|
for (const r of nonPass.slice(0, 10)) console.log(` ${r.status} ${r.project} ${r.fullName} :: ${r.reason.slice(0, 120)}`);
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# 验收 §8 放行条件复跑结果(最终代码树)
|
||||||
|
|
||||||
|
生成时间:2026-09-22(UTC)。命令在**同一棵最终代码树**上串行执行,每步前后打印 :3000/:3100 端口占用数(0 才可用),
|
||||||
|
证据链编号:chain1(静态 + Lighthouse + GA4 生产复跑)→ chain2(作废,孤儿服务复用毒化)→ chain3(进程回收修复后复跑)
|
||||||
|
→ chain4/chain5/chain6(404 水印、`global-error` 兜底页、header 函数式 setState、移动端性能计时修复后,`EXIT_TEST=0` 归零轮)
|
||||||
|
→ chain7(Lighthouse 死断言处理 + axe 规则级通道)→ chain8(覆盖率棘轮 + iPhone SE 口径复跑)→ chain9(静态门禁与 Lighthouse 的第二次连续全绿)。
|
||||||
|
复跑脚本随证据存放于本目录:`chain7-dead-audit-and-rule-coverage.sh`、`chain8-coverage-and-iphone-se.sh`、`chain9-lighthouse-9urls.sh`。
|
||||||
|
|
||||||
|
## ② E2E 归零(`npm run test` = `test:functional && test:visual:all`)
|
||||||
|
|
||||||
|
| 轮次 | 结果 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| chain3 | 功能 `1048 passed / 4 failed / 28 skipped`(19.0m),端口预检 0 | 4 例均为**满负载竞态**(隔离复跑 `/tmp/repro-4.log` 33 例全过):p2 hover 下拉链接点击、p3 `isVisible→boundingBox` 两段式解析、uj-11 `scrollIntoViewIfNeeded` 撞 detach、p4 把 `waitForTimeout(1500)` 计入加载耗时 |
|
||||||
|
| chain4 | 功能 `1049 passed / 3 failed / 28 skipped`(15.7m) | 上述 4 例全过;新暴露同类 3 例:移动端 LCP 2736ms(dev 现场编译计入计时)、`website-acceptance.spec.ts:121` 与 `mobile.spec.ts:101` 找不到 `[data-testid="mobile-navigation"]` |
|
||||||
|
| chain6 | 功能 `1052 passed / 0 failed / 28 skipped`(17.4m)→ 视觉 `115 passed / 0 failed`(3.7m);`EXIT_TEST=0 2026-09-22T04:20:43Z` | **已归零**。chain4 的 3 例(移动端 LCP、两例 `mobile-navigation`)全部通过;视觉阶段真正执行(`/tmp/e2e-final9.log` 末段逐条列出 `visual-webkit-desktop` 的 115 例,含 L1 13 路由 + L2 组件态 + L3 排版色彩 + 浅色/深色主题各一)。全程未使用 `--update-snapshots`,基线未被改写。`POST_TEST_LISTENERS=0` |
|
||||||
|
|
||||||
|
chain4 的移动端两例经**根因定位后按产品缺陷修复**:`src/components/layout/header.tsx:184` 原为 `setIsOpen(!isOpen)`,
|
||||||
|
读取渲染闭包里的 `isOpen`,与抽屉的 `AnimatePresence` 退出动画(0.2s + 0.25s)叠加时高负载下丢点击 ⇒ 用户连点汉堡按钮可能打不开抽屉。
|
||||||
|
改为函数式 `setIsOpen((prev) => !prev)`。同文件同类的另外两处(`admin/login/page.tsx:80`、`admin/notifications/page.tsx:167`)
|
||||||
|
未在失败用例链路上,记为残留项交范围裁定。视觉基线未被改写(全程未使用 `--update-snapshots`)。
|
||||||
|
|
||||||
|
## ④ 静态门禁(chain6 最终树)
|
||||||
|
|
||||||
|
| 门禁 | 命令 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 类型 | `npm run type-check` | chain6 `EXIT_TYPECHECK=0`(2026-09-22T03:51:14Z,2s)。**2s 触发怀疑,已用正/负对照自证不是空跑**:临时写入 `src/__typecheck-probe.ts`(`const probe: number = "not a number"`)⇒ `tsc` 报 `error TS2322`(11.1s,冷缓存);删除后复跑干净(2.9s,`tsconfig.compilerOptions.incremental=true` 命中 `.tsbuildinfo`)。探针文件已删除,未进入代码树 |
|
||||||
|
| 单元 | `npm run test:unit` | chain6 `EXIT_UNIT=0`:**137 suites / 1659 tests 全通过**(`Time: 6.936 s`,含新增 `src/app/global-error.test.tsx` 3 例) |
|
||||||
|
| Lint | `npm run lint` | chain6 `EXIT_LINT=0`:**0 error / 106 warning**(chain4 同为 106)。⚠️ [AGENTS.md](../../../AGENTS.md) §5 记的「实测 105 条」已过期,需同步;warning 是否清零由范围裁定,见 CONTEXT.md 决策表。其中 1 条是 `Unused eslint-disable directive`(死抑制,非真问题但可清) |
|
||||||
|
| 品牌文本通道 | `npm run check:brand-token` | chain6 `EXIT=0`,双通道契约未违反 |
|
||||||
|
| 对比度(令牌配对) | `npm run check:contrast` | chain6 **合计 44 组:✅ 44 / ❌ 0 / ⚠️ 0** → `contrast-and-headings.txt`(chain3/4/6 各一段) |
|
||||||
|
| 标题层级 | `npm run check:headings` | chain6 逐页 **0 问题**,`EXIT=0` |
|
||||||
|
| 构建 | `npm run build` | chain6 `EXIT_BUILD=0`(8s 增量,路由表与 24 个 html 产物 mtime 新于 03:50 已核对,非空跑) |
|
||||||
|
| Lighthouse | `npm run lighthouse` | chain6 `EXIT=0`(21 运行,但含 3 条死断言)→ **chain7 `EXIT_LIGHTHOUSE=0`(04:50:08Z)与 chain9 `EXIT_LIGHTHOUSE=0`(05:10:58Z)连续两次:9 URL × 3 次 = 27 运行、0 error、0 条 `not a known audit`**。URL 表已补 `/products/erp-upgrade`、`/about/brand`(§7 缺口的 Lighthouse 半边),两条新路由四类目均满分(perf 100 / a11y 1.0 / bp 1.0 / seo 1.0,LCP 786ms 与 794ms) |
|
||||||
|
| axe 节点计数(⑤) | 见下节 | `passed=true` |
|
||||||
|
|
||||||
|
**chain9 二次确认(05:03–05:11Z)**:处理完死断言、改过 `config/test/lighthouserc.json` 与文档/探针之后,把 `type-check`、`lint`、`test:unit`、`check:contrast`、`check:headings`、`check:brand-token` 全量重跑,**6 项 `EXIT=0`**(lint 仍 0 error / 106 warning),前后 `LISTENERS=0`。chain9 同时是 Lighthouse 在 9 URL 配置下的第二次连续全绿。
|
||||||
|
|
||||||
|
### Lighthouse 的 3 条死断言(chain6 新发现,已在最终树处理)
|
||||||
|
|
||||||
|
`config/test/lighthouserc.json:95-97` 的 `autocomplete-valid` / `presentation-role-conflict` / `svg-img-alt` 三条 `warn` 断言,
|
||||||
|
每轮都稳定打印 `"…" is not a known audit. expected: >=1 found: 0`(chain4+chain6 合计 42 条 = 3 条 × 7 URL × 2 轮)。
|
||||||
|
两个独立信源确认它们是**恒不成立的死断言**,而非「页面恰好没问题」:
|
||||||
|
|
||||||
|
1. **运行时报告**:最新 JSON 报告(`lighthouse-reports/contact-2026_09_22_04_25_34.report.json`,`lighthouseVersion 12.6.1`)的 `audits` 键集合里没有这三个 id,而同批断言的 `color-contrast`/`heading-order`/`target-size`/`aria-conditional-attr`/`skip-link` 均在列。
|
||||||
|
2. **审计注册表**:`@lhci/cli@0.15.1` 自带的是 `lighthouse@12.6.1`(不是顶层的 13.4.1 —— 这层版本裂差本身就是「为什么本地包里没有该审计」的答案),其 `core/audits/accessibility/` 共 64 个审计文件,无此三者。
|
||||||
|
|
||||||
|
覆盖率影响并非对等,因此不能简单删掉了事:用与门禁同款的 `runOnly: {type:'tag', values:['wcag2a','wcag2aa','wcag21a','wcag21aa']}`
|
||||||
|
做正/负对照探针(`docs/acceptance/2026-09-21-axe/probe-axe-rule-coverage.mjs`,每条规则各造一个必然违规的最小节点):
|
||||||
|
|
||||||
|
| 规则 | axe 4.11.4 是否在门禁 tag 内 | 探针是否抓到违规 |
|
||||||
|
|---|---|---|
|
||||||
|
| `autocomplete-valid` | 在 | ✅ 1 node |
|
||||||
|
| `svg-img-alt` | 在 | ✅ 1 node |
|
||||||
|
| `presentation-role-conflict` | **不在**(属 `best-practice` tag) | 规则未参与运行 |
|
||||||
|
|
||||||
|
⇒ 前两条的真实兜底是 ⑤ 的 axe 通道(已 34 路由 × 4 组合 = 0 违规);第三条在 axe 通道里也测不到,必须以规则级 `runOnly` 显式补上,
|
||||||
|
否则「删死断言」会变成「悄悄拆掉一道门」。
|
||||||
|
|
||||||
|
**chain7 复验(最终处理)**:`lighthouserc.json` 删掉这 3 条;`axe-contrast-evidence.mjs` 在 tag 通道之外增加第二次
|
||||||
|
`axe.run(document, {runOnly: {type:'rules', values: EXTRA_RULES}})`,并按页记录 `extraRulesChecked` 与 `extraRuleNodes`,
|
||||||
|
把 `extraRuleNodes === 0` 与 `extraRulePagesUnderCovered === 0`(即每页三条规则确实都跑了)一并写入 `thresholds` 与 `passed`。
|
||||||
|
结果:`EXIT_AXE_FULL=0`,`Σ extraRulesChecked = 408 = 34 路由 × 4 组合 × 3 规则`(分母闭合,不是"没测到所以 0")、
|
||||||
|
`extraRuleNodes = 0`;`EXIT_LIGHTHOUSE=0` 且 chain7 段 `not a known audit` 计数 **0**(chain4+chain6 历史段共 42 条,保留作对照)。
|
||||||
|
|
||||||
|
## ⑤ 全站双引擎双主题 axe 节点计数 = 0
|
||||||
|
|
||||||
|
**chain7 最终树(`docs/acceptance/2026-09-21-axe/axe-evidence.json`,`generatedAt 2026-09-22T04:43:02Z`,axe-core 4.11.4)**
|
||||||
|
——同一棵 src 代码树,`EXIT_AXE_FULL=0`;chain6(tag 通道单跑)快照存为 `axe-evidence-chain6-tagonly.json`,chain5 前一刻快照存为 `axe-evidence-chain5-preheader-fix.json`。
|
||||||
|
|
||||||
|
| 组合 | pages | contrastNodes | violationNodes | themeMismatch | bgMismatch | extraRuleNodes | extraRulePagesUnderCovered |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| chromium / light | 34 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
|
| chromium / dark | 34 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
|
| firefox / light | 34 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
|
| firefox / dark | 34 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||||
|
|
||||||
|
`routeCount=34 rows=136(= 34 × 4 组合,分母断言)passed=true`;`Σ extraRulesChecked = 408 = 34 × 4 × 3`,
|
||||||
|
规则级通道覆盖 `autocomplete-valid` / `presentation-role-conflict` / `svg-img-alt`(后一条不在 wcag tag 集合内,只能靠它)。
|
||||||
|
|
||||||
|
**口径升级(这是 ⑤ 的关键,不是换实现)**:清单来源从「`/sitemap.xml` 的 29 条」扩为
|
||||||
|
**sitemap ∪ 预渲染产物(`dist/standalone/dist/server/app/**/*.html`)∪ 站内链接 BFS**(`crawl-sitemap.mjs`,打印 `skippedInternalDocs=1`)。
|
||||||
|
扩目录首轮(chain3)就**不是 0**:`/_not-found` 的 h1 用 `text-brand-ink` + `opacity-20` ⇒ 双引擎双主题各 1 个 `color-contrast` 节点;
|
||||||
|
`/_global-error` 是 Next 内置 500 文档(`<html id="__next_error__">` 无 `lang`、深色丢本站令牌)。
|
||||||
|
两处修复见 `docs/lessons-learned.md §5.24` 与 `CONTEXT.md` 决策行;框架内置文档按**内容**(非名字)排除并留痕,
|
||||||
|
一旦框架把本站 `global-error.tsx` 预渲染到该路径,条件即不再命中、路由自动回到清单。
|
||||||
|
|
||||||
|
历史对照:`axe-evidence-sitemap29.json`(29 页版,`passed=true`,116 行)—— 当时的 0 是真 0,但分母不足以覆盖 404 与兜底页。
|
||||||
|
|
||||||
|
## ⑥ 与 A-7 的补充复跑(chain8,最终树)
|
||||||
|
|
||||||
|
| 项 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 覆盖率棘轮(A-7) | `npm run test:coverage` → **`EXIT_COVERAGE=0`**(2026-09-22T04:50:44Z):137 套件 / 1659 例全通过,`All files` = **Stmts 77.92 / Branch 84.6 / Funcs 77.64 / Lines 77.92**,对照 `config/test/jest.config.js` 的 `coverageThreshold`(82 / 75 / 75 / 75)四项均达标。数值已回写 `AGENTS.md` §5(原记 84.71 / 77.55 / 77.89 / 77.89 是 2026-09-21 树,`global-error.tsx` 加入后微移)→ `coverage-final-tree.txt` |
|
||||||
|
| iPhone SE 口径复跑(⑥ 的仿真部分) | `EXIT_SE_AUDIT=0`(2026-09-22T04:52:07Z),证据写入**新目录** `docs/acceptance/2026-09-22-iphone-se/`(40 图 + `measurements.json` + `audit.log`),不覆盖 09-21 那份 21:07Z 的旧证据。本轮把探针路由从 4 条扩到 5 条(新增 `/no-such-route-404-check`),得 **20 行 = 5 页 × 深/浅 × 同意条 pending/dismissed**:`overflow=0`(`docScrollW == clientW == 375` 全部成立)、`themeNull=0`、`dismissed` 态 `footerBottomGap=64` 一致(R-1 的双安全区空带已消除)。`pendingBannerOverlapFooter=8` 是 `fixed bottom-*` 浮层的既定行为,可关闭后消失,与旧证据同口径。**限制不变**:这是 Playwright 仿真,条件⑥ 要的「真机 iPhone SE + 系统深色」仍只能由交付决策人实机走查 |
|
||||||
|
|
||||||
|
**新探针当场挖到的一条观察(不是回归,待裁定)**:404 路由的 `footerBottomGap` 全为 `null` ⇒ 预渲染产物 `dist/standalone/dist/server/app/_not-found.html` 里**没有 `<footer>`**(同一探针下首页有 `<footer data-testid="footer" role="contentinfo">`)。根因不是 bug:`src/app/layout.tsx` 本身不渲染 Header/Footer,页面级外壳由各页自行组合,而 `src/app/not-found.tsx` 只返回 `NotFoundContent`(自带「返回首页 / 搜索」出口)。因此**404 与根错误页无站内导航、无备案页脚**——是否给边界页补齐页头页脚属设计判断,未擅自改动,列入决策清单。
|
||||||
|
|
||||||
|
## ① 四项 P0 的回归测试归属(非快照,指向可点开的具体用例)
|
||||||
|
|
||||||
|
- A-1/A-2 权限提升与改密鉴权:`src/app/api/admin/users/route.test.ts`(11 例,含「不得借先建号再自我提权绕过」「资料字段不得先落库再被角色校验拒绝」「不得停用/降级超管」)、`src/app/api/admin/roles/route.test.ts`(9 例)、`src/lib/permissions.test.ts`(10 例)。
|
||||||
|
- A-3 存储型 XSS:`src/lib/sanitize.test.ts`(15 例)。
|
||||||
|
- A-4 上传白名单 + 魔数:`src/lib/media/upload-policy.test.ts`(扩展名/MIME 申报不符、双扩展名、`.html`/`.svg`/可执行、路径穿越、空与超限)、`src/lib/media/media-service.test.ts:60,69`(**`upload-policy` 未被 mock**,断言「HTML 载荷在落盘前即被拒绝」,因此同时是路由下游的接线回归)、`src/app/api/admin/media/route.test.ts`(12 例:401/403/400/体积/多图)。
|
||||||
|
|
||||||
|
## 已知口径限制(不掩饰)
|
||||||
|
|
||||||
|
1. **chain2 那次 `805 passed / 263 failed` 作废**:`check:headings`/`check:contrast` 的脚本只 kill 包装进程 ⇒ `next-server` 孤儿占住 :3000,被 Playwright `reuseExistingServer` 静默复用,205 个失败是 `page.goto` 超时而非断言失败。已修(`detached` + 负 PID 杀进程组 + `process.exit` 前显式收服),chain3/4/6 的 `PRE/POST_*_LISTENERS` 均为 0 即为正/负对照。
|
||||||
|
2. **满负载竞态与"隔离必过"的差别**:本地 `retries: 0`、CI `retries: 2`(`e2e/playwright.config.ts`),且 4 个 project × 多 worker 同机跑 3 个引擎。本轮不做"加 retries 掩盖",而是逐项消除竞态成因(预热、auto-retry 断言、限定 `main` 作用域、函数式 setState)。残留风险:同机并发仍可能出新的竞态例。
|
||||||
|
3. ③「Jenkins 无 `|| echo` 后连续两次全绿」只能由交付决策人触发;本地已按 Jenkinsfile 原样跑通同一命令序列(`npm run build` + `npm run test:e2e:prod`)。
|
||||||
|
4. ⑥ 已在最终树上以仿真复跑(chain8,`docs/acceptance/2026-09-22-iphone-se/`,含新增的 404 路由);`measurements.json` 的 `caveat` 仍写明「Playwright 仿真,非真机」,**真机走查只能由交付决策人完成**,本条件不得由本地证据替代。
|
||||||
|
5. ② 的「提交信息留痕」需一次经授权的提交才能落地;**本轮全部改动(含 `src/app/global-error.tsx`、header setState、4 个 spec 修订、`CONTEXT.md`/`lessons-learned.md`)均未提交**。
|
||||||
|
6. 28 skipped 的拆解见 §5.23 与下表;其中 16 条 GA4 `@critical` 已在**生产目标**下单独复跑并全部真断言(`ga4-production-run.txt`:`Running 4 tests using 4 workers` → `4 passed (7.2s)`,含 `next start` 与 `output: standalone` 的告警原文)。
|
||||||
|
7. **覆盖缺口的处理进度**:`/products/erp-upgrade` 与 `/about/brand` 此前既不在 Lighthouse URL 表也不在视觉回归的 13 条路由表里(视觉表有 `/products/erp`,与 `erp-upgrade` 是**不同路由**)——这正是 §5.22 已记过的同型缺口。**Lighthouse 半边已闭合**(chain7 起为 9 URL × 3 = 27 运行,`EXIT=0`);**视觉半边仍未闭合**,把两条路由加进 `e2e/visual-regression.spec.ts` 需要 `--update-snapshots` 重写已审批基线,属破坏性动作,须单独授权。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
=== GA4 生产目标复跑 2026-09-21T22:47:51Z ===
|
||||||
|
(node:60835) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
[2m[WebServer] [22m[33m[1m⚠[22m[39m Warning: Next.js ignored package-lock.json in /Users/zhangxiang because it is outside the current Git repository (/Users/zhangxiang/Codes/Novalon/novalon-website).
|
||||||
|
[2m[WebServer] [22m To use this directory, set `outputFileTracingRoot` in your Next.js config.
|
||||||
|
[2m[WebServer] [22m
|
||||||
|
[2m[WebServer] [22m[33m[1m⚠[22m[39m "next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.
|
||||||
|
|
||||||
|
Running 4 tests using 4 workers
|
||||||
|
|
||||||
|
(node:60886) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
(node:60883) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
(node:60885) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
(node:60884) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
|
||||||
|
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
✓ 3 [chromium] › ga4-event-tracking.spec.ts:121:7 › GA4 事件追踪验证 › TC-GA4-001: 页面浏览追踪 - 首屏由应用发出唯一一次 config @analytics @regression @critical (2.2s)
|
||||||
|
✓ 4 [chromium] › ga4-event-tracking.spec.ts:234:7 › GA4 事件追踪验证 › TC-GA4-004: 页面浏览追踪 - 客户端路由跳转触发带 page_path 的 config @analytics @regression @critical (2.5s)
|
||||||
|
✓ 1 [chromium] › ga4-event-tracking.spec.ts:141:7 › GA4 事件追踪验证 › TC-GA4-002: 联系表单提交触发 form_submit 和 conversion 事件 @analytics @regression @critical (4.1s)
|
||||||
|
✓ 2 [chromium] › ga4-event-tracking.spec.ts:199:7 › GA4 事件追踪验证 › TC-GA4-003: 同意横幅「接受所有」点击触发 button_click 事件 @analytics @regression @critical (5.1s)
|
||||||
|
|
||||||
|
4 passed (7.2s)
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 lighthouse
|
||||||
|
> lhci autorun --config=config/test/lighthouserc.json
|
||||||
|
|
||||||
|
✅ .lighthouseci/ directory writable
|
||||||
|
✅ Configuration file found
|
||||||
|
✅ Chrome installation found
|
||||||
|
Healthcheck passed!
|
||||||
|
|
||||||
|
Started a web server with "npm run start"...
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/about
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/services
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/products
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/cases
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/news
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/contact
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Done running Lighthouse!
|
||||||
|
|
||||||
|
Checking assertions against 7 URL(s), 21 total run(s)
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/ :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/about :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/services :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/products :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/cases :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/news :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/contact :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
All results processed!
|
||||||
|
|
||||||
|
Dumping 21 reports to disk at /Users/zhangxiang/Codes/Novalon/novalon-website/lighthouse-reports...
|
||||||
|
Done writing reports to disk.
|
||||||
|
|
||||||
|
Done running autorun.
|
||||||
|
|
||||||
|
===== chain6 lighthouse(最终树)2026-09-22T04:20:43Z =====
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 lighthouse
|
||||||
|
> lhci autorun --config=config/test/lighthouserc.json
|
||||||
|
|
||||||
|
✅ .lighthouseci/ directory writable
|
||||||
|
✅ Configuration file found
|
||||||
|
✅ Chrome installation found
|
||||||
|
Healthcheck passed!
|
||||||
|
|
||||||
|
Started a web server with "npm run start"...
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/about
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/services
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/products
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/cases
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/news
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/contact
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Done running Lighthouse!
|
||||||
|
|
||||||
|
Checking assertions against 7 URL(s), 21 total run(s)
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/ :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/about :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/services :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/products :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/cases :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/news :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
3 result(s) for http://localhost:3000/contact :
|
||||||
|
|
||||||
|
⚠️ autocomplete-valid warning for auditRan assertion
|
||||||
|
"autocomplete-valid" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ presentation-role-conflict warning for auditRan assertion
|
||||||
|
"presentation-role-conflict" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ svg-img-alt warning for auditRan assertion
|
||||||
|
"svg-img-alt" is not a known audit.
|
||||||
|
|
||||||
|
expected: >=1
|
||||||
|
found: 0
|
||||||
|
all values: 0, 0, 0
|
||||||
|
|
||||||
|
All results processed!
|
||||||
|
|
||||||
|
Dumping 21 reports to disk at /Users/zhangxiang/Codes/Novalon/novalon-website/lighthouse-reports...
|
||||||
|
Done writing reports to disk.
|
||||||
|
|
||||||
|
Done running autorun.
|
||||||
|
|
||||||
|
===== chain7 lighthouse(移除 3 条死断言后)2026-09-22T04:43:05Z =====
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 lighthouse
|
||||||
|
> lhci autorun --config=config/test/lighthouserc.json
|
||||||
|
|
||||||
|
✅ .lighthouseci/ directory writable
|
||||||
|
✅ Configuration file found
|
||||||
|
✅ Chrome installation found
|
||||||
|
Healthcheck passed!
|
||||||
|
|
||||||
|
Started a web server with "npm run start"...
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/about
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/services
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/products
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/cases
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/news
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/contact
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/products/erp-upgrade
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/about/brand
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Done running Lighthouse!
|
||||||
|
|
||||||
|
Checking assertions against 9 URL(s), 27 total run(s)
|
||||||
|
|
||||||
|
All results processed!
|
||||||
|
|
||||||
|
Dumping 27 reports to disk at /Users/zhangxiang/Codes/Novalon/novalon-website/lighthouse-reports...
|
||||||
|
Done writing reports to disk.
|
||||||
|
|
||||||
|
Done running autorun.
|
||||||
|
|
||||||
|
===== chain9 lighthouse(9 URL × 3 次 = 27 运行,补 erp-upgrade / about-brand)2026-09-22T05:04:19Z =====
|
||||||
|
|
||||||
|
> ruixin-website-react@1.0.0-phase1 lighthouse
|
||||||
|
> lhci autorun --config=config/test/lighthouserc.json
|
||||||
|
|
||||||
|
✅ .lighthouseci/ directory writable
|
||||||
|
✅ Configuration file found
|
||||||
|
✅ Chrome installation found
|
||||||
|
Healthcheck passed!
|
||||||
|
|
||||||
|
Started a web server with "npm run start"...
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/about
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/services
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/products
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/cases
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/news
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/contact
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/products/erp-upgrade
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Running Lighthouse 3 time(s) on http://localhost:3000/about/brand
|
||||||
|
Run #1...done.
|
||||||
|
Run #2...done.
|
||||||
|
Run #3...done.
|
||||||
|
Done running Lighthouse!
|
||||||
|
|
||||||
|
Checking assertions against 9 URL(s), 27 total run(s)
|
||||||
|
|
||||||
|
All results processed!
|
||||||
|
|
||||||
|
Dumping 27 reports to disk at /Users/zhangxiang/Codes/Novalon/novalon-website/lighthouse-reports...
|
||||||
|
Done writing reports to disk.
|
||||||
|
|
||||||
|
Done running autorun.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { test } from '@playwright/test';
|
||||||
|
|
||||||
|
// 只打印、不判定:把「fixture browserName 值」与「真实引擎(userAgent)」两件事分开测,
|
||||||
|
// 因为跳过条件读的是前者,而能力(Touchscreen API)取决于后者。
|
||||||
|
test('probe browserName identity', async ({ browserName, page }, testInfo) => {
|
||||||
|
const ua = await page.evaluate(() => navigator.userAgent);
|
||||||
|
const touchPoints = await page.evaluate(() => 'ontouchstart' in window ? (navigator.maxTouchPoints ?? 0) : -1);
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
project: testInfo.project.name,
|
||||||
|
fixtureBrowserName: browserName,
|
||||||
|
useBrowserName: (testInfo.project.use as { browserName?: string }).browserName ?? null,
|
||||||
|
skipConditionIsTrue: browserName !== 'chromium',
|
||||||
|
ua: ua.slice(0, 90),
|
||||||
|
engine: /AppleWebKit\/.*Version\//.test(ua) && !/Chrome\//.test(ua) ? 'webkit'
|
||||||
|
: /Firefox\//.test(ua) ? 'firefox'
|
||||||
|
: /Chrome\//.test(ua) ? 'chromium' : 'unknown',
|
||||||
|
touchPoints,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
// 探针:`chromium-mobile` 项目里 browserName 到底是什么、真正跑的是哪个引擎。
|
||||||
|
// 起因:最终树 28 条 skipped 有 7 条落在 chromium-mobile(cms-workflow ×3 + Touchscreen ×1 + GA4 ×4),
|
||||||
|
// 而跳过条件写成 `browserName !== 'chromium'` ⇒ 该项目内条件为真。
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: __dirname,
|
||||||
|
fullyParallel: true,
|
||||||
|
reporter: [['list']],
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'replica-chromium-mobile',
|
||||||
|
use: {
|
||||||
|
...devices['iPhone 14'],
|
||||||
|
viewport: { width: 390, height: 844 },
|
||||||
|
isMobile: true,
|
||||||
|
hasTouch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 正对照:显式写 browserName,看是否与"仅 spread devices"得到不同的 fixture 值
|
||||||
|
name: 'explicit-chromium-mobile',
|
||||||
|
browserName: 'chromium',
|
||||||
|
use: {
|
||||||
|
...devices['iPhone 14'],
|
||||||
|
viewport: { width: 390, height: 844 },
|
||||||
|
isMobile: true,
|
||||||
|
hasTouch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: 'plain-desktop', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
{
|
||||||
|
"generatedAt": "2026-09-22T05:18:05.880Z",
|
||||||
|
"source": "e2e/allure-results(chain6 最终代码树 `npm run test` 功能+视觉两阶段的 allure 落盘)",
|
||||||
|
"window": {
|
||||||
|
"from": "2026-09-22T03:50:00.000Z",
|
||||||
|
"to": "2026-09-22T04:25:00.000Z"
|
||||||
|
},
|
||||||
|
"totalResults": 1195,
|
||||||
|
"byStatus": {
|
||||||
|
"passed": 1167,
|
||||||
|
"skipped": 28
|
||||||
|
},
|
||||||
|
"skippedCount": 28,
|
||||||
|
"skipTally": [
|
||||||
|
{
|
||||||
|
"group": "cms-workflow.spec.ts :: CMS workflow test runs only in chromium",
|
||||||
|
"count": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "ga4-event-tracking.spec.ts :: 构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "p3-compatibility.spec.ts :: Touchscreen API only available in Chromium",
|
||||||
|
"count": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipped": [
|
||||||
|
{
|
||||||
|
"file": "2219cf08-fad3-4399-8ec7-90aaf2d53ae7-result.json",
|
||||||
|
"mtime": "2026-09-22T04:07:45.576Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:265:7",
|
||||||
|
"name": "admin 可完成 创建草稿 → 提交审核 → 发布 → 前台可见"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "7de4326b-8932-4c95-880a-1e80296196b4-result.json",
|
||||||
|
"mtime": "2026-09-22T04:13:35.011Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:265:7",
|
||||||
|
"name": "admin 可完成 创建草稿 → 提交审核 → 发布 → 前台可见"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "ab09e8f1-e6bf-4936-a6a8-330dbb04fc10-result.json",
|
||||||
|
"mtime": "2026-09-22T04:03:26.179Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:265:7",
|
||||||
|
"name": "admin 可完成 创建草稿 → 提交审核 → 发布 → 前台可见"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "55ec3c0e-1cce-4e62-b0a1-2812f419c924-result.json",
|
||||||
|
"mtime": "2026-09-22T04:13:35.012Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:279:7",
|
||||||
|
"name": "非法状态流转会被拦截"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "625d1024-b16b-4698-9365-bacbbbad6df8-result.json",
|
||||||
|
"mtime": "2026-09-22T04:03:26.185Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:279:7",
|
||||||
|
"name": "非法状态流转会被拦截"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "8ab7a06e-ec1f-4636-a4ec-f83311a5426e-result.json",
|
||||||
|
"mtime": "2026-09-22T04:07:45.578Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:279:7",
|
||||||
|
"name": "非法状态流转会被拦截"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "42921f30-5ef2-402c-a0cd-c75190f16bf1-result.json",
|
||||||
|
"mtime": "2026-09-22T04:03:26.185Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:297:7",
|
||||||
|
"name": "多角色权限分离:管理员配置权限 → 编辑创建/提交 → 审核员发布 → 前台可见"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "52d69dbe-2aeb-4a1c-a5b5-857f2f5cf0f1-result.json",
|
||||||
|
"mtime": "2026-09-22T04:07:45.579Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:297:7",
|
||||||
|
"name": "多角色权限分离:管理员配置权限 → 编辑创建/提交 → 审核员发布 → 前台可见"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "f4d7c0ab-7d86-41bf-9e0c-9dba8af4fcf6-result.json",
|
||||||
|
"mtime": "2026-09-22T04:13:35.013Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "CMS workflow test runs only in chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "cms-workflow.spec.ts",
|
||||||
|
"fullName": "cms-workflow.spec.ts:297:7",
|
||||||
|
"name": "多角色权限分离:管理员配置权限 → 编辑创建/提交 → 审核员发布 → 前台可见"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "08dcaad7-65e9-4f8f-ba09-8ccf9998eea6-result.json",
|
||||||
|
"mtime": "2026-09-22T04:08:01.577Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:121:7",
|
||||||
|
"name": "TC-GA4-001: 页面浏览追踪 - 首屏由应用发出唯一一次 config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "19adc24e-76cd-4c67-86e3-6dc3349ad6c0-result.json",
|
||||||
|
"mtime": "2026-09-22T04:13:53.422Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:121:7",
|
||||||
|
"name": "TC-GA4-001: 页面浏览追踪 - 首屏由应用发出唯一一次 config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "3e38da66-8835-4bd0-bcd5-4f25b9632243-result.json",
|
||||||
|
"mtime": "2026-09-22T04:03:45.432Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:121:7",
|
||||||
|
"name": "TC-GA4-001: 页面浏览追踪 - 首屏由应用发出唯一一次 config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "8e30c9be-b79a-463c-959d-3d89ecc8f79b-result.json",
|
||||||
|
"mtime": "2026-09-22T04:00:03.361Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:121:7",
|
||||||
|
"name": "TC-GA4-001: 页面浏览追踪 - 首屏由应用发出唯一一次 config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "0e4356a2-c4fe-4992-8152-7a42b02323a5-result.json",
|
||||||
|
"mtime": "2026-09-22T04:08:06.884Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:141:7",
|
||||||
|
"name": "TC-GA4-002: 联系表单提交触发 form_submit 和 conversion 事件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "19d37331-c2d4-4467-89fe-a3f1979e7a28-result.json",
|
||||||
|
"mtime": "2026-09-22T04:00:03.245Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:141:7",
|
||||||
|
"name": "TC-GA4-002: 联系表单提交触发 form_submit 和 conversion 事件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "29da23ac-66f4-4bed-b9a7-5680ee020b52-result.json",
|
||||||
|
"mtime": "2026-09-22T04:13:53.581Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:141:7",
|
||||||
|
"name": "TC-GA4-002: 联系表单提交触发 form_submit 和 conversion 事件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "d74226e3-b1ee-45e8-8ece-dd756bd2428d-result.json",
|
||||||
|
"mtime": "2026-09-22T04:03:44.905Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:141:7",
|
||||||
|
"name": "TC-GA4-002: 联系表单提交触发 form_submit 和 conversion 事件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "37e77148-e420-4c3f-ba73-e7348fbab00c-result.json",
|
||||||
|
"mtime": "2026-09-22T04:08:06.810Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:199:7",
|
||||||
|
"name": "TC-GA4-003: 同意横幅「接受所有」点击触发 button_click 事件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "868f0726-a517-48d4-953f-8a226ee4b5aa-result.json",
|
||||||
|
"mtime": "2026-09-22T04:13:52.839Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:199:7",
|
||||||
|
"name": "TC-GA4-003: 同意横幅「接受所有」点击触发 button_click 事件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "8be78b7c-be72-4d56-a8a7-e2898c54f1cb-result.json",
|
||||||
|
"mtime": "2026-09-22T04:00:04.472Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:199:7",
|
||||||
|
"name": "TC-GA4-003: 同意横幅「接受所有」点击触发 button_click 事件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "c47b32cd-0f86-46b8-98c1-641664fa84c4-result.json",
|
||||||
|
"mtime": "2026-09-22T04:03:44.635Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:199:7",
|
||||||
|
"name": "TC-GA4-003: 同意横幅「接受所有」点击触发 button_click 事件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "0c13405f-07ca-465f-babe-7694785d1386-result.json",
|
||||||
|
"mtime": "2026-09-22T04:00:06.235Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:234:7",
|
||||||
|
"name": "TC-GA4-004: 页面浏览追踪 - 客户端路由跳转触发带 page_path 的 config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "2a78aee1-234c-4b59-9d03-96e5fb749a93-result.json",
|
||||||
|
"mtime": "2026-09-22T04:03:48.732Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:234:7",
|
||||||
|
"name": "TC-GA4-004: 页面浏览追踪 - 客户端路由跳转触发带 page_path 的 config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "5ce72f00-9339-4ed9-9243-376f97720ea1-result.json",
|
||||||
|
"mtime": "2026-09-22T04:13:55.190Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:234:7",
|
||||||
|
"name": "TC-GA4-004: 页面浏览追踪 - 客户端路由跳转触发带 page_path 的 config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "be9ce18f-0ab5-4016-9697-047f0208eb00-result.json",
|
||||||
|
"mtime": "2026-09-22T04:08:08.969Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "构建未注入 GA 脚本(NEXT_PUBLIC_GA_MEASUREMENT_ID 为空)",
|
||||||
|
"project": "",
|
||||||
|
"suite": "ga4-event-tracking.spec.ts",
|
||||||
|
"fullName": "ga4-event-tracking.spec.ts:234:7",
|
||||||
|
"name": "TC-GA4-004: 页面浏览追踪 - 客户端路由跳转触发带 page_path 的 config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "2c6ec67e-4473-4796-a81b-d51ec9bef653-result.json",
|
||||||
|
"mtime": "2026-09-22T04:05:29.786Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "Touchscreen API only available in Chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "p3-compatibility.spec.ts",
|
||||||
|
"fullName": "p3-compatibility.spec.ts:226:7",
|
||||||
|
"name": "触摸滑动流畅(无卡顿)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "50fe3c7c-8d10-49b1-800d-cf504f89511f-result.json",
|
||||||
|
"mtime": "2026-09-22T04:15:12.200Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "Touchscreen API only available in Chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "p3-compatibility.spec.ts",
|
||||||
|
"fullName": "p3-compatibility.spec.ts:226:7",
|
||||||
|
"name": "触摸滑动流畅(无卡顿)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "da7b926e-4c87-4ca5-9b21-4b4d9eb0ff4a-result.json",
|
||||||
|
"mtime": "2026-09-22T04:10:56.093Z",
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "Touchscreen API only available in Chromium",
|
||||||
|
"project": "",
|
||||||
|
"suite": "p3-compatibility.spec.ts",
|
||||||
|
"fullName": "p3-compatibility.spec.ts:226:7",
|
||||||
|
"name": "触摸滑动流畅(无卡顿)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { mkdirSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
|
const BASE = process.env.BASE || 'http://localhost:3000';
|
||||||
|
const OUT = process.env.OUT || '/tmp/iphone-se-evidence';
|
||||||
|
mkdirSync(OUT, { recursive: true });
|
||||||
|
|
||||||
|
const PAGES = [
|
||||||
|
['home', '/'],
|
||||||
|
['news-detail', '/news/company-founded'],
|
||||||
|
['news-list', '/news'],
|
||||||
|
['contact', '/contact'],
|
||||||
|
// 边界页:404 的 h1 由 opacity-20 改为 text-brand-ink/80,需在 375px 窄屏(`text-8xl` 档)复核
|
||||||
|
['not-found', '/no-such-route-404-check'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const MEASURE = () => {
|
||||||
|
const footer = document.querySelector('footer, [data-testid="footer"]');
|
||||||
|
let lastTextBottom = 0;
|
||||||
|
let label = '';
|
||||||
|
if (footer) {
|
||||||
|
footer.querySelectorAll('*').forEach((el) => {
|
||||||
|
if (el.children.length || !el.textContent.trim()) return;
|
||||||
|
const b = el.getBoundingClientRect();
|
||||||
|
if (b.height > 0 && b.bottom > lastTextBottom) {
|
||||||
|
lastTextBottom = b.bottom;
|
||||||
|
label = el.textContent.trim().slice(0, 24);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const fr = footer ? footer.getBoundingClientRect() : null;
|
||||||
|
const banner = document.querySelector('[data-testid="cookie-consent-banner"]');
|
||||||
|
const br = banner ? banner.getBoundingClientRect() : null;
|
||||||
|
return {
|
||||||
|
theme: document.documentElement.getAttribute('data-theme'),
|
||||||
|
footerBottomGap: fr && lastTextBottom ? Math.round(fr.bottom - lastTextBottom) : null,
|
||||||
|
lastText: label,
|
||||||
|
footerBottomVsViewport: fr ? Math.round(fr.bottom - innerHeight) : null,
|
||||||
|
bannerTop: br ? Math.round(br.top) : null,
|
||||||
|
bannerOverlapsFooter: br && fr ? br.top < fr.bottom : false,
|
||||||
|
docScrollW: document.documentElement.scrollWidth,
|
||||||
|
clientW: document.documentElement.clientWidth,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const rows = [];
|
||||||
|
for (const theme of ['dark', 'light']) {
|
||||||
|
for (const consent of ['dismissed', 'pending']) {
|
||||||
|
const ctx = await browser.newContext({
|
||||||
|
viewport: { width: 375, height: 667 },
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
isMobile: true,
|
||||||
|
hasTouch: true,
|
||||||
|
colorScheme: theme,
|
||||||
|
...(consent === 'dismissed'
|
||||||
|
? {
|
||||||
|
storageState: {
|
||||||
|
cookies: [],
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
origin: BASE,
|
||||||
|
localStorage: [
|
||||||
|
{
|
||||||
|
name: 'novalon-cookie-preferences',
|
||||||
|
value: JSON.stringify({
|
||||||
|
necessary: true,
|
||||||
|
analytics: false,
|
||||||
|
marketing: false,
|
||||||
|
functionality: true,
|
||||||
|
timestamp: 1784284000000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
for (const [name, path] of PAGES) {
|
||||||
|
await page.goto(BASE + path, { waitUntil: 'load', timeout: 45000 });
|
||||||
|
if (consent === 'pending') await page.waitForTimeout(2600);
|
||||||
|
else await page.waitForTimeout(900);
|
||||||
|
const tag = `${theme}-${consent}-${name}`;
|
||||||
|
await page.screenshot({ path: `${OUT}/se-${tag}.png` });
|
||||||
|
// 逐帧滚到底:首页有入场分级动画,内容高度在滚动后才增长,
|
||||||
|
// 单次 scrollTo(scrollHeight) 会停在半页,测出的 footer 位置无效。
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
let last = -1;
|
||||||
|
for (let i = 0; i < 12; i += 1) {
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
await new Promise((r) => setTimeout(r, 220));
|
||||||
|
if (Math.round(window.scrollY) === last) break;
|
||||||
|
last = Math.round(window.scrollY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(700);
|
||||||
|
await page.screenshot({ path: `${OUT}/se-${tag}-footer.png` });
|
||||||
|
const m = await page.evaluate(MEASURE);
|
||||||
|
rows.push({ tag, ...m });
|
||||||
|
console.log(tag.padEnd(28), JSON.stringify(m));
|
||||||
|
}
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
`${OUT}/measurements.json`,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
target: BASE,
|
||||||
|
device: 'iPhone SE 口径仿真:viewport 375x667 / deviceScaleFactor 2 / isMobile+hasTouch / colorScheme 由 CDP 强制',
|
||||||
|
caveat: 'Playwright 仿真,非真机;用于复现 R-1/R-3 的布局与叠层判定,不能替代实机验收',
|
||||||
|
thresholds: { noHorizontalOverflow: 'docScrollW <= clientW', consentClearsFooter: 'bannerOverlapsFooter === false' },
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log(`WROTE ${OUT}/measurements.json (${rows.length} rows)`);
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
{
|
||||||
|
"generatedAt": "2026-09-21T21:07:27.236Z",
|
||||||
|
"target": "http://localhost:3000",
|
||||||
|
"device": "iPhone SE 口径仿真:viewport 375x667 / deviceScaleFactor 2 / isMobile+hasTouch / colorScheme 由 CDP 强制",
|
||||||
|
"caveat": "Playwright 仿真,非真机;用于复现 R-1/R-3 的布局与叠层判定,不能替代实机验收",
|
||||||
|
"thresholds": {
|
||||||
|
"noHorizontalOverflow": "docScrollW <= clientW",
|
||||||
|
"consentClearsFooter": "bannerOverlapsFooter === false"
|
||||||
|
},
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-home",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-news-detail",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-news-list",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-contact",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-home",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-news-detail",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-news-list",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-contact",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-home",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-news-detail",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-news-list",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-contact",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-home",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-news-detail",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-news-list",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-contact",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,232 @@
|
|||||||
|
{
|
||||||
|
"generatedAt": "2026-09-22T04:52:07.803Z",
|
||||||
|
"target": "http://localhost:3000",
|
||||||
|
"device": "iPhone SE 口径仿真:viewport 375x667 / deviceScaleFactor 2 / isMobile+hasTouch / colorScheme 由 CDP 强制",
|
||||||
|
"caveat": "Playwright 仿真,非真机;用于复现 R-1/R-3 的布局与叠层判定,不能替代实机验收",
|
||||||
|
"thresholds": {
|
||||||
|
"noHorizontalOverflow": "docScrollW <= clientW",
|
||||||
|
"consentClearsFooter": "bannerOverlapsFooter === false"
|
||||||
|
},
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-home",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-news-detail",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-news-list",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-contact",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-dismissed-not-found",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": null,
|
||||||
|
"lastText": "",
|
||||||
|
"footerBottomVsViewport": null,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-home",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-news-detail",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-news-list",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-contact",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dark-pending-not-found",
|
||||||
|
"theme": "dark",
|
||||||
|
"footerBottomGap": null,
|
||||||
|
"lastText": "",
|
||||||
|
"footerBottomVsViewport": null,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-home",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-news-detail",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-news-list",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-contact",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-dismissed-not-found",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": null,
|
||||||
|
"lastText": "",
|
||||||
|
"footerBottomVsViewport": null,
|
||||||
|
"bannerTop": null,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-home",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-news-detail",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-news-list",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-contact",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": 64,
|
||||||
|
"lastText": "蜀ICP备2026013658号",
|
||||||
|
"footerBottomVsViewport": -128,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": true,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "light-pending-not-found",
|
||||||
|
"theme": "light",
|
||||||
|
"footerBottomGap": null,
|
||||||
|
"lastText": "",
|
||||||
|
"footerBottomVsViewport": null,
|
||||||
|
"bannerTop": 461,
|
||||||
|
"bannerOverlapsFooter": false,
|
||||||
|
"docScrollW": 375,
|
||||||
|
"clientW": 375
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 127 KiB |