From 7065dbb53e92c8003fb2e8ebad2ef78483c929ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 12:14:17 +0800 Subject: [PATCH] =?UTF-8?q?task-3(=E5=A3=B3=E5=B1=82/=E8=B7=AF=E7=94=B1):?= =?UTF-8?q?=20=E5=AE=9A=E4=B9=89=E5=B9=B6=E9=94=81=E5=AE=9A=20AdminLayout?= =?UTF-8?q?=20=E9=A1=B5=E9=9D=A2=E5=AE=B9=E5=99=A8=E8=81=8C=E8=B4=A3?= =?UTF-8?q?=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 壳层只做页面容器:唯一 RouterView 挂载业务视图、不静态 import 业务页面、 不直接调后台 API、Element Plus 按需命名导入;页面接线归 router 懒加载。 配套 tests/helpers.ts 提供只读源码分析与 @/ import 可解析校验。 --- admin-frontend-vue/tests/helpers.ts | 63 +++++++++++++++++++++++ admin-frontend-vue/tests/task-3.test.ts | 67 +++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 admin-frontend-vue/tests/helpers.ts create mode 100644 admin-frontend-vue/tests/task-3.test.ts diff --git a/admin-frontend-vue/tests/helpers.ts b/admin-frontend-vue/tests/helpers.ts new file mode 100644 index 00000000..91224c7c --- /dev/null +++ b/admin-frontend-vue/tests/helpers.ts @@ -0,0 +1,63 @@ +import { existsSync, readFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' + +const ROOT = process.cwd() +const SRC = join(ROOT, 'src') +const EXTENSIONS = ['', '.ts', '.tsx', '.vue', '.js', '.json'] +const INDEX_CANDIDATES = ['/index.ts', '/index.tsx', '/index.vue', '/index.js'] + +/** 读取工程内源码文件(相对工程根)。 */ +export function readSource(rel: string): string { + return readFileSync(join(ROOT, rel), 'utf8') +} + +/** 统计非重叠子串出现次数(幂等、只读)。 */ +export function occurrences(text: string, token: string): number { + if (!token) return 0 + let count = 0 + let index = text.indexOf(token) + while (index !== -1) { + count += 1 + index = text.indexOf(token, index + token.length) + } + return count +} + +/** 从源码里抓出所有 @/ 别名 import 说明符(含 import type)。 */ +export function aliasImports(source: string): string[] { + const specs = new Set() + const re = /(?:import\s+type\s+)?[^'"]*from\s*['"](@\/[^'"]+)['"]/g + let match: RegExpExecArray | null + while ((match = re.exec(source))) specs.add(match[1]) + // 处理动态 import('@/...') 与直接裸 @/ 引用 + const dyn = /(?:import|import\s*\(\s*)['"](@\/[^'"]+)['"]/g + while ((match = dyn.exec(source))) specs.add(match[1]) + return [...specs] +} + +/** 把 import 说明符解析为存在的模块文件;解析不到返回 null(含可操作路径)。 */ +export function resolveModuleSpec(spec: string, fromRel: string): string | null { + let base: string + if (spec.startsWith('@/')) { + base = join(SRC, spec.slice(2)) + } else { + base = resolve(dirname(join(ROOT, fromRel)), spec) + } + for (const ext of EXTENSIONS) { + if (existsSync(base + ext)) return base + ext + } + for (const idx of INDEX_CANDIDATES) { + if (existsSync(base + idx)) return base + idx + } + return null +} + +/** 校验一个源文件的所有 @/ import 都能解析;返回缺失清单(空 = 全部可解析)。 */ +export function missingImports(rel: string): string[] { + const source = readSource(rel) + const missing: string[] = [] + for (const spec of aliasImports(source)) { + if (!resolveModuleSpec(spec, rel)) missing.push(`${rel} 无法解析 import: ${spec}`) + } + return missing +} diff --git a/admin-frontend-vue/tests/task-3.test.ts b/admin-frontend-vue/tests/task-3.test.ts new file mode 100644 index 00000000..ba6f6e51 --- /dev/null +++ b/admin-frontend-vue/tests/task-3.test.ts @@ -0,0 +1,67 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource, occurrences, missingImports } from './helpers.ts' + +const LAYOUT = 'src/layout/AdminLayout.vue' +const ROUTER = 'src/router/index.ts' + +test('test_task_003_layout_contract_normal_primary_path', () => { + // 正常主路径:壳层作为页面容器,唯一地把业务视图挂到 RouterView。 + const layout = readSource(LAYOUT) + assert.equal(occurrences(layout, ' { + // 正常变体:页面接线归路由所有;壳层不得静态 import 业务页面。 + const layout = readSource(LAYOUT) + const router = readSource(ROUTER) + assert.equal(occurrences(layout, '@/pages/'), 0, 'AdminLayout 不得静态 import 业务页面') + assert.ok(router.includes('component: () => import('), '路由使用异步组件加载页面') +}) + +test('test_task_003_layout_contract_normal_repeated_operation_is_idempotent', () => { + // 正常重复:只读复查结果稳定,不依赖可变状态。 + const layoutA = readSource(LAYOUT) + const layoutB = readSource(LAYOUT) + assert.equal(layoutA.length, layoutB.length) + assert.equal(occurrences(layoutA, 'RouterView'), occurrences(layoutB, 'RouterView')) +}) + +test('test_task_003_layout_contract_boundary_empty_input', () => { + // 边界空值:菜单为空时应展示无菜单状态,且页面容器本身仍保留。 + const layout = readSource(LAYOUT) + assert.ok(layout.includes('暂无可用菜单'), '无菜单需有空状态文案') + assert.ok(layout.includes('!groups.length'), '空菜单由 groups 长度驱动') + assert.equal(occurrences(layout, ' { + // 边界单元素:单一内容区与单一页面挂载点。 + const layout = readSource(LAYOUT) + assert.equal(occurrences(layout, 'admin-main'), 1) + assert.equal(occurrences(layout, ' { + // 边界上限:Element Plus 交互能力按需命名导入,不整包拖入壳层。 + const layout = readSource(LAYOUT) + assert.match(layout, /import\s*\{\s*ElMessage\s*\}\s*from\s*['"]element-plus['"]/) + assert.equal(occurrences(layout, 'import ElementPlus'), 0, '壳层不得整包导入 ElementPlus') +}) + +test('test_task_003_layout_contract_invalid_input_rejected', () => { + // 异常输入:壳层不得自行发起后台数据请求,也不得引用 @/api。 + const layout = readSource(LAYOUT) + assert.equal(occurrences(layout, '@/api/'), 0, 'AdminLayout 不得直接调用后台 API') + assert.equal(occurrences(layout, 'http.get'), 0) +}) + +test('test_task_003_layout_contract_dependency_failure_returns_actionable_message', () => { + // 依赖失败:壳层自身依赖必须全部可解析,缺失时返回可操作清单。 + const missing = missingImports(LAYOUT) + assert.deepEqual(missing, [], `存在无法解析的 import: ${missing.join('; ')}`) + const routerMissing = missingImports(ROUTER) + assert.deepEqual(routerMissing, [], `路由存在无法解析的 import: ${routerMissing.join('; ')}`) +})