name: Regression # 每次 push / PR 自动跑回归。 # 所有检查都在 Node 20 上执行;playwright 仅作为 CI 开发依赖。 on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: permissions: contents: read jobs: regression: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v4 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' - name: Cache playwright browsers uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('package.json', 'package-lock.json') }} restore-keys: | ${{ runner.os }}-playwright- - name: Install dependencies run: npm ci - name: Install playwright chromium run: npx --no-install playwright install --with-deps chromium # npm run verify:nav 的断言依赖字体度量(7 条链接的文字宽度决定顶栏装不装得下)。 # 缺 CJK 字体时中文会退化成豆腐块或拉丁回退,宽度随之改变 → 断言假失败。 - name: Install CJK fonts run: sudo apt-get update -qq && sudo apt-get install -y -qq fonts-noto-cjk - name: Verify zero runtime dependencies run: | node -e " const p = require('./package.json'); const deps = Object.keys(p.dependencies || {}); if (deps.length) { console.error('FAIL: 运行时依赖必须为空,实际: ' + deps.join(', ')); process.exit(1); } console.log('zero-dep OK'); " - name: Start dev server run: | node site/dev-server.js & for i in $(seq 1 30); do if curl -sf http://127.0.0.1:3311/site/data.json > /dev/null; then echo "server ready after ${i} attempt(s)" exit 0 fi sleep 0.5 done echo "FAIL: dev-server 未在 15 秒内就绪" exit 1 # 三套分发产物都要能重建:PC(dist/)+ 移动端(dist/mobile/)+ PC×uni-app(dist/uniapp-pc/)。 # dist/ 不入库,所以必须先构建,后面的隔离门禁(C 组)才检查得到真实产物。 - name: Build distribution artifacts run: npm run build # 移动端的 site/m 与 tests/mobile 是**生成物且入库**(与 PC 的 site/components、tests/.html 同策略: # 部署直接拷仓库,不跑构建)。因此必须证明「仓库里的版本 = 构建能复现的版本」, # 否则会出现「改了索引忘了重跑构建」→ 部署的仍是旧页面,且没有任何报错。 - name: Assert mobile artifacts are reproducible run: | if ! git diff --quiet -- site/m tests/mobile; then echo "FAIL: 移动端生成物与仓库内已提交版本不一致,请重跑 npm run build:mobile 并提交" git diff --stat -- site/m tests/mobile exit 1 fi echo "mobile artifacts reproducible" # 隔离这种"没发生的事"只能靠断言证明:PC 的目录/数据/测试/分发里不得出现任何移动端痕迹, # 移动端索引声明的 6 端文件必须齐全。28 条断言,见 tools/verify-mobile-isolation.mjs。 - name: Verify mobile isolation run: npm run verify:isolation # uni-app 端(移动端 18 个 + PC 端 3 个试点):SFC 结构 / node --check 语法 / 禁 DOM API / # 手势必须 touch 事件 / 类名与令牌前缀按平台隔离 / 移动 rpx、PC px。 - name: Verify uni-app ends run: npm run verify:uniapp # uni-app **真实编译**(H5 + 微信小程序):静态门禁只能证明「源码长得合规」, # 证明不了「编译器接受它」——本仓库有两次「静态全绿却编译不过」的实例 # (RangeQuickPicker 的 const 重赋值、CodeInput 的 emit 遮蔽),都是真编译才暴露的。 # 依赖装在隔离目录 .tmp/uniapp-build,主 job 的依赖不含 @dcloudio; # 未缓存时**跳过并告警**而不是红:那是环境问题不是代码问题(脚本自身退出码 2)。 - name: Verify uni-app real build run: | if [ -d .tmp/uniapp-build/node_modules/@dcloudio ]; then node tools/verify-uniapp-build.mjs else echo "::warning::uni-app 工具链未缓存,跳过真实编译验证(安装命令见 tools/verify-uniapp-build.mjs 头部)" fi # 移动端文档站的完整性(生成物的"缺内容"不会报错,只会悄悄少一块): # 组件页 14 个小节齐全 / 侧栏列全组件且恰好一个高亮 / **契约 API 与 4 个框架端源码逐名一致** # / 每端代码块非空 / 契约变体类名在 CSS 里真实存在 / 页面壳无残留占位符。 - name: Verify mobile docs completeness run: npm run verify:mobile-docs - name: Verify i18n run: node tools/verify-i18n.mjs # API 表说明的出处可溯性:每条说明都要能指认来源,「规格」类必须是规格原文逐字引用 # (判据与反例见 tools/verify-api-docs.mjs 头部)。 - name: Verify API docs provenance run: npm run verify:api-docs - name: Verify site routing run: node tools/verify-site-routing.mjs # 无 # 路由(History API)只在真浏览器里成立:服务器回落 + SPA 壳 + # 资源绝对地址 + pushState/历史四件事得同时对,静态检查只能证明文件里有那几行。 - name: Verify site routes in browser run: node tools/verify-site-routes.mjs - name: Verify cross-platform structure run: node tools/verify-cross-platform.mjs - name: Verify theme modes run: node tools/verify-theme.mjs # 模板页库(S4-P11):4 个典型后台页的浏览器实测 —— 令牌生效 / 非白屏 / 零控制台错误 / # 逐页交互(表单校验、筛选分页、批量操作、抽屉、开关、保存与放弃)/ 375–1024px 无横向溢出。 # 静态检查证明不了「交互可用」,而模板页的价值恰恰全在交互上。 - name: Verify page templates run: npm run verify:templates # 演示帧内「覆盖型浮层」必须铺满真实视口:模态 / 图片预览 / 全屏弹窗 / 加载遮罩的 # position:fixed 以帧视口为包含块,而演示帧是按内容高度撑开的 —— 「点击图片只在 # 演示框内全屏」就是本脚本要拦的缺陷。含反向用例:贴边浮层与水印不得被提升。 - name: Verify stage overlay lift run: npm run verify:stage-overlay - name: Run regression run: node tools/run-regression.mjs # 移动端回归:同引擎同口径,不同数据源(site/m/data.mobile.json)与报告(tests/mobile-report.json)。 # 触控行为断言(滑动/下拉/标签切换)只在移动端行为库里,PC 侧不加载、不受影响。 - name: Run mobile regression run: npm run regression:mobile # 移动端文档站/测试站的浏览器实测:本次实测踩到「组件文档页资源路径少一级 → 样式与演示帧静默 404, # 页面文字照常显示、结构断言全过」。四类取值只能在真浏览器里拿到:HTTP 状态、控制台错误、 # 演示帧是否真有内容(不是 404 页)、令牌是否解析成 44px。 - name: Verify mobile site in browser run: npm run verify:mobile-site - name: Smoke test site run: npm run smoke:site # 顶栏窄屏折叠(≤1366px 汉堡 + 抽屉):18 个宽度 × 中英双语 + 抽屉交互,约 35 秒。 # 必须在浏览器里跑 —— 元素"可见"不等于"点得到":遮罩层叠、visibility 过渡 # 都会让点击静默失效,而矩形与可见性断言全都会通过(改动前实测踩中两个)。 - name: Verify responsive nav run: npm run verify:nav # 「在线测试」页把用户编辑的 HTML 灌进 iframe 跑,是站上唯一的不可信内容入口。 # 安全边界(不透明源 / sandbox 权限 / 两条 CSP 取交集)与预览帧的资源基准 # (srcdoc 继承父页 URL,演示源码里的 ./Xxx.css 会解析错)都只在真浏览器里可判: # 静态读代码只能看到属性字符串,看不到"到底拦没拦住、样式落在哪个 URL"。 - name: Verify playground run: npm run verify:playground - name: Upload report uses: actions/upload-artifact@v4 if: always() with: name: regression-report path: | tests/report.json tests/report-junit.xml tests/cross-platform-report.json tests/mobile-report.json tests/mobile-report-junit.xml retention-days: 30 - name: Publish test summary if: always() run: | if [ -f tests/report.json ]; then node -e " const r = require('./tests/report.json'); const a = r.assertions; console.log('## 回归结果(PC)'); console.log(''); console.log('| 项 | 值 |'); console.log('|---|---|'); console.log('| 通过率 | ' + r.passRate + '% |'); console.log('| 断言 | ' + a.pass + '/' + (a.pass + a.fail) + ' |'); console.log('| N/A | ' + (a.na || 0) + ' |'); console.log('| 页面全通过 | ' + r.pagesAllPass + '/' + r.pages + ' |'); console.log('| 超时 | ' + ((r.timedOut || []).length) + ' |'); " >> "$GITHUB_STEP_SUMMARY" fi if [ -f tests/mobile-report.json ]; then node -e " const r = require('./tests/mobile-report.json'); const a = r.assertions; console.log(''); console.log('## 回归结果(移动端)'); console.log(''); console.log('| 项 | 值 |'); console.log('|---|---|'); console.log('| 通过率 | ' + r.passRate + '% |'); console.log('| 断言 | ' + a.pass + '/' + (a.pass + a.fail) + ' |'); console.log('| N/A | ' + (a.na || 0) + ' |'); console.log('| 页面全通过 | ' + r.pagesAllPass + '/' + r.pages + ' |'); console.log('| 超时 | ' + ((r.timedOut || []).length) + ' |'); " >> "$GITHUB_STEP_SUMMARY" fi