Files
YG_FT/frontend/scripts/regression-page-surface.mjs
caoxiaozhu e09c6e81df feat: 用户与权限管理
新增 PermissionCode 权限类型与用户/权限 API,路由注册用户设置/创建/权限页与无权访问页并接入权限守卫,AppSidebar 按权限过滤菜单并新增用户设置入口,auth store 与 mock 适配器同步支持用户管理与新登录认证,回归脚本与 npm 脚本注册。
2026-07-14 16:10:50 +08:00

192 lines
8.4 KiB
JavaScript

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))
const [globalStyles, routerSource, mainLayoutSource, trainingLogSource, trainingOverviewSource, fineTuneCreateSource] = await Promise.all([
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'),
readFile(path.resolve(scriptDir, '../src/views/system/training-log/TrainingTaskOverview.vue'), 'utf8'),
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',
'model-inference/chat/:id',
'model-manage',
'data-process',
'data-process/create',
'dataset',
'user-settings',
]
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\);/, '主内容区未使用灰色外层背景')
const pageCanvasBlock = extractCssBlock(mainLayoutStyle, '\n.page-canvas {')
assert.match(pageCanvasBlock, /flex:\s*1 0 auto;/, '白色页面画布没有铺满可用高度')
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;/, '页面根卡片仍形成重复卡片层级')
const trainingLogStyle = [trainingLogSource, trainingOverviewSource]
.flatMap((source) => parseSfc(source).descriptor.styles.map((item) => item.content))
.join('\n')
const businessSurfaceBlock = extractCssBlock(trainingLogStyle, '.profile-section,\n.runtime-panel')
assert.match(
businessSurfaceBlock,
/background:\s*var\(--app-surface-bg\);/,
'任务、数据集和运行概况未使用统一白色内容表面',
)
console.log('全局页面背景与内容表面回归检查通过')