import assert from 'node:assert/strict' import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import path from 'node:path' import { parse as parseSfc } from '@vue/compiler-sfc' const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const viewPath = path.resolve(scriptDir, '../src/views/data-process/DataProcessCreateView.vue') const createDir = path.resolve(scriptDir, '../src/views/data-process/create') const confirmDialogPath = path.resolve(scriptDir, '../src/components/AppConfirmDialog.vue') const layoutPath = path.resolve(scriptDir, '../src/layouts/MainLayout.vue') const apiPath = path.resolve(scriptDir, '../src/api/modules/dataProcess.ts') const contractTypesPath = path.resolve(scriptDir, '../src/types/dataProcess.ts') const viewSource = await readFile(viewPath, 'utf8') const layoutSource = await readFile(layoutPath, 'utf8') const [draftSource, stateSource, generationSource, viewStyleSource, apiSource, contractTypesSource] = await Promise.all([ readFile(path.join(createDir, 'useDataProcessDraft.ts'), 'utf8'), readFile(path.join(createDir, 'dataProcessCreateState.ts'), 'utf8'), readFile(path.join(createDir, 'useDataProcessGeneration.ts'), 'utf8'), readFile(path.join(createDir, 'data-process-create.scss'), 'utf8'), readFile(apiPath, 'utf8'), readFile(contractTypesPath, 'utf8'), ]) const implementationSource = [viewSource, draftSource, stateSource, generationSource].join('\n') assert.ok(existsSync(confirmDialogPath), '缺少公共确认弹窗组件 AppConfirmDialog') const confirmDialogSource = await readFile(confirmDialogPath, 'utf8') for (const marker of ['', 'role="alertdialog"', ':aria-modal="true"', 'handleKeydown', 'Escape']) { assert.ok(confirmDialogSource.includes(marker), `公共确认弹窗缺少可访问性能力:${marker}`) } assert.match(confirmDialogSource, /min-height:\s*44px/, '公共确认弹窗按钮触控区域不足 44px') assert.match(confirmDialogSource, /focus\(\)/, '公共确认弹窗打开后没有管理键盘焦点') assert.match(confirmDialogSource, /defineExpose\(\{ open \}\)/, '公共确认弹窗没有暴露 Promise 式 open API') assert.match(confirmDialogSource, /width:\s*min\(480px,\s*100%\)/, '企业级确认弹窗宽度应保持紧凑的 480px') assert.match(confirmDialogSource, /border-radius:\s*8px/, '企业级确认弹窗应使用克制的 8px 圆角') assert.doesNotMatch(confirmDialogSource, /backdrop-filter/, '企业级确认弹窗不应使用装饰性背景模糊') assert.ok(confirmDialogSource.includes('app-confirm-header'), '企业级确认弹窗缺少独立标题栏') assert.match(confirmDialogSource, /\.app-confirm-button\s*\{[\s\S]*?height:\s*34px/, '桌面端操作按钮应使用紧凑的 34px 高度') assert.match(confirmDialogSource, /@media \(max-width: 520px\)[\s\S]*?\.app-confirm-button\s*\{[\s\S]*?min-height:\s*44px/, '移动端操作按钮仍需保留 44px 触控高度') assert.match(viewSource, /import AppConfirmDialog from '@\/components\/AppConfirmDialog\.vue'/, '创建页没有接入公共确认弹窗') assert.match(viewSource, //, '路由离开确认没有改为异步公共弹窗流程') assert.doesNotMatch(viewSource, /window\.confirm|ElMessageBox/, '创建页仍在使用系统或 Element Plus 确认框') assert.match(viewSource, /const WIZARD_STEPS = \[/, '向导步骤尚未改为固定常量') for (const title of ['创建任务', '大模型选择', '上传文件', '数据预览', '开始生成', '结果编辑与保存']) { assert.ok(viewSource.includes(`title: '${title}'`), `缺少固定步骤:${title}`) } assert.match( viewSource, /\{ id: 'create',[\s\S]*?\{ id: 'model',[\s\S]*?\{ id: 'upload',[\s\S]*?\{ id: 'preview',[\s\S]*?\{ id: 'generate',[\s\S]*?\{ id: 'results'/, '六步向导顺序必须为创建任务、大模型选择、上传文件、数据预览、开始生成、结果编辑与保存', ) assert.doesNotMatch(viewSource, /steps\s*=\s*computed|all\.filter/, '步骤仍根据处理类型动态增减') assert.match( viewStyleSource, /@media \(max-width: 1100px\)[\s\S]*?\.step-title\s*\{[\s\S]*?display:\s*none[\s\S]*?\.step-item\.is-active \.step-title\s*\{[\s\S]*?display:\s*block/, '六步向导在中等宽度下没有收起非当前步骤标题', ) assert.match(draftSource, /localStorage\.setItem\(DATA_PROCESS_DRAFT_STORAGE_KEY/, '草稿没有持久化') assert.match(draftSource, /localStorage\.getItem\(DATA_PROCESS_DRAFT_STORAGE_KEY\)/, '草稿没有恢复读取') assert.match(viewSource, /restoreDraft\(\)/, '页面没有恢复草稿') assert.ok(viewSource.split('\n').length < 1000, 'DataProcessCreateView 拆分后仍超过 1000 行') assert.match(viewSource, /useDataProcessGeneration\(\{/, '生成流程没有拆分到独立 composable') const expectedComponents = [ 'TaskSetupStep.vue', 'ModelSelectionStep.vue', 'SourceUploadStep.vue', 'PreviewCompareStep.vue', 'GenerationStep.vue', 'ResultEditorStep.vue', ] for (const component of expectedComponents) { assert.ok(existsSync(path.join(createDir, component)), `缺少步骤组件:${component}`) assert.ok(viewSource.includes(component.replace('.vue', '')), `父页面未使用:${component}`) } const typesPath = path.join(createDir, 'types.ts') const modelPath = path.join(createDir, 'previewModel.ts') assert.ok(existsSync(typesPath), '缺少向导类型定义') assert.ok(existsSync(modelPath), '缺少来源映射模型') const [typesSource, modelSource, previewSource] = await Promise.all([ readFile(typesPath, 'utf8'), readFile(modelPath, 'utf8'), readFile(path.join(createDir, 'PreviewCompareStep.vue'), 'utf8'), ]) for (const field of ['sourceStart', 'sourceEnd', 'originalContent', 'editedContent']) { assert.ok(typesSource.includes(field), `PreviewItem 缺少字段:${field}`) } assert.match(typesSource, /sourceFileId/, 'PreviewItem 缺少来源文件标识') assert.match(typesSource, /export type StepId = 'create' \| 'model' \| 'upload' \| 'preview' \| 'generate' \| 'results'/, '步骤类型缺少独立大模型选择步骤') assert.match(modelSource, /export function sourceLines/, '缺少源文件行偏移生成函数') assert.doesNotMatch(modelSource, /buildPreviewItems/, '前端不应保留与后端重复的本地切片算法') assert.match(viewSource, /selectedPreviewFileId/, '父页面缺少当前预览文件状态') assert.match( viewSource, /buildDataProcessPreview\(taskId\.value,\s*\{[\s\S]*?source_file_ids:\s*uploadedFiles\.value\.map/, '预览没有通过后端按已上传源文件构建', ) assert.match(viewSource, /getDataProcessPreview\(taskId\.value,\s*\{ page:\s*1, page_size:\s*500 \}\)/, '预览构建后没有分页读取后端数据') assert.doesNotMatch(viewSource, /buildPreviewItems\(/, '创建向导仍在本地构建集成预览数据') for (const marker of [ 'preview-workspace', 'source-viewer', 'source-line', 'is-highlighted', 'preview-item', 'preview-editor', 'scrollIntoView', ]) { assert.ok(previewSource.includes(marker), `第四步缺少结构或行为:${marker}`) } assert.match(previewSource, /sourceStart/, '第四步未使用来源起始偏移') assert.match(previewSource, /sourceEnd/, '第四步未使用来源结束偏移') assert.match(previewSource, /filterable/, '文件选择器必须可搜索') assert.match(previewSource, /当前文件/, '预览缺少当前文件切换器') assert.doesNotMatch(previewSource, /located-badge|sync-label|已定位到/, '源文件栏不应显示冗余定位提示') assert.match(previewSource, /const PREVIEW_PAGE_SIZE = 6/, '切片列表必须限制每页展示数量') assert.match(previewSource, /const pagedItems = computed/, '切片列表缺少分页数据') assert.match(previewSource, /v-for="item in pagedItems"/, '切片列表没有使用分页数据') assert.match(previewSource, /\(null\)/, '缺少切片编辑模式状态') assert.match(previewSource, /const editorDraft = ref\(''\)/, '缺少编辑临时草稿') assert.match(previewSource, /function openEditor\(item: PreviewItem\)/, '列表缺少打开切片编辑器的动作') assert.match(previewSource, /function closeEditor\(\)/, '编辑器缺少返回列表的动作') assert.match(previewSource, /function saveEditor\(\)/, '编辑器缺少保存动作') assert.match(previewSource, /