From db94d9c1190cbba3e91187dec2697bf3352aab2b Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 11 Sep 2026 22:58:19 +0800 Subject: [PATCH] =?UTF-8?q?test(web):=20=E8=A1=A5=E4=B8=A4=E7=B1=BB?= =?UTF-8?q?=E5=AE=88=E5=8D=AB=EF=BC=8C=E4=B8=93=E6=8A=93=E3=80=8C=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=20setup=20=E6=9C=9F=E6=8A=9B=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E3=80=8D=E8=BF=99=E7=B1=BB=E6=97=A2=E6=9C=89=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=8A=93=E4=B8=8D=E5=88=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:基础数据「运动员」页整块空白的根因是 setup 期 TDZ(watch 注册时同步求值, 间接引用到后面才声明的 const),vue-tsc 与 449 条单元测试都没抓到——组件从不真正挂载。 1) scripts/check-declaration-order.mjs + src/shared/tests/declaration-order.test.ts 静态检查:从 watch/watchEffect 的源出发沿 const 依赖链做传递闭包, 任何声明行晚于求值行的 const 即判违规。已随既有测试命令运行(纯 node,无需浏览器)。 变异验证:把 AthletesSection 退回 ab7662b 的写法 → 精确报 「AthletesSection.vue:84 会求值到 filtered,但它声明在第 272 行」。 2) scripts/smoke-admin.mjs(pnpm smoke) 真 Chromium 冒烟:14 个管理端路由 + 7 个基础数据分区 + 大屏页, 任何 console 错误或未捕获异常即失败并给出路由名。 变异验证:同一份代码下修复版通过、退回版报 「/base-data › 运动员 ← ReferenceError: Cannot access 'Y' before initialization」。 playwright 不是本仓库依赖(离线开发装不上),脚本缺依赖时会明确报错退出而不是假装通过。 --- web/package.json | 3 +- web/scripts/check-declaration-order.mjs | 167 ++++++++++++++++++ web/scripts/smoke-admin.mjs | 111 ++++++++++++ .../shared/tests/declaration-order.test.ts | 56 ++++++ 4 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 web/scripts/check-declaration-order.mjs create mode 100644 web/scripts/smoke-admin.mjs create mode 100644 web/src/shared/tests/declaration-order.test.ts diff --git a/web/package.json b/web/package.json index 7371884..54ac2a4 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", - "preview": "vite preview" + "preview": "vite preview", + "smoke": "node scripts/smoke-admin.mjs" }, "dependencies": { "pinia": "^4.0.3", diff --git a/web/scripts/check-declaration-order.mjs b/web/scripts/check-declaration-order.mjs new file mode 100644 index 0000000..5ae40be --- /dev/null +++ b/web/scripts/check-declaration-order.mjs @@ -0,0 +1,167 @@ +/** + * 声明顺序守卫:抓「setup 期就会求值的表达式(含它的间接依赖)引用了后面才声明的 const」这一类 TDZ。 + * + * 背景:AthletesSection.vue 里 `filteredIds = computed(() => filtered.value...)` 引用了 + * 199 行之后才声明的 `filtered`,而 `watch(filteredIds, ...)` 在注册时会**同步求值一次源** —— + * setup 阶段就访问到 TDZ 中的 `filtered`,抛 + * `ReferenceError: Cannot access 'filtered' before initialization`,整个分区渲染失败、 + * 页面只剩标签栏。这个缺陷躲过了 vue-tsc 与全部单元测试(没有 jsdom,组件从不真正挂载), + * 在 dev_0.1.6 / 0.1.7 / 0.1.8 三个版本里都没被发现,所以单独做一个静态检查。 + * + * 规则(保守,只报确定会炸的): + * 1. `watch(...)` / `watchEffect(...)` 是**注册时同步求值**的(Vue 要取初始值); + * 2. 从它的第一个实参出发,沿「const 初始化式里引用了哪些 const」这条边做**传递闭包**; + * 3. 闭包里任何 const 的声明行落在 watch 调用行**之后**,就是一次必然的 TDZ。 + * 直接依赖与间接依赖都要查 —— 只查直接依赖会漏掉本次事故(引用的 filteredIds 声明在前面)。 + * + * 只扫 `` + const violations = findOrderViolations('Broken.vue', broken) + assert.ok(violations.length > 0, '应当报出 watch 求值到后声明的 filtered') + assert.equal(violations[0].name, 'filtered') + assert.ok(violations[0].declaredAt > violations[0].line, '声明行必须晚于求值行') +}) + +test('声明顺序正确时不报', () => { + const fixed = `` + assert.deepEqual(findOrderViolations('Fixed.vue', fixed), []) +}) + +test('函数局部的 const 不算违规(只看脚本块顶层的声明)', () => { + const localConst = `` + assert.deepEqual(findOrderViolations('Local.vue', localConst), []) +}) + +test('整个 web/src 目录当前没有这类写法', () => { + const root = new URL('../../', import.meta.url).pathname + const violations = scanWebSources(root) + assert.deepEqual( + violations, + [], + violations.map((v) => `${v.file}:${v.line} 会求值到 ${v.name}(声明于第 ${v.declaredAt} 行)`).join('\n'), + ) +})