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 viewPath = path.resolve(scriptDir, '../src/views/system/TrainingLogView.vue') const source = await readFile(viewPath, 'utf8') const { descriptor } = parseSfc(source, { filename: viewPath }) const template = descriptor.template?.content || '' const style = descriptor.styles.map((item) => item.content).join('\n') const templateAst = parseTemplate(template) 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 boundExpression(node, name) { const prop = node.props.find( (item) => item.type === 7 && item.name === 'bind' && item.arg?.type === 4 && item.arg.content === name, ) return prop?.exp?.type === 4 ? prop.exp.content : undefined } 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 relativeLuminance(hex) { const channels = hex .replace('#', '') .match(/.{2}/g) .map((channel) => Number.parseInt(channel, 16) / 255) .map((value) => (value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4)) return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2] } function contrastRatio(foreground, background) { const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background)) const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background)) return (lighter + 0.05) / (darker + 0.05) } const overview = findElements( templateAst, (node) => staticAttribute(node, 'class')?.split(/\s+/).includes('overview-layout'), ) assert.equal(overview.length, 1, '双栏任务档案容器必须且只能存在一个') for (const expectedClass of ['task-profile', 'dataset-profile', 'runtime-panel', 'parameter-groups']) { const matched = findElements( templateAst, (node) => staticAttribute(node, 'class')?.split(/\s+/).includes(expectedClass), ) assert.equal(matched.length, 1, `缺少或重复布局结构:${expectedClass}`) } const toggleButtons = findElements( templateAst, (node) => node.tag === 'button' && staticAttribute(node, 'class')?.split(/\s+/).includes('params-toggle-button'), ) assert.equal(toggleButtons.length, 1, '参数折叠必须使用唯一的原生 button') const toggleButton = toggleButtons[0] assert.equal(boundExpression(toggleButton, 'aria-expanded'), 'paramsExpanded', '折叠按钮未绑定 aria-expanded') assert.equal(staticAttribute(toggleButton, 'aria-controls'), 'training-parameter-content', '折叠按钮缺少正确的 aria-controls') const controlledRegions = findElements( templateAst, (node) => staticAttribute(node, 'id') === 'training-parameter-content', ) assert.equal(controlledRegions.length, 1, 'aria-controls 指向的参数内容区域不存在或重复') const firstChartIndex = template.indexOf('') assert.notEqual(firstChartIndex, -1, '未找到训练曲线边界,无法限定首屏检查范围') assert.equal( template.slice(0, firstChartIndex).includes('= 4.5, `未配置文本颜色 ${mutedColor} 与白色背景对比度不足 4.5:1`, ) console.log('训练日志详情布局回归检查通过')