694 lines
39 KiB
JavaScript
694 lines
39 KiB
JavaScript
import test from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { existsSync, readFileSync } from 'node:fs'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { dirname, resolve } from 'node:path'
|
||
|
||
const currentDir = dirname(fileURLToPath(import.meta.url))
|
||
const srcDir = resolve(currentDir, '..')
|
||
|
||
function readSource(relativePath) {
|
||
return readFileSync(resolve(srcDir, relativePath), 'utf8')
|
||
}
|
||
|
||
test('generate page hides the expert mode entry from one-click recovery actions', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const deprecatedExpertModeEntry = ['进入', '专家', '模式'].join('')
|
||
|
||
assert.equal(source.includes(deprecatedExpertModeEntry), false)
|
||
})
|
||
|
||
test('generate page restores blueprint and flow_config panels in expert mode', () => {
|
||
assert.equal(existsSync(resolve(srcDir, 'components/AiTaskStatusPanel.vue')), true)
|
||
assert.equal(existsSync(resolve(srcDir, 'components/AppBlueprintPanel.vue')), true)
|
||
assert.equal(existsSync(resolve(srcDir, 'components/BusinessBlueprintPanel.vue')), false)
|
||
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes("import AiTaskStatusPanel from '@/components/AiTaskStatusPanel.vue'"), false)
|
||
assert.equal(source.includes("import AppBlueprintPanel from '@/components/AppBlueprintPanel.vue'"), true)
|
||
assert.equal(source.includes("import BusinessBlueprintPanel from '@/components/BusinessBlueprintPanel.vue'"), false)
|
||
assert.equal(source.includes('<AiTaskStatusPanel'), false)
|
||
assert.equal(source.includes('<AppBlueprintPanel'), true)
|
||
assert.equal(source.includes('<BusinessBlueprintPanel'), false)
|
||
assert.equal(source.includes('expert-adjustment-actions'), true)
|
||
assert.equal(source.includes('module-adjustment-anchor'), true)
|
||
assert.equal(source.includes('business-loop-plan-panel'), true)
|
||
assert.equal(source.includes('flow-config-adjustment-anchor'), true)
|
||
assert.equal(source.includes('database-adjustment-anchor'), true)
|
||
assert.equal(source.includes('page-adjustment-anchor'), true)
|
||
assert.equal(source.includes('业务闭环'), true)
|
||
assert.equal(source.includes('flow_config'), true)
|
||
assert.equal(source.includes('handleAuditBusinessLoop'), true)
|
||
assert.equal(source.includes('handleApplyBusinessLoopBindings'), true)
|
||
})
|
||
|
||
test('generate page removes reviewed expert summary and section chrome', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const blueprintPanelSource = readSource('components/AppBlueprintPanel.vue')
|
||
const summary = source.match(/<div class="expert-adjustment-summary">[\s\S]*?<\/div>/)
|
||
|
||
assert.notEqual(summary, null)
|
||
assert.doesNotMatch(summary[0], /业务闭环/)
|
||
assert.doesNotMatch(summary[0], /businessLoopSummary\.requiredActions/)
|
||
assert.equal(source.includes('class="expert-pipeline-strip"'), false)
|
||
assert.equal(source.includes('class="expert-adjustment-note"'), false)
|
||
assert.equal(source.includes('恢复展示系统角色、前台菜单、后台菜单和模块图。'), false)
|
||
assert.doesNotMatch(source, /<el-button(?:(?!<\/el-button>)[\s\S])*生成系统模块设计(?:(?!<\/el-button>)[\s\S])*<\/el-button>/)
|
||
assert.equal(source.includes('恢复展示非 CRUD 业务动作、规则校验和数据影响。'), false)
|
||
assert.doesNotMatch(source, /<el-button(?:(?!<\/el-button>)[\s\S])*生成业务流程设计(?:(?!<\/el-button>)[\s\S])*<\/el-button>/)
|
||
assert.equal(blueprintPanelSource.includes('class="database-header-actions"'), false)
|
||
assert.equal(blueprintPanelSource.includes('<el-radio-group'), false)
|
||
assert.equal(blueprintPanelSource.includes('角色:{{ roles.length }}'), false)
|
||
})
|
||
|
||
test('generate page keeps AI task history out of the simplified expert surface', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const panelSource = readSource('components/AiTaskStatusPanel.vue')
|
||
|
||
assert.equal(source.includes('listGenerations'), true)
|
||
assert.match(source, /const generationHistory = ref\(\[\]\)/)
|
||
assert.match(source, /async function refreshGenerationHistory/)
|
||
assert.equal(source.includes(':generation-history="generationHistory"'), false)
|
||
assert.equal(source.includes('<AiTaskStatusPanel'), false)
|
||
assert.equal(panelSource.includes('generation-history'), true)
|
||
assert.equal(panelSource.includes('generationHistory'), true)
|
||
})
|
||
|
||
test('generate page keeps database designer mounted before AI returns tables', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('<DatabaseDesigner'), true)
|
||
assert.equal(source.includes('v-model="database"'), true)
|
||
assert.equal(source.includes('<template v-if="database.tables.length">'), false)
|
||
assert.equal(source.includes('点击生成数据库后,这里会展示可编辑表结构'), false)
|
||
})
|
||
|
||
test('generate page hides the database ER diagram center entry', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes("import ErDiagramView from '@/components/ErDiagramView.vue'"), false)
|
||
assert.equal(source.includes('<ErDiagramView'), false)
|
||
assert.equal(source.includes('label="er"'), false)
|
||
assert.equal(source.includes('去图表中心编辑 ER 图'), false)
|
||
assert.equal(source.includes('openDiagramCenter(\'er\')'), false)
|
||
assert.equal(source.includes('function openDiagramCenter'), false)
|
||
})
|
||
|
||
test('generate page does not clip database designer field editor', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.doesNotMatch(source, /\.designer-panel\s*\{[\s\S]*?overflow:\s*hidden;[\s\S]*?\}/)
|
||
})
|
||
|
||
test('generate page persists flow_config as the only editable business-flow json', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('const flowConfigJson = ref(formatJson(emptyFlowConfig()))'), true)
|
||
assert.equal(source.includes('async function applyFlowConfigJson()'), true)
|
||
assert.equal(source.includes('const businessBlueprintJson'), false)
|
||
assert.equal(source.includes('function applyBusinessBlueprintJson'), false)
|
||
})
|
||
|
||
test('generate page links to standalone page designer before preview', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes("import FrontendPageDesigner from '@/components/FrontendPageDesigner.vue'"), false)
|
||
assert.equal(source.includes('<FrontendPageDesigner'), false)
|
||
assert.equal(source.includes('class="page-design-summary-grid"'), true)
|
||
assert.equal(source.includes('class="page-design-summary-card"'), true)
|
||
assert.equal(source.includes('openPageDesigner(\'frontend\')'), true)
|
||
assert.equal(source.includes('listPageDesigns'), true)
|
||
assert.equal(source.includes('initPageDesigns'), true)
|
||
assert.equal(source.includes('savePageDesign'), true)
|
||
assert.equal(source.includes('listBusinessBlocks'), true)
|
||
assert.equal(source.includes('const pageDesigns = ref([])'), true)
|
||
assert.equal(source.includes('const businessBlockDefinitions = ref([])'), true)
|
||
assert.equal(source.includes('@create-design="handleCreatePageDesign"'), false)
|
||
assert.equal(source.includes('@delete-design="handleDeletePageDesign"'), false)
|
||
assert.equal(source.includes('@save-design="handleSavePageDesign"'), false)
|
||
assert.equal(source.includes(':block-definitions="businessBlockDefinitions"'), false)
|
||
assert.equal(source.includes('async function savePendingPageDesigns'), true)
|
||
assert.equal(source.includes('listBusinessBlocks(id)'), true)
|
||
assert.equal(source.includes("await savePendingPageDesigns(pageDesignerRef, pageDesigns, 'frontend')"), true)
|
||
assert.equal(source.includes("await savePendingPageDesigns(adminPageDesignerRef, adminPageDesigns, 'admin')"), true)
|
||
assert.match(source, /await savePendingPageDesigns\(pageDesignerRef, pageDesigns, 'frontend'\)[\s\S]*await savePendingPageDesigns\(adminPageDesignerRef, adminPageDesigns, 'admin'\)[\s\S]*await createAiTask\(projectId\.value, aiTaskPayload\(DATABASE_CHANGE_SYNC_TYPE\)\)/)
|
||
})
|
||
|
||
test('generate page removes stale deleted page when backend already deleted it', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('function shouldRemoveMissingPage(error)'), true)
|
||
assert.equal(source.match(/if \(shouldRemoveMissingPage\(error\)\)/g)?.length, 2)
|
||
assert.equal(source.includes('页面已不存在,已从列表移除'), true)
|
||
})
|
||
|
||
test('generate page renders frontend and admin designer entry cards', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('前台页面设计'), true)
|
||
assert.equal(source.includes('后台页面设计'), true)
|
||
assert.equal(source.includes('openPageDesigner(\'frontend\')'), true)
|
||
assert.equal(source.includes('openPageDesigner(\'admin\')'), true)
|
||
assert.equal(source.includes('const adminPageDesigns = ref([])'), true)
|
||
assert.equal(source.includes('const adminPageDesignerRef = ref(null)'), true)
|
||
assert.equal(source.includes('listPageDesigns(id, \'admin\')'), true)
|
||
assert.equal(source.includes('initPageDesigns(id, \'admin\')'), true)
|
||
assert.equal(source.includes("savePendingPageDesigns(adminPageDesignerRef, adminPageDesigns, 'admin')"), true)
|
||
})
|
||
|
||
test('generate page keeps page design entry cards styled if page scoped css lags', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const globalStyles = readSource('styles/global.scss')
|
||
|
||
assert.equal(source.includes('<el-row :gutter="16" class="page-design-summary-grid">'), true)
|
||
assert.equal(source.includes('<el-card class="page-design-summary-card" shadow="never">'), true)
|
||
assert.match(globalStyles, /\.page-design-summary-grid\s*\{[\s\S]*padding:\s*20px;[\s\S]*\}/)
|
||
assert.match(globalStyles, /\.page-design-summary-card\s*\{[\s\S]*border-radius:\s*8px;[\s\S]*\}/)
|
||
assert.match(globalStyles, /\.summary-kicker\s*\{[\s\S]*font-size:\s*12px;[\s\S]*\}/)
|
||
})
|
||
|
||
test('generate page keeps expert generation controls out of the hero card', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const heroStart = source.indexOf('<div v-if="expertMode" class="panel expert-adjustment-panel"')
|
||
const modulePanelStart = source.indexOf('module-adjustment-anchor', heroStart)
|
||
const expertSection = heroStart >= 0 && modulePanelStart >= 0 ? source.slice(heroStart, modulePanelStart) : null
|
||
|
||
assert.notEqual(expertSection, null)
|
||
assert.doesNotMatch(expertSection, /生成系统模块设计/)
|
||
assert.doesNotMatch(expertSection, /生成 flow_config/)
|
||
assert.doesNotMatch(expertSection, /生成数据库/)
|
||
assert.doesNotMatch(expertSection, /生成业务流程设计/)
|
||
assert.doesNotMatch(expertSection, /<el-button[\s\S]*闭环审计/)
|
||
assert.doesNotMatch(expertSection, /生成源码预览/)
|
||
assert.doesNotMatch(expertSection, /启动运行预览/)
|
||
assert.equal(source.includes('class="expert-pipeline-strip"'), false)
|
||
assert.match(source, /class="[^"]*business-loop-plan-panel/)
|
||
assert.equal(source.includes('生成 flow_config'), true)
|
||
assert.equal(source.includes('闭环审计'), true)
|
||
})
|
||
|
||
test('generate page replaces expert code analysis and back buttons with source preview', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const actions = source.match(/<div class="expert-adjustment-actions toolbar">[\s\S]*?<\/div>/)
|
||
|
||
assert.notEqual(actions, null)
|
||
assert.match(actions[0], /@click="handleOpenSourcePreview"/)
|
||
assert.match(actions[0], /查看源码预览/)
|
||
assert.doesNotMatch(actions[0], /openCodeAnalysis\(projectId\)/)
|
||
assert.doesNotMatch(actions[0], /setWorkspaceMode\('one-click'\)/)
|
||
assert.doesNotMatch(actions[0], /代码解读/)
|
||
assert.doesNotMatch(actions[0], new RegExp(['返回一键', '模式'].join('')))
|
||
})
|
||
|
||
test('generate page exposes one-click generation as the default flow', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /one_click_project/)
|
||
assert.match(source, /handleGenerateCompleteProject/)
|
||
assert.match(source, /validateOneClickInput/)
|
||
assert.match(source, /oneClickResult/)
|
||
assert.match(source, /expertMode/)
|
||
assert.equal(source.includes('const workspaceMode = ref(normalizeWorkspaceMode(route.query.mode))'), true)
|
||
assert.equal(source.includes('v-if="!expertMode" class="panel one-click-card"'), true)
|
||
assert.equal(source.includes('v-if="expertMode" class="panel expert-adjustment-panel"'), true)
|
||
assert.equal(source.includes("mode: normalizedMode"), true)
|
||
})
|
||
|
||
test('generate page clears stale one-click task before creating a regenerated task', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const handlerIndex = source.indexOf('async function handleGenerateCompleteProject()')
|
||
const createTaskIndex = source.indexOf('createOneClickProjectTask(id, oneClickTaskPayload())', handlerIndex)
|
||
const clearStoredTaskIndex = source.indexOf('forgetActiveTask(id, ONE_CLICK_GENERATE_TYPE)', handlerIndex)
|
||
const clearVisibleTaskIndex = source.indexOf('oneClickAiTask.value = null', handlerIndex)
|
||
|
||
assert.notEqual(handlerIndex, -1)
|
||
assert.notEqual(createTaskIndex, -1)
|
||
assert.notEqual(clearStoredTaskIndex, -1)
|
||
assert.notEqual(clearVisibleTaskIndex, -1)
|
||
assert.equal(clearStoredTaskIndex < createTaskIndex, true)
|
||
assert.equal(clearVisibleTaskIndex < createTaskIndex, true)
|
||
})
|
||
|
||
test('generate page restores the requested one-click task from the route', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const restoreFunction = source.match(/async function restoreTaskById\(id, taskId, expectedGenerateType\) \{[\s\S]*?\n\}/)
|
||
|
||
assert.notEqual(restoreFunction, null)
|
||
assert.match(restoreFunction[0], /await getAiTask\(id, taskId\)/)
|
||
assert.match(restoreFunction[0], /task\.generateType !== expectedGenerateType/)
|
||
assert.match(source, /const requestedTaskId = normalizeProjectId\(route\.query\.taskId\)/)
|
||
assert.match(source, /requestedTaskId[\s\S]*restoreTaskById\(id, requestedTaskId, ONE_CLICK_GENERATE_TYPE\)[\s\S]*restoreActiveTask\(id, ONE_CLICK_GENERATE_TYPE\)/)
|
||
assert.match(source, /\(\) => route\.query\.taskId/)
|
||
})
|
||
|
||
test('restoring a route task cancels an earlier poll before applying the task', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const restoreFunction = source.match(/async function restoreTaskById\(id, taskId, expectedGenerateType\) \{[\s\S]*?\n\}/)
|
||
|
||
assert.notEqual(restoreFunction, null)
|
||
const cancelIndex = restoreFunction[0].indexOf('pollingVersions[taskChannel(task.generateType)] += 1')
|
||
const assignIndex = restoreFunction[0].indexOf('targetRef.value = task')
|
||
assert.notEqual(cancelIndex, -1)
|
||
assert.notEqual(assignIndex, -1)
|
||
assert.equal(cancelIndex < assignIndex, true)
|
||
assert.match(restoreFunction[0], /FINISHED_AI_STATUSES\.includes\(task\.status\)[\s\S]*oneClickGenerating\.value = false/)
|
||
assert.match(restoreFunction[0], /String\(oneClickAiTask\.value\?\.taskId \|\| ''\) === String\(task\.taskId\)/)
|
||
})
|
||
|
||
test('generate page records a newly-created one-click task in the route', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const handlerIndex = source.indexOf('async function handleGenerateCompleteProject()')
|
||
const createTaskIndex = source.indexOf('createOneClickProjectTask(id, oneClickTaskPayload())', handlerIndex)
|
||
const routeSyncIndex = source.indexOf('syncOneClickTaskRoute(id, task.taskId)', createTaskIndex)
|
||
const waitTaskIndex = source.indexOf('waitForAiTask(id, task)', createTaskIndex)
|
||
|
||
assert.notEqual(createTaskIndex, -1)
|
||
assert.notEqual(routeSyncIndex, -1)
|
||
assert.notEqual(waitTaskIndex, -1)
|
||
assert.equal(createTaskIndex < routeSyncIndex, true)
|
||
assert.equal(routeSyncIndex < waitTaskIndex, true)
|
||
assert.match(source.slice(createTaskIndex, waitTaskIndex), /syncOneClickTaskRoute\(id, task\.taskId\)\.catch\(\(\) => \{\}\)/)
|
||
})
|
||
|
||
test('generate page integrates project generation history with route state', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /import OneClickGenerationHistoryDrawer from '@\/components\/OneClickGenerationHistoryDrawer\.vue'/)
|
||
assert.match(source, /<OneClickGenerationHistoryDrawer/)
|
||
assert.match(source, /v-model="generationHistoryVisible"/)
|
||
assert.match(source, /@select="handleSelectOneClickHistoryTask"/)
|
||
assert.match(source, /@retry="handleRetryOneClickHistoryTask"/)
|
||
assert.match(source, /@click="openOneClickGenerationHistory"/)
|
||
assert.match(source, /\(\) => route\.query\.history/)
|
||
assert.match(source, /history: '1'/)
|
||
assert.match(source, /delete query\.history/)
|
||
})
|
||
|
||
test('generate history views and retries the exact server task', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const selectHandler = source.match(/async function handleSelectOneClickHistoryTask\(item\) \{[\s\S]*?\n\}/)
|
||
const retryHandlerStart = source.indexOf('async function handleRetryOneClickHistoryTask(item)')
|
||
const retryHandlerEnd = source.indexOf('\n}', retryHandlerStart)
|
||
const retryHandler = retryHandlerStart >= 0 && retryHandlerEnd > retryHandlerStart
|
||
? source.slice(retryHandlerStart, retryHandlerEnd + 2)
|
||
: ''
|
||
|
||
assert.notEqual(selectHandler, null)
|
||
assert.match(selectHandler[0], /restoreTaskById\(projectId\.value, item\.taskId, ONE_CLICK_GENERATE_TYPE\)/)
|
||
assert.match(selectHandler[0], /syncOneClickTaskRoute\(projectId\.value, item\.taskId\)/)
|
||
assert.match(retryHandler, /ElMessageBox\.confirm/)
|
||
assert.match(retryHandler, /oneClickStageLabel\(item\.resumeStage\)/)
|
||
assert.match(retryHandler, /restoreTaskById\(projectId\.value, item\.taskId, ONE_CLICK_GENERATE_TYPE\)/)
|
||
assert.match(retryHandler, /await retryTask\(oneClickAiTask\)/)
|
||
assert.match(retryHandler, /generationHistoryDrawerRef\.value\?\.reload\(\)/)
|
||
})
|
||
|
||
test('generate page places source preview and download actions next to the one-click generate button', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const generateButtonIndex = source.indexOf('@click="handleGenerateCompleteProject"')
|
||
const previewButtonIndex = source.indexOf('@click="openGeneratedPreview"')
|
||
const downloadButtonIndex = source.indexOf('@click="handleDownloadOneClickSource"')
|
||
const progressCardIndex = source.indexOf('class="one-click-progress-card"')
|
||
const progressActions = source.match(/<div v-if="oneClickShowRecoveryActions" class="one-click-progress-actions">[\s\S]*?<\/div>/)
|
||
const previewButton = source.match(/<el-button(?=[^>]*@click="openGeneratedPreview")[^>]*>/)
|
||
const downloadButton = source.match(/<el-button(?=[^>]*@click="handleDownloadOneClickSource")[^>]*>/)
|
||
|
||
assert.notEqual(generateButtonIndex, -1)
|
||
assert.notEqual(previewButtonIndex, -1)
|
||
assert.notEqual(downloadButtonIndex, -1)
|
||
assert.notEqual(progressCardIndex, -1)
|
||
assert.equal(generateButtonIndex < previewButtonIndex, true)
|
||
assert.equal(previewButtonIndex < downloadButtonIndex, true)
|
||
assert.equal(downloadButtonIndex < progressCardIndex, true)
|
||
assert.match(source, /class="one-click-primary-actions toolbar"/)
|
||
assert.notEqual(progressActions, null)
|
||
assert.equal(progressActions[0].includes('openGeneratedPreview'), false)
|
||
assert.equal(progressActions[0].includes('handleDownloadOneClickSource'), false)
|
||
assert.notEqual(previewButton, null)
|
||
assert.notEqual(downloadButton, null)
|
||
assert.match(previewButton[0], /type="primary"/)
|
||
assert.match(downloadButton[0], /type="primary"/)
|
||
})
|
||
|
||
test('generate page removes code analysis from one-click result actions', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const primaryActions = source.match(/<div class="one-click-primary-actions toolbar">[\s\S]*?<\/div>/)
|
||
const resultHeader = source.match(/<div v-if="!expertMode && oneClickResult" class="panel result-card">[\s\S]*?<div class="content">/)
|
||
|
||
assert.notEqual(primaryActions, null)
|
||
assert.notEqual(resultHeader, null)
|
||
assert.doesNotMatch(primaryActions[0], /代码解读/)
|
||
assert.doesNotMatch(resultHeader[0], /代码解读/)
|
||
assert.doesNotMatch(source, /openCodeAnalysis/)
|
||
assert.doesNotMatch(source, /\bSearch\b/)
|
||
})
|
||
|
||
test('generate page loads managed style preset choices and sends the selected preset', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const oneClickPayload = source.match(/function oneClickTaskPayload\(\) \{([\s\S]*?)\n\}/)
|
||
const expertPayload = source.match(/function aiTaskPayload\(generateType\) \{([\s\S]*?)\n\}/)
|
||
const projectPayload = source.match(/function projectPayload\(\) \{([\s\S]*?)\n\}/)
|
||
|
||
assert.match(source, /const availablePageStylePresets = ref\(\[\]\)/)
|
||
assert.match(source, /stylePreset: ''/)
|
||
assert.match(source, /projectForm\.stylePreset/)
|
||
assert.match(source, /const result = await listPageTemplateOptions\(\)/)
|
||
assert.doesNotMatch(source, /const PAGE_STYLE_PRESETS/)
|
||
assert.doesNotMatch(source, /const VISUAL_SOLUTION_DEFAULTS/)
|
||
assert.notEqual(oneClickPayload, null)
|
||
assert.match(oneClickPayload[1], /stylePreset: projectForm\.stylePreset/)
|
||
assert.notEqual(expertPayload, null)
|
||
assert.match(expertPayload[1], /stylePreset: projectForm\.stylePreset/)
|
||
assert.notEqual(projectPayload, null)
|
||
assert.match(projectPayload[1], /stylePreset: projectForm\.stylePreset/)
|
||
})
|
||
|
||
test('generate page exposes code template bundle selection and submits it', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const apiSource = readSource('api/project.js')
|
||
const advancedSettings = source.match(/<el-collapse-item title="高级设置" name="advanced">[\s\S]*?<\/el-collapse-item>/)
|
||
const projectPayload = source.match(/function projectPayload\(\) \{([\s\S]*?)\n\}/)
|
||
const oneClickPayload = source.match(/function oneClickTaskPayload\(\) \{([\s\S]*?)\n\}/)
|
||
const expertPayload = source.match(/function aiTaskPayload\(generateType\) \{([\s\S]*?)\n\}/)
|
||
|
||
assert.match(apiSource, /export function listCodeTemplateBundles\(\)/)
|
||
assert.match(apiSource, /url: '\/front\/project\/options\/code-templates'/)
|
||
assert.match(apiSource, /export function getProjectSupportedTemplateTypes\(projectId\)/)
|
||
assert.match(source, /const CODE_TEMPLATE_FALLBACK = 'qing'/)
|
||
assert.match(source, /const codeTemplateBundles = ref\(\[\]\)/)
|
||
assert.match(source, /projectForm\.codeTemplate/)
|
||
assert.match(source, /loadCodeTemplateBundles/)
|
||
assert.match(source, /class="code-template-option"/)
|
||
assert.notEqual(advancedSettings, null)
|
||
assert.match(advancedSettings[0], /label="代码模板"/)
|
||
assert.match(advancedSettings[0], /class="code-template-cover"/)
|
||
assert.match(advancedSettings[0], /templatePreviewUrl\(selectedCodeTemplateBundle\)/)
|
||
assert.match(advancedSettings[0], /selectedCodeTemplateBundle/)
|
||
assert.match(advancedSettings[0], /bundle\.previewImage/)
|
||
assert.match(source, /function templatePreviewUrl\(bundle\)/)
|
||
assert.notEqual(projectPayload, null)
|
||
assert.match(projectPayload[1], /codeTemplate: projectForm\.codeTemplate \|\| CODE_TEMPLATE_FALLBACK/)
|
||
assert.notEqual(oneClickPayload, null)
|
||
assert.match(oneClickPayload[1], /codeTemplate: projectForm\.codeTemplate \|\| CODE_TEMPLATE_FALLBACK/)
|
||
assert.notEqual(expertPayload, null)
|
||
assert.match(expertPayload[1], /codeTemplate: projectForm\.codeTemplate \|\| CODE_TEMPLATE_FALLBACK/)
|
||
})
|
||
|
||
test('generate page presents style presets as visual themes with previews', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /label="视觉方案"/)
|
||
assert.match(source, /class="style-theme-option"/)
|
||
assert.match(source, /class="style-theme-preview"/)
|
||
assert.match(source, /function themePreviewStyle/)
|
||
assert.match(source, /v-for="preset in availablePageStylePresets"/)
|
||
assert.match(source, /result\.themes\.map\(normalizePageStylePreset\)/)
|
||
assert.doesNotMatch(source, /business_clean/)
|
||
assert.doesNotMatch(source, /card_portal/)
|
||
assert.match(source, /solutionNavigation\(solution\.shell, 'frontend'\)/)
|
||
assert.match(source, /solutionNavigation\(solution\.shell, 'admin'\)/)
|
||
assert.match(source, /navigationLabel\(solutionNavigation\(solution\.shell, 'frontend'\), '前台'\)/)
|
||
assert.match(source, /navigationLabel\(solutionNavigation\(solution\.shell, 'admin'\), '后台'\)/)
|
||
assert.match(source, /scope === 'admin' \? 'adminNavigation' : 'frontendNavigation'/)
|
||
assert.match(source, /列表按图片自适应/)
|
||
assert.match(source, /'preview-layout-adaptive'/)
|
||
})
|
||
|
||
test('generate page shows one managed-theme error and never falls back to local presets', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /v-if="pageStylePresetError"/)
|
||
assert.match(source, /视觉方案不可用,请先在后台“页面主题管理”中配置并启用方案,然后刷新页面。/)
|
||
assert.match(source, /availablePageStylePresets\.value = \[\]/)
|
||
assert.match(source, /pageStylePresetError\.value = PAGE_STYLE_PRESET_ERROR/)
|
||
assert.doesNotMatch(source, /themes\.length \? themes :/)
|
||
assert.doesNotMatch(source, /result\?\.solutions/)
|
||
})
|
||
|
||
test('generate page keeps visual themes compact and removes the large impact preview', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const advancedSettings = source.match(/<el-collapse-item title="高级设置" name="advanced">[\s\S]*?<\/el-collapse-item>/)
|
||
|
||
assert.notEqual(advancedSettings, null)
|
||
assert.match(advancedSettings[0], /label="视觉方案"/)
|
||
assert.match(advancedSettings[0], /label="用户前台"/)
|
||
assert.match(advancedSettings[0], /label="代码模板"/)
|
||
assert.doesNotMatch(source, /class="style-theme-impact"/)
|
||
assert.doesNotMatch(source, /class="theme-impact-shell"/)
|
||
assert.doesNotMatch(source, /class="theme-impact-points"/)
|
||
assert.doesNotMatch(source, /class="style-theme-note"/)
|
||
})
|
||
|
||
test('generate page keeps visual theme cards readable and aligned in the new settings grid', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /class="advanced-settings-body"/)
|
||
assert.match(source, /class="advanced-secondary-grid"/)
|
||
assert.match(source, /grid-template-columns: repeat\(auto-fill, minmax\(min\(260px, 100%\), 1fr\)\)/)
|
||
assert.match(source, /\.style-theme-option \{[\s\S]*?width: 100%;[\s\S]*?min-height: 138px;/)
|
||
assert.match(source, /\.style-theme-card \{[\s\S]*?width: 100%;[\s\S]*?height: 100%;/)
|
||
assert.match(source, /\.style-solution-tags \{[\s\S]*?display: grid;/)
|
||
})
|
||
|
||
test('generate page displays one-click business loop report', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('<el-divider content-position="left">业务闭环</el-divider>'), true)
|
||
assert.equal(source.includes('const oneClickReport = computed'), true)
|
||
assert.equal(source.includes('const oneClickMissingRequiredActions = computed'), true)
|
||
assert.equal(source.includes('const oneClickBusinessLoopWarnings = computed'), true)
|
||
assert.equal(source.includes('v-if="oneClickMissingRequiredActions.length"'), true)
|
||
assert.equal(source.includes('v-if="oneClickBusinessLoopWarnings.length"'), true)
|
||
assert.equal(source.includes('businessLoopWarnings'), true)
|
||
assert.equal(source.includes('oneClickReport.businessLoopComplete'), true)
|
||
assert.equal(source.includes('oneClickReport.loopActions'), true)
|
||
})
|
||
|
||
test('generate page recognizes applied business loop actions stored with business prefix', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const placementFunction = source.match(/function businessActionPlacement\(design, actionCode\) \{[\s\S]*?\n\}/)
|
||
const actionCodesFunction = source.match(/function businessActionCodes\(actionCode\) \{[\s\S]*?\n\}/)
|
||
|
||
assert.notEqual(placementFunction, null)
|
||
assert.notEqual(actionCodesFunction, null)
|
||
assert.match(placementFunction[0], /businessActionCodes\(actionCode\)/)
|
||
|
||
const businessActionCodes = new Function(`${actionCodesFunction[0]}; return businessActionCodes`)()
|
||
assert.deepEqual(businessActionCodes('borrow_book'), ['borrow_book', 'business:borrow_book'])
|
||
assert.deepEqual(businessActionCodes('business:return_book'), ['return_book', 'business:return_book'])
|
||
})
|
||
|
||
test('generate page prefers exact page binding before table fallback', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const helperStart = source.indexOf('function findBusinessLoopBindingDesign')
|
||
const placementStart = source.indexOf('function businessActionPlacement')
|
||
const helperSource = helperStart >= 0 && placementStart > helperStart
|
||
? source.slice(helperStart, placementStart)
|
||
: ''
|
||
|
||
assert.notEqual(helperStart, -1)
|
||
assert.match(source, /const boundDesign = findBusinessLoopBindingDesign\(designs, binding\)/)
|
||
assert.match(helperSource, /matchedByPageCode/)
|
||
assert.match(helperSource, /matchedByTableName/)
|
||
assert.equal(helperSource.indexOf('matchedByPageCode') < helperSource.indexOf('matchedByTableName'), true)
|
||
})
|
||
|
||
test('generate page labels asynchronous run preview states', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('function oneClickPreviewStatusText'), true)
|
||
assert.equal(source.includes('function oneClickPreviewStatusTagType'), true)
|
||
assert.equal(source.includes('const oneClickCanRestartRunPreview = computed'), true)
|
||
assert.equal(source.includes('handleRestartOneClickRunPreview'), true)
|
||
assert.equal(source.includes('const previewStatus = oneClickDisplayResult.value?.previewStatus'), true)
|
||
assert.equal(source.includes("case 'STARTING'"), true)
|
||
assert.equal(source.includes("case 'FAILED'"), true)
|
||
assert.equal(source.includes('oneClickPreviewStatusText(oneClickResult)'), true)
|
||
assert.equal(source.includes('oneClickPreviewStatusTagType(oneClickResult)'), true)
|
||
})
|
||
|
||
test('generate page displays one-click run preview expiration time', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('预览到期时间'), true)
|
||
assert.equal(source.includes('oneClickPreviewExpiresAtText(oneClickResult)'), true)
|
||
assert.match(source, /expiresAt: status\.expiresAt \|\| current\.expiresAt/)
|
||
})
|
||
|
||
test('generate page opens one-click result urls in a new page', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /import \{[^}]*Link[^}]*View[^}]*\} from '@element-plus\/icons-vue'/)
|
||
assert.equal(source.includes('oneClickResultUrlText(oneClickResult.backendUrl)'), true)
|
||
assert.equal(source.includes('oneClickResultUrlText(oneClickResult.frontendUrl)'), true)
|
||
assert.equal(source.includes('oneClickResultUrlText(oneClickResult.adminFrontendUrl)'), true)
|
||
assert.equal(source.includes('@click="openOneClickResultUrl(oneClickResult.backendUrl)"'), true)
|
||
assert.equal(source.includes('@click="openOneClickResultUrl(oneClickResult.frontendUrl)"'), true)
|
||
assert.equal(source.includes('@click="openOneClickResultUrl(oneClickResult.adminFrontendUrl)"'), true)
|
||
assert.match(source, /function oneClickResultUrlText\(url\) \{[\s\S]*?return url \|\| '-'/)
|
||
assert.match(source, /function openOneClickResultUrl\(url\) \{[\s\S]*?if \(!url\) return[\s\S]*?window\.open\(url, '_blank', 'noopener,noreferrer'\)/)
|
||
})
|
||
|
||
test('generate page polls realtime run preview status for pending one-click previews', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /const RUN_PREVIEW_PENDING_STATUSES = \['PREPARING', 'STARTING'\]/)
|
||
assert.match(source, /let oneClickRunPreviewPollingTimer = null/)
|
||
assert.match(source, /function startOneClickRunPreviewPolling/)
|
||
assert.match(source, /function clearOneClickRunPreviewPolling/)
|
||
assert.match(source, /async function pollOneClickRunPreviewStatus/)
|
||
assert.match(source, /function applyOneClickRunPreviewStatus/)
|
||
assert.match(source, /function isRunPreviewProbeCandidate/)
|
||
assert.match(source, /function shouldPollOneClickRunPreview/)
|
||
assert.match(source, /await getRunPreviewStatus\(id\)/)
|
||
assert.match(source, /watch\(\s*\(\) => oneClickDisplayResult\.value\?\.previewStatus/)
|
||
assert.match(source, /shouldPollOneClickRunPreview\(status\)/)
|
||
assert.match(source, /clearOneClickRunPreviewPolling\(\)/)
|
||
assert.match(source, /status === 'FAILED'[\s\S]*result\?\.downloadReady[\s\S]*result\?\.failedStage === 'VERIFY_AND_PREVIEW'/)
|
||
assert.match(source, /const previewRecovered = status\.status === 'RUNNING' \|\| isRunPreviewPending\(status\.status\)/)
|
||
assert.match(source, /oneClickAiTask\.value = \{[\s\S]*resultPayload: JSON\.stringify\(nextResult\)/)
|
||
})
|
||
|
||
test('generate page one-click validation requires a managed visual theme', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const match = source.match(/function validateOneClickInput\(\) \{([\s\S]*?)\n\}/)
|
||
|
||
assert.notEqual(match, null)
|
||
assert.match(match[1], /projectForm\.projectName/)
|
||
assert.match(match[1], /projectForm\.stylePreset/)
|
||
assert.match(match[1], /availablePageStylePresets\.value\.some/)
|
||
assert.doesNotMatch(match[1], /projectForm\.keyword/)
|
||
})
|
||
|
||
test('generate page one-click mode no longer exposes supplemental requirements', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const oneClickForm = source.match(/<div v-if="!expertMode" class="panel one-click-card"[\s\S]*?<div v-if="oneClickStatusVisible"/)
|
||
const oneClickPayload = source.match(/function oneClickTaskPayload\(\) \{([\s\S]*?)\n\}/)
|
||
|
||
assert.notEqual(oneClickForm, null)
|
||
assert.doesNotMatch(oneClickForm[0], /补充需求/)
|
||
assert.doesNotMatch(oneClickForm[0], /projectForm\.keyword/)
|
||
assert.notEqual(oneClickPayload, null)
|
||
assert.doesNotMatch(oneClickPayload[1], /projectForm\.keyword/)
|
||
assert.match(oneClickPayload[1], /extraRequirements: ''/)
|
||
})
|
||
|
||
test('generate page preserves the original description while AI completes detailed business rules', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const apiSource = readSource('api/project.js')
|
||
const oneClickForm = source.match(/<div v-if="!expertMode" class="panel one-click-card"[\s\S]*?<div v-if="oneClickStatusVisible"/)
|
||
const mergeHelper = source.match(/const AI_REQUIREMENT_MARKER = '【AI补全需求】'[\s\S]*?\n\}/)
|
||
|
||
assert.notEqual(oneClickForm, null)
|
||
assert.match(oneClickForm[0], /class="project-description-label"/)
|
||
assert.match(oneClickForm[0], /AI补全需求/)
|
||
assert.match(oneClickForm[0], /@click="handleGenerateRequirement"/)
|
||
assert.match(oneClickForm[0], /:loading="generatingRequirement"/)
|
||
assert.match(oneClickForm[0], /v-model="projectForm\.projectDesc"/)
|
||
assert.match(oneClickForm[0], /:rows="8"/)
|
||
assert.match(oneClickForm[0], /期限、状态、数量限制、金额公式和异常边界/)
|
||
assert.match(source, /generateRequirementSuggestion/)
|
||
assert.match(source, /const generatingRequirement = ref\(false\)/)
|
||
assert.match(source, /await generateRequirementSuggestion\(\{[\s\S]*projectName: projectForm\.projectName[\s\S]*projectDesc: projectForm\.projectDesc/)
|
||
assert.match(source, /const AI_REQUIREMENT_MARKER = '【AI补全需求】'/)
|
||
assert.match(source, /function mergeRequirementSuggestion\(currentDescription, generatedRequirement\)/)
|
||
assert.match(source, /const supplement = `\$\{AI_REQUIREMENT_MARKER\}\\n\$\{generated\}`/)
|
||
assert.match(source, /return original \? `\$\{original\}\\n\\n\$\{supplement\}` : supplement/)
|
||
assert.match(source, /projectForm\.projectDesc = mergeRequirementSuggestion\(projectForm\.projectDesc, requirement\)/)
|
||
assert.match(source, /需求已补全,请确认 AI 假设和业务规则/)
|
||
assert.notEqual(mergeHelper, null)
|
||
const mergeRequirementSuggestion = new Function(`${mergeHelper[0]}; return mergeRequirementSuggestion`)()
|
||
const first = mergeRequirementSuggestion('读者可以借书', '业务规则:借期30天')
|
||
assert.equal(first, '读者可以借书\n\n【AI补全需求】\n业务规则:借期30天')
|
||
assert.equal(
|
||
mergeRequirementSuggestion(first, '业务规则:借期30天,逾期每天0.50元'),
|
||
'读者可以借书\n\n【AI补全需求】\n业务规则:借期30天,逾期每天0.50元'
|
||
)
|
||
assert.equal(mergeRequirementSuggestion('', '业务规则:借期30天'), '【AI补全需求】\n业务规则:借期30天')
|
||
assert.doesNotMatch(source, /function generateProjectRequirementSuggestion\(projectName\)/)
|
||
assert.doesNotMatch(source, /pattern: \/图书/)
|
||
assert.match(apiSource, /export function generateRequirementSuggestion\(data\)/)
|
||
assert.match(apiSource, /url: '\/front\/project\/generate-requirement'/)
|
||
})
|
||
|
||
test('generate page expert mode no longer exposes requirement editing', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const expertSection = source.match(/<div v-if="expertMode" class="panel expert-adjustment-panel"[\s\S]*?<div v-if="expertMode" v-loading="oneClickTaskBusy" class="panel designer-panel database-adjustment-anchor">/)
|
||
const expertPayload = source.match(/function aiTaskPayload\(generateType\) \{([\s\S]*?)\n\}/)
|
||
|
||
assert.notEqual(expertSection, null)
|
||
assert.doesNotMatch(expertSection[0], /label="项目描述"/)
|
||
assert.doesNotMatch(expertSection[0], /需求关键词/)
|
||
assert.doesNotMatch(expertSection[0], /projectForm\.keyword/)
|
||
assert.notEqual(expertPayload, null)
|
||
assert.match(expertPayload[1], /projectDesc: projectForm\.projectDesc/)
|
||
assert.match(expertPayload[1], /extraRequirements: ''/)
|
||
})
|
||
|
||
test('generate page sends current flow_config with expert AI tasks', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const expertPayload = source.match(/function aiTaskPayload\(generateType\) \{([\s\S]*?)\n\}/)
|
||
|
||
assert.notEqual(expertPayload, null)
|
||
assert.match(source, /function currentFlowConfigPayload\(\)/)
|
||
assert.match(expertPayload[1], /flowConfig: currentFlowConfigPayload\(\)/)
|
||
})
|
||
|
||
test('generate page clears stale one-click failure state before retry polling', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /function prepareTaskForRetry\(task\)/)
|
||
assert.match(source, /resultPayload: ''/)
|
||
assert.match(source, /errorMessage: ''/)
|
||
assert.match(source, /oneClickResult\.value = null/)
|
||
assert.match(source, /if \(oneClickTaskBusy\.value\) return ''/)
|
||
assert.match(source, /if \(!retryAccepted\)[\s\S]*targetRef\.value = previousTask/)
|
||
})
|
||
|
||
test('generate page no longer exposes backend module selection', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('后台功能模块'), false)
|
||
assert.equal(source.includes('listAvailableModules'), false)
|
||
assert.equal(source.includes('projectForm.moduleIds'), false)
|
||
})
|
||
|
||
test('generate page no longer exposes industry template input', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.equal(source.includes('行业模板'), false)
|
||
assert.equal(source.includes('projectForm.industryTemplate'), false)
|
||
})
|
||
|
||
test('generate page leaves enough space for business loop placement status tags', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const bindingTable = source.match(/<el-table :data="businessLoopBindingRows" border>[\s\S]*?<\/el-table>/)
|
||
const statusColumn = bindingTable?.[0].match(/<el-table-column label="状态"[^>]*>/)
|
||
|
||
assert.notEqual(bindingTable, null)
|
||
assert.notEqual(statusColumn, null)
|
||
assert.match(statusColumn[0], /min-width="1[4-9]\d"/)
|
||
})
|
||
|
||
test('generate page projects every flow in a multi-flow config', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
|
||
assert.match(source, /version: '1\.1'/)
|
||
assert.match(source, /flows: \[\]/)
|
||
assert.match(source, /const flows = Array\.isArray\(config\?\.flows\) && config\.flows\.length \? config\.flows : \[config\]/)
|
||
assert.match(source, /flows\.forEach\(\(flow\) =>/)
|
||
assert.match(source, /stateMachines\.push\(\{/)
|
||
assert.match(source, /actions\.push\(\.\.\.flowActions\)/)
|
||
})
|
||
|
||
test('generate page only reports completion after the server audit passes', () => {
|
||
const source = readSource('views/GenerateView.vue')
|
||
const completion = source.match(/const businessLoopPlanComplete = computed\(\(\) => \{([\s\S]*?)\n\}\)/)
|
||
|
||
assert.notEqual(completion, null)
|
||
assert.match(completion[1], /businessLoopAuditComplete\.value/)
|
||
assert.match(source, /const requirementLoopAudit = computed/)
|
||
assert.doesNotMatch(source, /countActionEffects/)
|
||
})
|