Files
yidaima/RuoYi-Vue/easycode-web/src/views/generateView.test.mjs

694 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/)
})