- 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 产物装配与部署形态。
36 KiB
测试文档
⚠ 结构时效警示(2026-08-31 全站一致性审计):本文档描述的
e2e/src/分层结构(config/data/fixtures/pages/tests 子目录、Page Object 模式)与当前代码不符——现行e2e/为扁平 spec 结构(如cases-filter.spec.ts、user-journey.spec.ts等直接位于e2e/根,配合playwright.config.ts)。Page Object 类、fixtures 等示例代码在代码库中不存在,npm scripts 以实际package.json为准。本文档保留作 Playwright 使用参考,具体结构以代码为准。
测试概述
项目使用 Playwright 进行端到端(E2E)测试,测试框架位于 e2e/ 目录(扁平 spec 结构)。
测试框架结构
e2e/
├── src/
│ ├── config/ # 配置文件
│ │ ├── environments.ts # 环境配置
│ │ └── network-configs.ts # 网络配置
│ ├── data/ # 测试数据
│ │ └── test-data.ts
│ ├── fixtures/ # 测试 Fixtures
│ │ ├── base.fixture.ts # 基础 Fixture
│ │ └── a11y.fixture.ts # 可访问性 Fixture
│ ├── pages/ # Page Object Model
│ │ ├── BasePage.ts # 基础页面
│ │ ├── HomePage.ts # 首页
│ │ ├── AboutPage.ts # 关于页
│ │ ├── CasesPage.ts # 案例页
│ │ ├── ContactPage.ts # 联系页
│ │ ├── NewsPage.ts # 新闻页
│ │ ├── ProductsPage.ts # 产品页
│ │ ├── ServicesPage.ts # 服务页
│ │ └── SolutionsPage.ts # 解决方案页
│ └── tests/ # 测试用例
│ ├── accessibility/ # 可访问性测试
│ ├── debug/ # 调试测试
│ ├── deployment/ # 部署就绪测试
│ ├── error-handling/ # 错误处理测试
│ ├── mobile/ # 移动端测试
│ ├── performance/ # 性能测试
│ ├── regression/ # 回归测试
│ ├── responsive/ # 响应式测试
│ ├── security/ # 安全测试
│ ├── smoke/ # 冒烟测试
│ ├── utils/ # 工具测试
│ └── visual/ # 视觉回归测试
├── playwright.config.ts # Playwright 配置
├── package.json
└── .env.example
测试类型
1. 冒烟测试 (Smoke Tests)
目录: e2e/src/tests/smoke/
目的: 验证核心功能是否正常工作
测试文件:
all-pages.spec.ts- 所有页面加载测试home-page.smoke.spec.ts- 首页冒烟测试contact-page.smoke.spec.ts- 联系页冒烟测试navigation.smoke.spec.ts- 导航冒烟测试
运行命令:
npm run test:smoke
# 或
npx playwright test --grep @smoke
2. 回归测试 (Regression Tests)
目录: e2e/src/tests/regression/
目的: 确保新代码没有破坏现有功能
测试文件:
contact-form.regression.spec.ts- 联系表单回归测试home-page.regression.spec.ts- 首页回归测试navigation.spec.ts- 导航回归测试
运行命令(package.json 无 test:regression 脚本,真实入口是 test:e2e:standard):
npm run test:e2e:standard # = cd e2e && npx playwright test --grep @regression
3. 性能测试 (Performance Tests)
目录: e2e/src/tests/performance/
目的: 验证页面性能指标
测试文件:
core-web-vitals.spec.ts- Core Web Vitals 测试performance.spec.ts- 通用性能测试image-loading.spec.ts- 图片加载性能interaction-performance.spec.ts- 交互性能测试
监控指标:
- LCP (Largest Contentful Paint) < 2.5s
- FID (First Input Delay) < 100ms
- CLS (Cumulative Layout Shift) < 0.1
- TTFB (Time to First Byte) < 600ms
运行命令(两条入口不是一回事,勿混用):
npm run test:performance # = k6 run tests/performance/load-test.js(压测脚本,不是 Playwright 用例;文件确在磁盘)
npx playwright test --grep @performance # 浏览器内性能用例(e2e/mobile-performance.spec.ts)
test:performance / test:stress / test:performance:api / test:performance:soak 四条都依赖本机 k6 CLI——package.json 里的 k6@0.0.0 只是 Grafana/LoadImpact 官方的「Dummy package for autocompleting k6 scripts」编辑器类型包,不含可执行文件(验收 N-10)。
4. 响应式测试 (Responsive Tests)
目录: e2e/src/tests/responsive/
目的: 验证多设备适配
测试文件:
responsive.spec.ts- 响应式布局测试mobile-interaction.spec.ts- 移动端交互测试
测试设备:
- Desktop: 1920x1080, 1366x768
- Tablet: iPad Pro (1024x1366), iPad Air (820x1180)
- Mobile: iPhone 12 (390x844), iPhone SE (375x667), Pixel 5 (393x851)
运行命令(⚠ @responsive 这个标签在当前 e2e/*.spec.ts 里零命中,--grep @responsive 会跑到 0 个用例;响应式覆盖由 project 维度承担,真实存在的入口只有这两条):
npm run test:visual:tablet # visual-chromium-tablet(平板断点全页)
npm run test:visual:mobile # visual-chromium-mobile(窄视口全页)
# 功能侧的窄视口执行量在 chromium-mobile project(iPhone 14, 390×844):
cd e2e && npx playwright test --project=chromium-mobile
5. 可访问性测试 (Accessibility Tests)
目录: e2e/src/tests/accessibility/
目的: 验证 WCAG 合规性
测试文件:
accessibility.spec.ts- 可访问性测试wcag-compliance.spec.ts- WCAG 合规测试
检查项:
- 颜色对比度 ≥ 4.5:1 (AA 级别);≥24px 大字档(含
aria-hidden的装饰序号)按 1.4.3 的 3:1 判定——axe-core 的color-contrast不因aria-hidden/role="none"/ CSSopacity豁免,装饰文字须自带合规对比度(用text-text-hint令牌) - 键盘导航支持
- 屏幕阅读器兼容
- ARIA 属性正确性
- 焦点顺序合理
- 触摸目标:硬门禁为 WCAG 2.2 AA SC 2.5.8 ≥ 24×24px(
e2e/touch-targets.ts的aaViolations,须为 0);SC 2.5.5 ≥ 44×44px 属 AAA,仅作为建议清单打印不失败。历史上曾把 44px 误标为 AA,导致门禁长期红色。
运行命令(package.json 中真实存在的脚本;本节其余示例里的 test:accessibility / test:responsive 并不存在,勿照抄):
npm run check:a11y # 静态令牌门禁伞:check:contrast + check:headings + check:brand-token(已并入 test:all)
npm run check:contrast # 只读 globals.css 令牌表做配对计算(不启服务)
npm run check:headings # 自起服务扫描渲染后的标题层级:存在 dist/standalone/server.js 时**直起 standalone**,
# 产物缺失才回退 `npm run preview` 并打印告警(验收 N-24③,见 CLAUDE.md「Build & Preview」)
npm run check:brand-token # 品牌红文字通道令牌化检查(静态扫描源码)
npx playwright test --grep @accessibility # 浏览器内 axe / 触摸目标用例
6. 安全测试 (Security Tests)
目录: e2e/src/tests/security/
目的: 验证安全防护措施
测试文件:
security.spec.ts- 通用安全测试xss-protection.spec.ts- XSS 防护测试csrf-protection.spec.ts- CSRF 防护测试
检查项:
- XSS 攻击防护
- CSRF Token 验证
- 安全头部配置
- 表单验证
运行命令(两条入口不是一回事,勿混用):
npm run test:security # = npm audit --audit-level=high && npm run test:security:headers(依赖审计 + 线上响应头脚本,不跑 Playwright 用例)
npx playwright test --grep @security # 浏览器侧的安全头用例(e2e/security-headers.spec.ts)
7. 视觉回归测试 (Visual Tests)
测试文件: e2e/visual-regression.spec.ts(L1 全页 15 页 × 5 project + L2 组件状态 + L3 主题 = --list 实测 25 例/project × 5 project = 125 例)
⚠ 三个口径别混:
VISUAL_TEST_PAGES的页面数 15、每 project 的用例数 25、磁盘上的参照 PNG 数 107(5 个 project 目录各 21 张 + 目录外manual-acceptance/2 张)。用例数 − 参照数 ≠ 0 的那部分是覆盖空洞而非「多余用例」——/products/erp-upgrade与/about/brand在两页于 5 个 project 下均无参照,另有卡片/输入框两例死在选择器上([class*="card"]零匹配、input[type="text"]的.first()命中按设计隐藏的反垃圾蜜罐)。定性见 docs/acceptance/2026-09-23-gates/final-verdict.md N-15 与 A-9。@visual标签不存在(e2e/*.spec.ts内零命中),视觉层只按 project / spec 文件选取。
Project: visual-chromium-desktop / -tablet / -mobile / visual-firefox-desktop / visual-webkit-desktop,全部 reducedMotion: 'reduce'
快照目录: e2e/visual-snapshots/{projectName}/{testFilePath}/{arg}-{projectName}{ext}(模板见 e2e/playwright.config.ts)
运行命令:
npm run test:visual # 仅 chromium 桌面
npm run test:visual:all # 5 个 project 全量
npm run test:visual:browsers # 三引擎桌面
更新快照:
npm run test:visual:all -- --update-snapshots
确定性约束(2026-09-21 验收 83 例失败的根因,详见 docs/lessons-learned.md §5.14 / §5.15):
- 快照与
E2E_TARGET无关——e2e/fixtures.ts移除next dev注入的<nextjs-portal>(左下角 devtools 指示器),否则 dev 采集的基线在 production 复核时每个全页都多出同一簇。 - 视觉阶段不得与写库用例并发——
npm run test已拆为test:functional→test:visual:all两阶段串行。CMS 用例「建条目 → revalidate → 删条目」若与截图并发,生产 server 的预渲染缓存会保留含测试条目的那一版(x-nextjs-cache: HIT),数据行删干净了页面仍是脏的。 - dev 指示器由测试夹具自己剥掉,不是靠配置关掉——
e2e/fixtures.ts的context夹具注入 init script,用document.querySelectorAll('nextjs-portal').forEach(n => n.remove())移除next dev注入的<nextjs-portal>(挂 MutationObserver 于document,因为 document start 时documentElement还是 null)。它内部 shadow DOM 的<footer class="error-overlay-footer">会被 Playwright 的 CSS 引擎穿透 open shadow root,使locator('footer')严格模式解析到 2 个元素(2026-09-21 功能 E2E 68 例失败中的 56 例)。 以下写法是错的,勿再引入:曾有文档写「e2e/playwright.config.ts的webServer.env注入NEXT_DEV_INDICATORS=off,next.config.mjs据此设devIndicators: false」——2026-09-23 全仓 grep 证实:代码里不存在该机制。NEXT_DEV_INDICATORS仅出现在文档(docs/lessons-learned.md§5.16 的历史记录)中,devIndicators在代码里的唯一出现是e2e/fixtures.ts:8的一句注释(自述「实测devIndicators:false也拿不掉」);next.config.mjs与e2e/playwright.config.ts两个字符串都没有(后者webServer.env只注入CMS_REVALIDATE_SECRET,:142-144)。该方案当年被实测回滚:设了变量后nextjs-portal数量仍为 1,因为官方文档写明false只隐藏指示器本身,而 dev 下 CSP 禁eval的报错把 overlay 常驻挂住(详见 §5.16)。 - 交互客户端组件前先
expectHydrated()(e2e/hydrated.ts)——SSR HTML 一到 DOM 就 visible,但onClick/onMouseEnter要等 hydration(实测/contact提交按钮 first visible 后 1735ms);窗口内的指针事件被丢弃且不重放,表现为动作成功、状态永不变、轮询到 timeout。 - Cookie 同意条只在「未存偏好」时渲染,而
storageState.json预置了novalon-cookie-preferences——需要覆盖同意条的用例必须先addInitScript清 localStorage,再用[data-testid="cookie-consent-banner"]定位(a[href="/privacy"]会先命中 footer 的同名链接)。 - 视觉基线口径固定为「已表达 Cookie 偏好」——同意条在挂载 2s 后才出现,而
visual-firefox-desktop/visual-webkit-desktop用空 storage state(见5a4d310),慢引擎会把它画进首屏、快引擎赶不上。visual-regression.spec.ts顶部用addInitScript预置偏好(时间戳为常量)消除该竞态,详见 §5.19。 - 重采基线前先确认
ContentItem无残留测试数据:sqlite3 "file:prisma/dev.db?mode=ro" "select modelCode,count(*) from ContentItem group by 1;"。
8. 移动端测试 (Mobile Tests)
目录: e2e/src/tests/mobile/
目的: 验证移动端功能
测试文件:
compatibility/mobile-compatibility.spec.ts- 兼容性测试gesture/mobile-gesture.spec.ts- 手势测试network/network-environment.spec.ts- 网络环境测试performance/mobile-performance.spec.ts- 移动性能测试pwa/pwa-functionality.spec.ts- PWA 功能测试mobile-ux.spec.ts- 移动端 UX 测试
视口约定:mobile-*.spec.ts 顶部必须显式 test.use({ ...devices['iPhone 14'] })。只有 chromium-mobile project 在配置里设了移动视口,而这两个 spec 同时被 chromium / firefox / webkit 桌面 project 复用——不在 spec 内钉住视口,同一份断言会有一半执行量到桌面布局(断点类名、抽屉是否挂载、触摸目标尺寸全都不同),却被当成移动端口径。
E2E 目标(默认是 dev,不是产物):e2e/playwright.config.ts:132-135 按 E2E_TARGET 切换 webServer——production 走 HOSTNAME=localhost PORT=3000 node dist/standalone/server.js(服务 output: 'standalone' 产物,next.config.mjs:44),否则走 npm run dev。不是 npm run start:Next 16 在 standalone 输出下明确告警 next start 不可用,走它即测的是不受支持的降级路径(配置内注释 :128-131)。npm run test / test:functional / test:e2e 都不设该变量,即默认打 dev server;dev 下 Next 的安全头、压缩、预取与 ISR 行为与线上不同。要走产物口径用 npm run test:e2e:prod(脚本本身已带 E2E_TARGET=production,--grep '@smoke|@critical|@journey' × 4 project),且须先自行 npm run build——webServer 只负责起服务,不代跑构建,且 standalone 目录不自含 dist/static 与 public,须按 Jenkinsfile:353-355 的装配方式先拷进去(缺了它们 /_next/static/** 全 404)。reuseExistingServer 在 production 目标下恒为 false(:139),:3000 被占用会直接中止本轮,避免静默复用陈旧服务。
⚠ dev 目标下的 skipped 口径(不可忽略):
ga4-event-tracking.spec.ts的 4 个@critical用例(TC-GA4-001..004)在默认 dev 轮次里每个 project 都 skip,4 project 共 16 个 skipped(不是 passed)。链路:NEXT_PUBLIC_GA_MEASUREMENT_ID只写在.env.production(.env/.env.local里没有)→ dev 下src/components/analytics/GoogleAnalytics.tsx:81,119提前return null→ 无gtag/js请求 → 用例内test.skip(measurementId === null, '构建未注入 GA 脚本…')触发。证据:docs/acceptance/2026-09-21-gates/skipped-tests-final-tree.json(最终树npm run test全量 1195 结果 = 1167 passed / 28 skipped,其中该组count: 16;另两组为 cms-workflow「仅 chromium 执行」9、p3「Touchscreen API 仅 Chromium」3)。产物口径的 4 passed 见同目录ga4-production-run.txt。 结论:报告 GA4 覆盖时必须引用test:e2e:prod的结果,把npm run test的「全绿」当作 GA4 已验证即为口径错误。CI 侧的对应关系:Jenkinsfile「🌐 E2E 测试」阶段(仅branch 'main')先npm run build再npm run test:e2e:prod,即产物口径在 main 上被真实执行;本地开发轮的npm run test不覆盖它。
iPhone SE 口径复核(验收 §8-⑥):375×667 / deviceScaleFactor: 2 / isMobile + hasTouch / colorScheme 强制深·浅 × 同意条 pending·dismissed × 首页 / 新闻详情 / 新闻列表 / 联系页 = 16 组,产物在 docs/acceptance/2026-09-21-iphone-se/(32 图 + measurements.json + 探针脚本 iphone-se-audit.mjs)。口径与阈值:scrollWidth == clientWidth(无横向溢出)、footerBottomGap(备案行到 <footer> 底边的留白,R-1 修复前 192–226,修复后恒为 64)、bannerOverlapsFooter(首次访客的 fixed 浮层覆盖页脚属设计行为)。复跑:OUT=/tmp/se node --input-type=module -e "$(cat docs/acceptance/2026-09-21-iphone-se/iphone-se-audit.mjs)"(需在仓库目录内执行,脚本要 import 'playwright';且需一个跑在 3000 端口的 dev server)。滚到底必须逐帧轮询:首页入场分级会持续增高文档,单次 scrollTo(0, scrollHeight) 会停在半页并测出无效 footer 位置。该口径是浏览器仿真,不替代真机走查。
axe 全站节点计数门禁(验收 §8-⑤,2026-09-23 起为常驻门禁):既有 e2e/mobile-accessibility.spec.ts 不满足该条口径——它只断言 critical/serious 的规则条数 = 0(:67),覆盖 9 个路由、单一主题,因此「同一规则下 20 个节点不达标」与「0 个节点不达标」在它眼里等价。为此单独建 scripts/accessibility/axe-node-count.mjs(由 docs/acceptance/2026-09-21-axe/axe-contrast-evidence.mjs 平移而来,证据 JSON 仍留在原目录):在 chromium + firefox 两个引擎 × light + dark 两个主题下逐页跑 axe-core 4.11.4(wcag2a/wcag2aa/wcag21a/wcag21aa),按节点数记账并落 axe-evidence.json。闸口同时成立才算过:violationNodes / color-contrast 节点数 = 0(四个组合任一 > 0 即判红)、themeMismatch = 0(回读 document.documentElement[data-theme],防「以为在量深色其实还是浅色」)、bgMismatch = 0(body 底色须等于令牌值 light rgb(255,255,255) / dark rgb(10,14,20),防样式未加载导致的「对比度意外达标」),外加规则级第二次 runOnly 的分母断言:autocomplete-valid / presentation-role-conflict / svg-img-alt 三条必须在每一页 × 每一个组合都被判定过(Σ = 路由 × 4 × 3,且 extraRulePagesUnderCovered = 0)——脚本里另有一份写死的下限清单,删掉运行列表中的一条规则会当场判红而不是悄悄少测。
分母口径(2026-09-22 升级,勿再退回单取 sitemap):清单来源是 /sitemap.xml ∪ 预渲染产物(dist/standalone/dist/server/app/**/*.html)∪ 站内链接 BFS(scripts/accessibility/crawl-routes.mjs,原 docs/acceptance/2026-09-21-axe/crawl-sitemap.mjs),最终树实测 34 条路由(axe-evidence.json:routeCount=34、rows=136 = 34 × 4 组合、passed=true)。sitemap.xml 只覆盖「希望被收录的页」,单独取用它即为口径错误——扩目录首轮当场挖出 /_not-found 与 /_global-error 两个真实缺口(见 docs/lessons-learned.md §5.24、CONTEXT.md 决策行)。历史对照:axe-evidence-sitemap29.json(29 页版,当时的 0 是真 0,但分母不足)。
退出码即门禁判定(不再只是打印 PASSED):0 通过 / 1 判红(violation 节点 > 0、主题或底色不匹配、规则级通道分母不闭合、整组响应全非 2xx 的「你在扫 404」保险丝、扫描中途异常)/ 2 输入不可用(路由清单缺失或解析出 0 条;爬虫侧同样在清单为空或 BASE 完全不可达时退出 2 且不覆盖上一份清单)。
复跑(两条脚本可零参数直跑,env 有默认值):
npm run build # 产出 dist/standalone,并由 postbuild 自动装配静态资源(见下)
PORT=3100 HOSTNAME=127.0.0.1 node dist/standalone/server.js & # 不用 npm run start:Next 对 output:'standalone' 下的 next start 明确告警不支持
npm run check:axe:routes # → /tmp/axe-routes.xml(BASE 默认 http://localhost:3100)
npm run check:axe # 读同一份清单,证据写 docs/acceptance/2026-09-21-axe/axe-evidence.json
装配这一步现在由 package.json 的 postbuild 代做,等价于手工执行:
mkdir -p dist/standalone/dist/static dist/standalone/public
cp -R dist/static/. dist/standalone/dist/static/ && cp -R public/. dist/standalone/public/ # 与 Dockerfile.prod 同一套装配
静态资源必须在起服务之前并入 standalone 根目录:server.js 启动时缓存静态索引,缺 dist/static 会让 /_next/static/** 全 404,页面落在默认黑白底上——对比度会「意外达标」,正是 bgMismatch 判据要拦的情形。脚本要 import 'playwright' 与读取 node_modules/axe-core/axe.min.js,两者已按仓库根解析(不再依赖 cwd)。注入 axe 依赖站点 CSP 含 script-src 'unsafe-inline'(next.config.mjs:29-31);若收紧该指令,此扫描与既有 axe 用例会同时失效。
✅ CI 接线(2026-09-23 落地):
Jenkinsfile新增「♿♿ 全站 axe 节点计数」阶段(仅branch 'main',与 E2E / Lighthouse 同档,因为 34 路由 × 4 组合 ≈ 14 分钟),复用前一阶段构建好的dist/standalone,按上面的顺序装配静态资源 → 起node dist/standalone/server.js(专用端口AXE_PORT,默认 3100)→ 依次npm run check:axe:routes与npm run check:axe,set -e下任一非 0 退出即判红、无|| echo;服务进程按记录到的 PID 收服(含trap … EXIT),只 kill 本阶段自己启动的进程——历史事故是只 kill 包装进程留下next-server孤儿占住 :3000,被reuseExistingServer静默复用而毒化整轮验证(docs/acceptance/2026-09-21-gates/final-tree-results.md「已知口径限制」1)。证据axe-evidence.json在post { always }归档,判红时同样留档。
📊 最近一次全链路实测(2026-09-23,装配后的 standalone 产物):axe
PASSED=true(chromium/firefox×light/dark各 34 页,bgMismatch/contrastNodes/violationNodes全为 0;唯一non2xxResponses=1是/_not-found的正确 404),Lighthouse 27 次运行断言全通过、a11y 9/9 URL 均 100。这一格在修 N-9(standalone 缺dist/static⇒ 此前两条门禁实际是在 CSS 全 404 的裸 HTML 上出数)之前是不可信的。完整判定、未闭环项与待授权清单见 docs/acceptance/2026-09-23-gates/final-verdict.md。
9. 部署就绪测试 (Deployment Tests)
目录: e2e/src/tests/deployment/
目的: 验证部署前检查
测试文件:
deployment-readiness.spec.ts- 部署就绪检查quick-check.spec.ts- 快速检查
门禁命令对照(2026-09-23 与 package.json 逐条核对)
只列 package.json 中真实存在的脚本;本节之外的示例命令(如 test:accessibility、test:responsive、test:regression、test:ui、test:debug、test:headed、test:allure*、test:ci)在当前脚本表中不存在,属历史参考;test:performance 与 test:security 虽然存在,但含义与下方各节的字面期待不同(见 §3、§6 的运行命令块)。
| 脚本 | 实际命令 | 目标 / 口径 | CI 阶段 |
|---|---|---|---|
npm run test |
test:functional + test:visual:all 串行 |
webServer = dev(npm run dev);GA4 的 16 个 @critical 实例 skipped |
— |
npm run test:functional / test:e2e |
4 功能 project | 同上(dev 目标) | — |
npm run test:e2e:prod |
E2E_TARGET=production + --grep '@smoke|@critical|@journey' × 4 project |
产物目标(node dist/standalone/server.js,非 next start);须先 npm run build 并装配 dist/static + public |
🌐 E2E 测试(仅 main) |
npm run test:critical / test:smoke / test:e2e:fast / test:e2e:standard / test:e2e:journey |
按标签 grep | 未设 E2E_TARGET ⇒ 仍是 dev 目标 |
— |
npm run test:visual / :all / :mobile / :tablet / :browsers |
视觉 project 子集 | 快照与目标无关(见 §7 约束 1) | 👁️ 视觉回归(仅 main,跑 test:visual) |
npm run check:a11y |
check:contrast → check:headings → check:brand-token |
令牌/标题/双通道红三条静态门禁;已并入 test:all |
♿ 可访问性门禁(逐条调用,全分支) |
npm run check:axe / check:axe:routes |
node scripts/accessibility/{axe-node-count,crawl-routes}.mjs |
全站路由清单(三源并集)+ 双引擎双主题 axe 节点计数与三条规则级通道的分母断言;退出码 0/1/2(2 = 清单缺失或 0 条)。需先有 node dist/standalone/server.js 在 :3100;未被 test:all 引用 |
♿♿ 全站 axe 节点计数(仅 main,复用「🏗️ 构建 dist」的产物) |
npm run test:all |
type-check + lint + test:coverage + test:integration:real + check:a11y + test:e2e:fast + test:security:headers(package.json:48 逐字) |
一键本地门禁(其 E2E 段仍是 dev 目标);CI 不按此脚本跑,见上表「CI 集成」 | — |
npm run test:unit / test:coverage / test:coverage:check |
jest(阈值单一真源 config/test/jest.config.js,根 jest.config.js 仅转发) |
单元 + 覆盖率棘轮 | 🧪 单元测试 |
npm run lighthouse |
lhci autorun --config=config/test/lighthouserc.json |
性能 + axe 断言 | ⚡ Lighthouse(仅 main,先 build) |
npm run test:security / test:security:headers |
npm audit --audit-level=high + 头部检查 |
依赖与响应头 | 🔒 安全扫描 |
Page Object Model
基础页面类
// e2e/src/pages/BasePage.ts
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async navigate(path: string) {
await this.page.goto(path);
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(name: string) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
}
首页 Page Object
// e2e/src/pages/HomePage.ts
import { BasePage } from './BasePage';
export class HomePage extends BasePage {
readonly heroSection: Locator;
readonly servicesSection: Locator;
readonly productsSection: Locator;
constructor(page: Page) {
super(page);
this.heroSection = page.locator('[data-testid="hero-section"]');
this.servicesSection = page.locator('#services');
this.productsSection = page.locator('#products');
}
async goto() {
await this.navigate('/');
await this.waitForPageLoad();
}
async scrollToSection(sectionId: string) {
await this.page.locator(`#${sectionId}`).scrollIntoViewIfNeeded();
}
}
使用示例
// e2e/src/tests/smoke/home-page.smoke.spec.ts
import { test, expect } from '@playwright/test';
import { HomePage } from '../../pages/HomePage';
test.describe('首页冒烟测试', () => {
let homePage: HomePage;
test.beforeEach(async ({ page }) => {
homePage = new HomePage(page);
await homePage.goto();
});
test('首页加载成功', async () => {
await expect(homePage.heroSection).toBeVisible();
});
});
测试配置
Playwright 配置
// e2e/playwright.config.ts
export default defineConfig({
testDir: './src/tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : '50%',
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
['allure-playwright'],
],
timeout: 90000,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
headless: true,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
],
webServer: {
command: 'cd .. && npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
环境配置
// e2e/src/config/environments.ts
export interface Environment {
name: string;
baseURL: string;
retries: number;
trace: 'on' | 'off' | 'on-first-retry';
screenshot: 'on' | 'off' | 'only-on-failure';
video: 'on' | 'off' | 'on-first-retry';
headless: boolean;
slowMo: number;
}
export const environments: Record<string, Environment> = {
development: {
name: 'development',
baseURL: 'http://localhost:3000',
retries: 0,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
headless: true,
slowMo: 0,
},
production: {
name: 'production',
baseURL: 'https://www.novalon.cn',
retries: 2,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
headless: true,
slowMo: 0,
},
};
export function getEnvironment(): Environment {
const env = process.env.TEST_ENV || 'development';
return environments[env];
}
运行测试
本地运行
# 依赖装在仓库根:e2e/ 下**没有** package.json(`cd e2e && npm install` 会报 ENOENT / 装出空目录),
# Playwright 与全部 reporter 都是根 package.json 的 devDependencies
npm ci # 或 npm install
npx playwright install # 安装 chromium / firefox / webkit 浏览器二进制
# 运行所有测试(功能 4 project → 视觉 5 project 串行;webServer 默认是 dev server)
npm run test
# 以下 playwright 命令须在 e2e/ 目录内执行(配置文件与 spec 同目录,根目录直接跑找不到 config)
cd e2e
npx playwright test path/to/test.spec.ts
npx playwright test --grep @smoke
# UI / 调试 / 有头模式:package.json 里没有 test:ui / test:debug / test:headed,
# 这三条此前是「读起来像真的」的假命令,跑必报 Missing script;正确写法是 Playwright 原生 flag
npx playwright test --ui
npx playwright test --debug
npx playwright test --headed
CI 环境运行
# CI 模式(禁止 only、增加重试)
CI=true npx playwright test
测试报告
HTML 报告
npx playwright show-report
Allure 报告
# Allure 报告:注意 package.json 未定义任何 test:allure* 脚本,
# 且 allure-commandline **不在 dependencies/devDependencies 里**(node_modules/allure-commandline 不存在),
# 因此旧版文档给的三条 `test:allure*` 脚本全部不可执行(package.json 里查无此名)。当前仓库真实产出的是
# e2e/allure-results/*.json(由 Playwright reporter 写盘)。要看报告需先自行安装 CLI:
npx -y allure-commandline generate e2e/allure-results -o e2e/allure-report --clean
npx -y allure-commandline open e2e/allure-report
JUnit 报告
用于 CI 集成,输出到 test-results/junit.xml。
测试最佳实践
1. 使用数据测试 ID
// 组件中
<div data-testid="hero-section">
// 测试中
await page.locator('[data-testid="hero-section"]')
2. 等待策略
// 等待元素可见
await expect(locator).toBeVisible();
// 等待网络空闲
await page.waitForLoadState('networkidle');
// 等待特定响应
await page.waitForResponse('**/api/contact');
3. 避免硬编码等待
// 不推荐
await page.waitForTimeout(1000);
// 推荐
await expect(locator).toBeVisible();
4. 使用 Fixtures
// e2e/src/fixtures/base.fixture.ts
import { test as base } from '@playwright/test';
import { HomePage } from '../pages/HomePage';
export const test = base.extend<{
homePage: HomePage;
}>({
homePage: async ({ page }, use) => {
const homePage = new HomePage(page);
await use(homePage);
},
});
5. 测试隔离
test.describe('测试组', () => {
test.beforeEach(async ({ page }) => {
// 每个测试前的初始化
});
test.afterEach(async ({ page }) => {
// 每个测试后的清理
});
});
CI 集成(本仓库的真实流水线)
本仓库的 CI 只有一套:根目录 Jenkinsfile(Jenkins 声明式 Pipeline)。 以下系统在本仓库不存在,历史文档若引用即为假事实,已删除:
.woodpecker.yml/ Woodpecker CI —— 磁盘上无此文件- GitLab CI(
pipeline:/image:语法)—— 无.gitlab-ci.yml;本节此前那段image: node:18-alpine且调用一个不存在的脚本(test:ci)的片段三重过期(CI 系统、node 版本、脚本名都不对),已整段移除 - GitHub Actions ——
.github/目录不存在 .gitea/存在但只有PULL_REQUEST_TEMPLATE.md(PR 模板,供scripts/check-pr-checklist.sh校验),不是 workflow 定义
Jenkinsfile 实际阶段与命令(when { branch 'main' } 标 ★,其余全分支执行;失败处理已统一为 set -e / || exit 1,历史上吞错的 || echo 只剩环境探测块,见验收 A-5):
| 阶段 | 命令 | 说明 |
|---|---|---|
| 🔧 环境检测与准备 | node -v / npm -v / rsync、ssh、curl 探测 |
探测类 ` |
| 📥 安装依赖 | npm ci →(失败兜底)npm install --legacy-peer-deps |
⚠ 该块先 rm -rf node_modules package-lock.json 再 npm ci,故 lockfile 从未真正生效(验收 N-26②,未修待决策) |
| 🔍 代码质量检查 → ESLint / TypeScript | npm run lint / npm run type-check |
判定口径 0 error(warning 不判红) |
| 🧪 单元测试 | CI=true npm run test:coverage:check |
覆盖率阈值真源 config/test/jest.config.js |
| 🌐 E2E 测试 ★ | npm run build → npm run test:e2e:prod |
产物目标;GA4 的 16 例只在这里断言 |
| 👁️ 视觉回归测试 ★ | npm run test:visual |
只跑桌面 chromium 一个 project,另四个视觉 project 在 CI 从不执行(验收 N-28) |
| ♿ 可访问性门禁 | check:contrast → check:brand-token → check:headings |
三条静态门禁,全分支 |
| ⚡ Lighthouse 性能与无障碍 ★ | npm run build → npm run lighthouse |
lhci 自起产物服务并断言 |
| 🧬 变异测试 ★ | npm run test:mutation:quick |
仅 src/lib/utils.ts;post 清 .stryker-tmp |
| 🔒 安全扫描 ★ | npm audit --audit-level=high → npm run test:security:headers |
⚠ 该脚本默认打线上 https://novalon.cn,与阶段上方「检查本分支构建产物」的打印相反(验收 N-23,未修待决策) |
| 🏗️ 构建 dist | npm run build:clean + 产物存在性/文件数校验 |
post { success } 归档 dist/** |
| ♿♿ 全站 axe 节点计数 ★ | node dist/standalone/server.js(AXE_PORT,默认 3100)→ OUT=$ROUTES npm run check:axe:routes → SITEMAP=$ROUTES npm run check:axe |
复用上一阶段产物;按记录 PID 收服,只 kill 本阶段自起的进程 |
| 🚀 部署到生产环境 ★ | ./scripts/deploy.sh deploy --skip-build(失败自动 rollback) |
需 DEPLOY_TO_PRODUCTION 参数 |
本地一键门禁对应关系:npm run test:all = type-check + lint + test:coverage + test:integration:real + check:a11y + test:e2e:fast + test:security:headers;CI 不跑 test:all(它按上表逐阶段拆开跑),两者集合不同——test:all 里的 E2E 是 dev 目标,CI 的 E2E 是产物目标。
调试技巧
1. Trace Viewer
# 运行测试并生成 trace
npx playwright test --trace on
# 查看 trace
npx playwright show-trace trace.zip
2. 截图和视频
// 手动截图
await page.screenshot({ path: 'debug.png' });
// 元素截图
await locator.screenshot({ path: 'element.png' });
// 全页截图
await page.screenshot({ path: 'full.png', fullPage: true });
3. 控制台日志
// 监听控制台
page.on('console', msg => console.log(msg.text()));
// 监听页面错误
page.on('pageerror', error => console.error(error));
4. Playwright Inspector
npx playwright test --debug