test(web): 补两类守卫,专抓「组件 setup 期抛异常」这类既有测试抓不到的问题
背景:基础数据「运动员」页整块空白的根因是 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 不是本仓库依赖(离线开发装不上),脚本缺依赖时会明确报错退出而不是假装通过。
This commit is contained in:
1 parent
8a27f5a4fb
commit
db94d9c119
4 files changed
+336
-1
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { scanWebSources, findOrderViolations } from '../../../scripts/check-declaration-order.mjs'
|
||||
|
||||
/**
|
||||
* 声明顺序守卫(见 scripts/check-declaration-order.mjs 的说明)。
|
||||
*
|
||||
* 守护的事故:AthletesSection 的 `watch(filteredIds, ...)` 会同步求值一次,
|
||||
* 而 filteredIds 的初始化式引用了后面才声明的 filtered → setup 抛 TDZ,
|
||||
* 页面整块空白。这个缺陷躲过了 vue-tsc 与全部既有单元测试(组件从不真正挂载)。
|
||||
*/
|
||||
test('注册期求值的表达式不得引用后面才声明的 const(间接依赖也要查)', () => {
|
||||
const broken = `<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
const filteredIds = computed(() => filtered.value.map((a) => a.id))
|
||||
watch(filteredIds, () => {})
|
||||
const filtered = computed(() => [])
|
||||
</script>`
|
||||
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 = `<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
const filtered = computed(() => [])
|
||||
const filteredIds = computed(() => filtered.value.map((a) => a.id))
|
||||
watch(filteredIds, () => {})
|
||||
</script>`
|
||||
assert.deepEqual(findOrderViolations('Fixed.vue', fixed), [])
|
||||
})
|
||||
|
||||
test('函数局部的 const 不算违规(只看脚本块顶层的声明)', () => {
|
||||
const localConst = `<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
const source = ref(0)
|
||||
const ids = computed(() => {
|
||||
const local = source.value * 2
|
||||
return [local]
|
||||
})
|
||||
watch(ids, () => {})
|
||||
</script>`
|
||||
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'),
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user