import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test, { after } from 'node:test'; import { fileURLToPath } from 'node:url'; import { createServer } from 'vite'; const vite = await createServer({ root: fileURLToPath(new URL('..', import.meta.url)), configFile: false, optimizeDeps: { noDiscovery: true }, server: { middlewareMode: true, hmr: false }, appType: 'custom' }); after(async () => { await vite.close(); }); const candidateView = await vite.ssrLoadModule('/src/services/candidate-view.ts'); const { buildCandidateFlow, candidateRoutes, formatCandidateFileSize, materialStatusTone } = candidateView; const record = (overrides = {}) => ({ time: '2026-07-10 10:00:00', candidate: '候选人0001', position: '生活顾问', score: 86, result: '建议进入下一轮', summary: '沟通完整', ...overrides }); const material = (overrides = {}) => ({ id: 1, ossId: 2, fileName: '简历.pdf', materialType: '补充资料', fileSize: 2048, status: '待审核', time: '2026-07-10 10:30:00', ...overrides }); const pageSource = async (path) => readFile(new URL(path, import.meta.url), 'utf8'); test('候选流程从面试、补资料、审核到完成只依赖真实记录', () => { assert.deepEqual(buildCandidateFlow([], []), { stage: 'interview', badge: '待面试', title: '完成结构化面试', desc: '按岗位题目完成回答,提交后生成正式面试记录。', progress: 0, progressText: '0/2', notice: '待完成结构化面试', cta: '开始面试', route: candidateRoutes.interview }); assert.equal(buildCandidateFlow([record()], []).stage, 'materials'); assert.equal(buildCandidateFlow([record()], []).route, candidateRoutes.materials); assert.equal(buildCandidateFlow([record()], [material({ status: '已驳回' })]).badge, '资料被驳回'); assert.equal(buildCandidateFlow([record()], [material()]).stage, 'review'); assert.equal(buildCandidateFlow([record()], [material()]).route, candidateRoutes.progress); assert.equal(buildCandidateFlow([record()], [material({ status: '已通过' })]).stage, 'complete'); assert.equal(buildCandidateFlow([record()], [material({ status: '已通过' })]).progress, 100); }); test('资料展示格式保持三态且文件大小可读', () => { assert.equal(materialStatusTone('已通过'), 'passed'); assert.equal(materialStatusTone('已驳回'), 'rejected'); assert.equal(materialStatusTone('待审核'), 'pending'); assert.equal(formatCandidateFileSize(0), '0KB'); assert.equal(formatCandidateFileSize(2048), '2KB'); assert.equal(formatCandidateFileSize(1572864), '1.5MB'); }); test('候选首页只做状态总览和独立页面分流', async () => { const source = await pageSource('../src/pages/candidate/index/index.vue'); assert.match(source, /getCandidateInterviewRecords/); assert.match(source, /getCandidateMaterials/); Object.values(candidateRoutes).forEach((route) => assert.match(source, new RegExp(route.replaceAll('/', '\\/')))); assert.doesNotMatch(source, /startCandidateInterview/); assert.doesNotMatch(source, /uploadCandidateMaterial/); assert.doesNotMatch(source, /searchKnowledge/); assert.doesNotMatch(source, /interviewQuestions/); }); test('候选端刷新失败不保留上一轮真实记录', async () => { const [home, materials, progress] = await Promise.all([ pageSource('../src/pages/candidate/index/index.vue'), pageSource('../src/pages/candidate/materials/index.vue'), pageSource('../src/pages/candidate/progress/index.vue') ]); assert.match(home, /catch \(error\) \{\n\s*clearData\(\);/); assert.match(materials, /catch \(error\) \{\n\s*clearData\(\);/); assert.match(progress, /catch \(error\) \{\n\s*clearData\(\);/); }); test('面试、资料、进度和预习页各自只承载一类真实任务', async () => { const [interview, materials, progress, study] = await Promise.all([ pageSource('../src/pages/candidate/interview/index.vue'), pageSource('../src/pages/candidate/materials/index.vue'), pageSource('../src/pages/candidate/progress/index.vue'), pageSource('../src/pages/candidate/study/index.vue') ]); assert.match(interview, /startCandidateInterview/); assert.match(interview, /saveInterviewAnswer/); assert.match(interview, /finishCandidateInterview/); assert.match(interview, /transcribeSpeechFile/); assert.match(interview, /录音转文字/); assert.doesNotMatch(interview, /uploadCandidateMaterial|searchKnowledge/); assert.match(materials, /chooseCandidateFile/); assert.match(materials, /uploadCandidateMaterial/); assert.doesNotMatch(materials, /startCandidateInterview|searchKnowledge/); assert.match(progress, /getCandidateInterviewRecords/); assert.match(progress, /getCandidateMaterials/); assert.match(progress, /latestRecord\.value\?\.reviewStatus/); assert.match(progress, /补充资料审核/); assert.doesNotMatch(progress, /startCandidateInterview|uploadCandidateMaterial|searchKnowledge/); assert.match(study, /searchKnowledge/); assert.doesNotMatch(study, /startCandidateInterview|uploadCandidateMaterial/); }); test('五个候选页面登录都使用 redirectTo 并保留当前任务页', async () => { const sources = await Promise.all([ pageSource('../src/pages/candidate/index/index.vue'), pageSource('../src/pages/candidate/interview/index.vue'), pageSource('../src/pages/candidate/materials/index.vue'), pageSource('../src/pages/candidate/progress/index.vue'), pageSource('../src/pages/candidate/study/index.vue') ]); sources.forEach((source) => { const goLogin = source.match(/const goLogin = \(\) => \{\n[\s\S]*?\n\};/)?.[0] || ''; assert.match(goLogin, /rememberLoginRedirect\(currentPath\)/); assert.match(goLogin, /uni\.redirectTo\(\{ url: '\/pages\/auth\/login\/index' \}\)/); assert.doesNotMatch(goLogin, /uni\.navigateTo\(/); }); }); test('候选子路由已注册为真实页面', async () => { const pages = await pageSource('../src/pages.json'); assert.match(pages, /pages\/candidate\/interview\/index/); assert.match(pages, /pages\/candidate\/materials\/index/); assert.match(pages, /pages\/candidate\/progress\/index/); assert.match(pages, /pages\/candidate\/study\/index/); }); test('候选面试生成题目和评分结果恢复语音播报', async () => { const interview = await pageSource('../src/pages/candidate/interview/index.vue'); assert.match(interview, /createSpeechPlaybackController/); assert.match(interview, /question-voice-button/); assert.match(interview, /speakQuestion/); assert.match(interview, /interviewResultSpeech/); assert.match(interview, /停止播报/); assert.match(interview, /onUnload/); assert.match(interview, /onHide\(\(\) => \{[\s\S]*speechPlayback\.stop\(\);[\s\S]*cancelQuestionRecording\(\);/); assert.match(interview, /const generation = \+\+questionCaptureGeneration/); assert.match(interview, /generation !== questionCaptureGeneration/); assert.match(interview, /recordingQuestionStarting\.value/); assert.match(interview, /abortQuestionTranscription\?\.\(\)/); assert.match(interview, /generation !== questionTranscriptionGeneration/); });