2026-07-10 16:46:23 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFile } from 'node:fs/promises'
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
import { parse as parseTemplate } from '@vue/compiler-dom'
|
|
|
|
|
import { parse as parseSfc } from '@vue/compiler-sfc'
|
|
|
|
|
|
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
2026-07-13 15:31:01 +08:00
|
|
|
const [globalStyles, routerSource, mainLayoutSource, trainingLogSource, trainingOverviewSource, fineTuneCreateSource] = await Promise.all([
|
2026-07-10 16:46:23 +08:00
|
|
|
readFile(path.resolve(scriptDir, '../src/styles/index.scss'), 'utf8'),
|
|
|
|
|
readFile(path.resolve(scriptDir, '../src/router/index.ts'), 'utf8'),
|
|
|
|
|
readFile(path.resolve(scriptDir, '../src/layouts/MainLayout.vue'), 'utf8'),
|
|
|
|
|
readFile(path.resolve(scriptDir, '../src/views/system/TrainingLogView.vue'), 'utf8'),
|
2026-07-13 15:31:01 +08:00
|
|
|
readFile(path.resolve(scriptDir, '../src/views/system/training-log/TrainingTaskOverview.vue'), 'utf8'),
|
2026-07-10 16:46:23 +08:00
|
|
|
readFile(path.resolve(scriptDir, '../src/views/fine-tune/FineTuneCreateView.vue'), 'utf8'),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
function extractCssBlock(css, marker) {
|
|
|
|
|
const markerIndex = css.indexOf(marker)
|
|
|
|
|
assert.notEqual(markerIndex, -1, `未找到样式规则:${marker}`)
|
|
|
|
|
const openBrace = css.indexOf('{', markerIndex)
|
|
|
|
|
assert.notEqual(openBrace, -1, `样式规则缺少左花括号:${marker}`)
|
|
|
|
|
|
|
|
|
|
let depth = 0
|
|
|
|
|
for (let index = openBrace; index < css.length; index += 1) {
|
|
|
|
|
if (css[index] === '{') depth += 1
|
|
|
|
|
if (css[index] === '}') depth -= 1
|
|
|
|
|
if (depth === 0) return css.slice(openBrace + 1, index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.fail(`样式规则缺少右花括号:${marker}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findElements(node, predicate, result = []) {
|
|
|
|
|
if (node?.type === 1 && predicate(node)) result.push(node)
|
|
|
|
|
for (const child of node?.children || []) findElements(child, predicate, result)
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function staticAttribute(node, name) {
|
|
|
|
|
const prop = node.props.find((item) => item.type === 6 && item.name === name)
|
|
|
|
|
return prop?.value?.content
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function boundAttribute(node, name) {
|
|
|
|
|
const prop = node.props.find(
|
|
|
|
|
(item) => item.type === 7 && item.name === 'bind' && item.arg?.content === name,
|
|
|
|
|
)
|
|
|
|
|
return prop?.exp?.content
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractRouteBlock(source, routePath) {
|
|
|
|
|
const pathPattern = new RegExp(`path:\\s*['"]${routePath.replaceAll('/', '\\/')}['"]`)
|
|
|
|
|
const pathIndex = source.search(pathPattern)
|
|
|
|
|
assert.notEqual(pathIndex, -1, `未找到路由 /${routePath}`)
|
|
|
|
|
|
|
|
|
|
const openBrace = source.lastIndexOf('{', pathIndex)
|
|
|
|
|
assert.notEqual(openBrace, -1, `路由 /${routePath} 缺少左花括号`)
|
|
|
|
|
|
|
|
|
|
let depth = 0
|
|
|
|
|
for (let index = openBrace; index < source.length; index += 1) {
|
|
|
|
|
if (source[index] === '{') depth += 1
|
|
|
|
|
if (source[index] === '}') depth -= 1
|
|
|
|
|
if (depth === 0) return source.slice(openBrace + 1, index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.fail(`路由 /${routePath} 缺少右花括号`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selfSurfaceRoutes = [
|
|
|
|
|
'fine-tune',
|
|
|
|
|
'model-eval',
|
|
|
|
|
'model-inference',
|
2026-07-13 15:31:01 +08:00
|
|
|
'model-inference/chat/:id',
|
2026-07-10 16:46:23 +08:00
|
|
|
'model-manage',
|
|
|
|
|
'data-process',
|
2026-07-13 15:31:01 +08:00
|
|
|
'data-process/create',
|
2026-07-10 16:46:23 +08:00
|
|
|
'dataset',
|
|
|
|
|
]
|
|
|
|
|
for (const routePath of selfSurfaceRoutes) {
|
|
|
|
|
const routeBlock = extractRouteBlock(routerSource, routePath)
|
|
|
|
|
assert.match(
|
|
|
|
|
routeBlock,
|
|
|
|
|
/meta:\s*\{[^}]*pageSurface:\s*['"]self['"][^}]*\}/,
|
|
|
|
|
`列表路由 /${routePath} 未声明 pageSurface: 'self'`,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
routerSource.match(/pageSurface:\s*['"]self['"]/g)?.length,
|
|
|
|
|
selfSurfaceRoutes.length,
|
|
|
|
|
"只能给指定的自带白色表面的列表路由声明 pageSurface: 'self'",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const rootBlock = extractCssBlock(globalStyles, ':root')
|
|
|
|
|
assert.match(rootBlock, /--app-shell-bg:\s*#f3f5f8;/, '全局样式缺少灰色外层背景 token')
|
|
|
|
|
assert.match(rootBlock, /--app-page-bg:\s*#ffffff;/, '全局样式缺少白色页面画布 token')
|
|
|
|
|
assert.match(rootBlock, /--app-surface-bg:\s*#ffffff;/, '全局样式缺少统一内容表面 token')
|
|
|
|
|
|
|
|
|
|
const bodyBlock = extractCssBlock(globalStyles, '\nbody {')
|
|
|
|
|
assert.match(bodyBlock, /background-color:\s*var\(--app-shell-bg\);/, 'body 未使用灰色外层背景')
|
|
|
|
|
|
|
|
|
|
const cardBlock = extractCssBlock(globalStyles, '.el-card {')
|
|
|
|
|
assert.match(cardBlock, /background-color:\s*var\(--app-surface-bg\);/, '全局卡片未使用统一内容表面')
|
|
|
|
|
|
|
|
|
|
const mainLayoutDescriptor = parseSfc(mainLayoutSource).descriptor
|
|
|
|
|
const mainLayoutScript = mainLayoutDescriptor.scriptSetup?.content || ''
|
|
|
|
|
const mainLayoutStyle = mainLayoutDescriptor.styles.map((item) => item.content).join('\n')
|
|
|
|
|
const mainLayoutTemplate = mainLayoutDescriptor.template?.content || ''
|
|
|
|
|
const mainLayoutAst = parseTemplate(mainLayoutTemplate)
|
|
|
|
|
|
|
|
|
|
assert.match(mainLayoutScript, /import\s*\{[^}]*\buseRoute\b[^}]*\}\s*from\s*['"]vue-router['"]/, '主布局未引入 useRoute')
|
|
|
|
|
assert.match(mainLayoutScript, /const\s+route\s*=\s*useRoute\(\)/, '主布局未获取当前路由')
|
|
|
|
|
|
|
|
|
|
const pageCanvases = findElements(
|
|
|
|
|
mainLayoutAst,
|
|
|
|
|
(node) => staticAttribute(node, 'class')?.split(/\s+/).includes('page-canvas'),
|
|
|
|
|
)
|
|
|
|
|
assert.equal(pageCanvases.length, 1, '主布局必须且只能提供一个全局白色页面画布')
|
|
|
|
|
assert.match(
|
|
|
|
|
boundAttribute(pageCanvases[0], 'class') || '',
|
|
|
|
|
/['"]is-self-surface['"]\s*:\s*route\.meta\.pageSurface\s*===\s*['"]self['"]/,
|
|
|
|
|
'主布局未根据 route.meta.pageSurface 为页面画布添加 is-self-surface 类',
|
|
|
|
|
)
|
|
|
|
|
assert.equal(
|
|
|
|
|
findElements(pageCanvases[0], (node) => node.tag === 'router-view').length,
|
|
|
|
|
1,
|
|
|
|
|
'所有业务路由必须渲染在全局页面画布内部',
|
|
|
|
|
)
|
|
|
|
|
assert.equal(
|
|
|
|
|
findElements(mainLayoutAst, (node) => node.tag.toLowerCase() === 'transition').length,
|
|
|
|
|
0,
|
|
|
|
|
'主布局仍包含会造成列表与二级页面表面错位的页面级 Transition',
|
|
|
|
|
)
|
|
|
|
|
assert.doesNotMatch(
|
|
|
|
|
mainLayoutStyle,
|
|
|
|
|
/\.fade-(?:enter|leave)-(?:active|from|to)/,
|
|
|
|
|
'主布局仍包含页面级透明度转场样式',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const layoutContentBlock = extractCssBlock(mainLayoutStyle, '.layout-content')
|
|
|
|
|
assert.match(layoutContentBlock, /background-color:\s*var\(--app-shell-bg\);/, '主内容区未使用灰色外层背景')
|
|
|
|
|
|
2026-07-13 15:31:01 +08:00
|
|
|
const pageCanvasBlock = extractCssBlock(mainLayoutStyle, '\n.page-canvas {')
|
|
|
|
|
assert.match(pageCanvasBlock, /flex:\s*1 0 auto;/, '白色页面画布没有铺满可用高度')
|
2026-07-10 16:46:23 +08:00
|
|
|
assert.match(pageCanvasBlock, /padding:\s*24px;/, '白色页面画布缺少统一内容内边距')
|
|
|
|
|
assert.match(pageCanvasBlock, /border-radius:\s*16px;/, '白色页面画布圆角与参考不一致')
|
|
|
|
|
assert.match(pageCanvasBlock, /background-color:\s*var\(--app-page-bg\);/, '全局页面画布未使用白色背景')
|
|
|
|
|
|
|
|
|
|
const selfSurfaceBlock = extractCssBlock(mainLayoutStyle, '.page-canvas.is-self-surface')
|
|
|
|
|
assert.match(selfSurfaceBlock, /padding:\s*0;/, '自带表面的页面仍保留全局画布内边距')
|
|
|
|
|
assert.match(selfSurfaceBlock, /border-radius:\s*0;/, '自带表面的页面仍保留全局画布圆角')
|
|
|
|
|
assert.match(selfSurfaceBlock, /background-color:\s*transparent;/, '自带表面的页面未透出灰色应用背景')
|
|
|
|
|
assert.match(selfSurfaceBlock, /box-shadow:\s*none;/, '自带表面的页面仍保留全局画布阴影')
|
|
|
|
|
|
|
|
|
|
assert.match(
|
|
|
|
|
mainLayoutStyle,
|
|
|
|
|
/\.page-canvas:not\(\.is-self-surface\)\s*>\s*:deep\(\.page-card-host\s*>\s*\.page-card\)/,
|
|
|
|
|
'显式根卡片宿主内的 PageCard 未被识别为页面根卡片',
|
|
|
|
|
)
|
|
|
|
|
assert.doesNotMatch(
|
|
|
|
|
mainLayoutStyle,
|
|
|
|
|
/:deep\(\*\s*>\s*\.page-card\)/,
|
|
|
|
|
'通用层级选择器会误伤训练日志等页面的内部业务卡片',
|
|
|
|
|
)
|
|
|
|
|
assert.match(
|
|
|
|
|
fineTuneCreateSource,
|
|
|
|
|
/class=["'][^"']*\bfine-tune-create\b[^"']*\bpage-card-host\b[^"']*["']/,
|
|
|
|
|
'创建训练任务页未显式标记根 PageCard 宿主',
|
|
|
|
|
)
|
|
|
|
|
const rootPageCardBlock = extractCssBlock(
|
|
|
|
|
mainLayoutStyle,
|
|
|
|
|
'.page-canvas:not(.is-self-surface) > :deep(.page-card)',
|
|
|
|
|
)
|
|
|
|
|
assert.match(rootPageCardBlock, /margin-bottom:\s*0;/, '页面根卡片仍在白色画布内保留额外外边距')
|
|
|
|
|
assert.match(rootPageCardBlock, /background-color:\s*transparent;/, '页面根卡片仍形成第二层白色背景')
|
|
|
|
|
assert.match(rootPageCardBlock, /border-radius:\s*0\s*!important;/, '页面根卡片仍形成第二层圆角边界')
|
|
|
|
|
assert.match(rootPageCardBlock, /box-shadow:\s*none\s*!important;/, '页面根卡片仍形成重复卡片层级')
|
|
|
|
|
|
2026-07-13 15:31:01 +08:00
|
|
|
const trainingLogStyle = [trainingLogSource, trainingOverviewSource]
|
|
|
|
|
.flatMap((source) => parseSfc(source).descriptor.styles.map((item) => item.content))
|
|
|
|
|
.join('\n')
|
2026-07-10 16:46:23 +08:00
|
|
|
const businessSurfaceBlock = extractCssBlock(trainingLogStyle, '.profile-section,\n.runtime-panel')
|
|
|
|
|
assert.match(
|
|
|
|
|
businessSurfaceBlock,
|
|
|
|
|
/background:\s*var\(--app-surface-bg\);/,
|
|
|
|
|
'任务、数据集和运行概况未使用统一白色内容表面',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
console.log('全局页面背景与内容表面回归检查通过')
|