Files
prop-ai-hr/mobile-uni/tests/mobile-user-pages.test.mjs
T

464 lines
21 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const pageSource = async (path) => readFile(new URL(path, import.meta.url), 'utf8');
test('共享输入框使用 border-box 且不覆盖原生控件', async () => {
const source = await pageSource('../src/styles/base.css');
const textareaStyle = source.match(/\.textarea\s*\{[\s\S]*?\n\}/)?.[0] || '';
assert.doesNotMatch(source, /view,\s*\ntext,\s*\ninput,/);
assert.doesNotMatch(source, /view,\s*\ntext,\s*\ntextarea,/);
assert.doesNotMatch(source, /(^|\n)input\s*\{[^}]*box-sizing:\s*border-box/m);
assert.doesNotMatch(source, /(^|\n)textarea\s*\{[^}]*box-sizing:\s*border-box/m);
assert.match(textareaStyle, /box-sizing:\s*border-box/);
});
test('SOP 查询框不再覆盖 textarea 的 box-sizing', async () => {
const source = await pageSource('../src/pages/user/sop/index.vue');
const queryPanelStyle = source.match(/\.query-panel \.textarea \{[\s\S]*?\n\}/)?.[0] || '';
assert.equal(queryPanelStyle.includes('box-sizing'), false);
});
test('员工首页核心工具保留真实业务入口', async () => {
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /<text class="voice-title">问老师傅<\/text>/);
assert.match(source, /<text class="question-board-title">问题榜<\/text>/);
assert.match(source, /@click="goSop"/);
assert.match(source, /const goPractice = \(\) =>/);
assert.match(source, /const openQuestions = \(\) =>/);
});
test('训练页提交失败会回滚本次乐观追加的对话', async () => {
const source = await pageSource('../src/pages/user/practice/index.vue');
assert.match(source, /const previousTurns = turns\.value\.slice\(\)/);
assert.match(source, /turns\.value = previousTurns/);
assert.match(source, /uni\.showToast\(\{ title: message\.value \|\| '提交失败', icon: 'none'/);
});
test('manifest 明确声明为 uni-app Vue3 CLI 工程', async () => {
const source = await pageSource('../src/manifest.json');
assert.match(source, /"vueVersion"\s*:\s*"3"/);
});
test('员工端 tabBar 继续保留四个固定入口', async () => {
const source = await pageSource('../src/pages.json');
assert.match(source, /"selectedColor"\s*:\s*"#e60012"/);
assert.match(source, /pages\/user\/today\/index/);
assert.match(source, /pages\/user\/practice\/index/);
assert.match(source, /pages\/user\/sop\/index/);
assert.match(source, /pages\/user\/profile\/index/);
});
test('登录页按高保真原型保留品牌结构和紧凑验证码行', async () => {
const source = await pageSource('../src/pages/auth/login/index.vue');
assert.match(source, /高效协同 · 贴心服务 · 让工作更简单/);
assert.match(source, /<uni-icons[^>]+type="phone"/);
assert.match(source, /<uni-icons[^>]+type="locked"/);
assert.doesNotMatch(source, /class="logo-badge"/);
assert.doesNotMatch(source, /grid-template-columns:\s*1fr;[\s\S]*?\.code-button/);
});
test('共享视觉基座包含员工端主色和卡片圆角 token', async () => {
const source = await pageSource('../src/styles/base.css');
assert.match(source, /--employee-primary:\s*#e60012/);
assert.match(source, /--employee-card-radius:\s*24px/);
assert.match(source, /\.employee-card\s*\{/);
});
test('员工端首页、问师傅和训练页接入当前共享基座', async () => {
const [todaySource, sopSource, practiceSource] = await Promise.all([
pageSource('../src/pages/user/today/index.vue'),
pageSource('../src/pages/user/sop/index.vue'),
pageSource('../src/pages/user/practice/index.vue')
]);
assert.match(todaySource, /employee-page-shell/);
assert.match(todaySource, /section-card employee-card/);
assert.match(sopSource, /sop-chat-page/);
assert.match(sopSource, /<ChatComposer/);
assert.match(practiceSource, /scenario-card employee-card/);
});
test('工作助手与查全网使用一致的来源切换头部', async () => {
const [sopSource, webAiSource, headerSource] = await Promise.all([
pageSource('../src/pages/user/sop/index.vue'),
pageSource('../src/pages/user/web-ai/index.vue'),
pageSource('../src/components/chat/AskSourceHeader.vue')
]);
for (const page of [sopSource, webAiSource]) {
assert.match(page, /import AskSourceHeader from ['"]@\/components\/chat\/AskSourceHeader\.vue['"]/);
assert.match(page, /<AskSourceHeader\b/);
assert.doesNotMatch(page, /(?:^|\n)\s*\.chat-source-switch(?:\s+\.source-option(?:\.active)?)?\s*\{/);
}
assert.match(headerSource, /<text class="head-title">问 · 数字师傅<\/text>/);
assert.match(headerSource, /\.source-option\s*\{[^}]*height:\s*44px/);
assert.match(headerSource, /\.new-chat-button\s*\{[^}]*min-height:\s*44px/);
assert.match(headerSource, /@click="emit\('newConversation'\)"/);
assert.doesNotMatch(sopSource, /(?:^|\n)\s*\.source-switch\s*\{/);
assert.doesNotMatch(sopSource, /(?:^|\n)\s*\.source-option(?:\.active)?\s*\{/);
});
test('登录后按组织身份切换主管首页', async () => {
const source = await pageSource('../src/services/position.ts');
assert.match(source, /\/api\/aihr\/mobile\/me/);
assert.match(source, /role === 'supervisor'/);
assert.match(source, /pages\/supervisor\/index\/index/);
assert.match(source, /removeStorageSync\(roleKey\)/);
});
test('普通员工请求主管深链时回到员工首页', async () => {
const source = await pageSource('../src/services/position.ts');
assert.match(source, /role === 'user' && isSupervisorRoute\(requested\)/);
assert.match(source, /return '\/pages\/user\/today\/index'/);
});
test('主管可切换到学员显示模式且组织角色保持不变', async () => {
const [positionSource, authSource, supervisorProfile, userProfile] = await Promise.all([
pageSource('../src/services/position.ts'),
pageSource('../src/services/auth.ts'),
pageSource('../src/pages/supervisor/profile/index.vue'),
pageSource('../src/pages/user/profile/index.vue')
]);
assert.match(authSource, /supervisorLearnerModeKey/);
assert.match(authSource, /enterSupervisorLearnerMode/);
assert.match(authSource, /leaveSupervisorLearnerMode/);
assert.match(authSource, /经理.*负责人.*组长/);
assert.match(authSource, /项目管理/);
assert.match(positionSource, /isSupervisorLearnerMode/);
assert.match(supervisorProfile, /进入学员模式/);
assert.match(userProfile, /返回主管工作台/);
assert.doesNotMatch(supervisorProfile, /setStorageSync\(roleKey,\s*'user'/);
});
test('员工首页保留师傅区、语音条、今日安排和问题榜结构', async () => {
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /mentor-hero/);
assert.match(source, /voice-panel/);
assert.match(source, /今日安排/);
assert.match(source, /问题榜/);
assert.match(source, /brand-bar/);
assert.match(source, /ask-card/);
assert.match(source, /小贴士/);
});
test('员工首页高保真入口继续使用真实图标和业务跳转', async () => {
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /type="notification"/);
assert.match(source, /type="chatboxes"/);
assert.match(source, /type="compose"/);
assert.match(source, /type="location"/);
assert.match(source, /const goSop = \(\) =>/);
assert.match(source, /const openPrimarySchedule = \(\) =>/);
assert.match(source, /getMobileHome\('user'\)/);
assert.match(source, /getPracticeAssignments\(true\)/);
assert.match(source, /src="\/static\/mentor-hifi\.png"/);
});
test('员工首页今日安排保留派发任务来源', async () => {
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /primaryAssignmentSourceLabel/);
assert.match(source, /source === 'retry'[\s\S]*错题再练/);
assert.match(source, /source === 'camp'[\s\S]*训练营/);
assert.match(source, /class="schedule-source"/);
assert.match(source, /今天先完成每日三题/);
});
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
const source = await pageSource('../src/pages/user/sop/index.vue');
assert.match(source, /query-panel/);
assert.match(source, /answer-panel/);
assert.match(source, /summary-panel/);
assert.match(source, /MY_PRACTICE_SUMMARY/);
assert.match(source, /TEAM_PRACTICE_SUMMARY/);
});
test('训练页保留任务卡、对话区、回复区和结果区并互斥切换', async () => {
const source = await pageSource('../src/pages/user/practice/index.vue');
assert.match(source, /scenario-card/);
assert.match(source, /assignment-panel/);
assert.match(source, /dialogue-card/);
assert.match(source, /reply-card/);
assert.match(source, /result-card/);
assert.match(source, /class="result-actions"/);
assert.match(source, />再练一次<\/button>/);
assert.match(source, /practiceView === 'prep'/);
assert.match(source, /practiceView === 'active'/);
assert.match(source, /practiceView\.value === 'result'/);
});
test('员工练习页显示当前岗位而不是固定生活顾问', async () => {
const source = await pageSource('../src/pages/user/practice/index.vue');
assert.match(source, /getSelectedPosition/);
assert.match(source, /const positionLabel = ref\(getSelectedPosition\(\) \|\| '生活顾问'\)/);
assert.match(source, /\{\{ positionLabel \}\} · 业主服务场景/);
assert.doesNotMatch(source, /scenario-desc">生活顾问 · 业主服务场景/);
});
test('员工问师傅页显示当前岗位而不是固定生活顾问', async () => {
const source = await pageSource('../src/pages/user/sop/index.vue');
assert.match(source, /getSelectedPosition/);
assert.match(source, /const positionLabel = ref\(getSelectedPosition\(\)\)/);
assert.match(source, /:context="`\$\{positionLabel\} · 住宅项目`"/);
assert.doesNotMatch(source, /sop-context">管家(生活顾问) · 住宅项目/);
});
test('训练页最近练过保留派发任务来源', async () => {
const source = await pageSource('../src/pages/user/practice/index.vue');
assert.match(source, /sourceLabel: assignmentSourceLabel\(item\.source\)/);
assert.match(source, /class="assignment-badge"/);
assert.match(source, /source === 'retry'[\s\S]*错题再练/);
assert.match(source, /source === 'camp'[\s\S]*训练营/);
});
test('学习页展示真实错题本并可进入已有再练任务', async () => {
const [source, practiceService] = await Promise.all([
pageSource('../src/pages/user/learning/index.vue'),
pageSource('../src/services/practice.ts')
]);
assert.match(source, /错题本/);
assert.match(source, /getPracticeMistakes/);
assert.match(source, /retryAssignmentId/);
assert.match(source, /pendingAssignmentKey/);
assert.match(source, /mistakeFilters/);
assert.match(source, /visibleMistakes/);
assert.match(source, /source === 'retry'[\s\S]*错题再练/);
assert.match(source, /source === 'camp'[\s\S]*训练营/);
assert.match(practiceService, /\/api\/aihr\/mobile\/practice\/mistakes/);
});
test('学习页允许员工确认本人正式岗前任务完成', async () => {
const [source, practiceService] = await Promise.all([
pageSource('../src/pages/user/learning/index.vue'),
pageSource('../src/services/practice.ts')
]);
assert.match(source, /completeOnboarding/);
assert.match(source, /确认完成/);
assert.match(practiceService, /\/api\/aihr\/mobile\/onboard\/tasks\/\$\{id\}\/complete/);
});
test('学习页显示岗位 SOP 适配状态而不冒充上岗资格', async () => {
const [source, knowledgeService] = await Promise.all([
pageSource('../src/pages/user/learning/index.vue'),
pageSource('../src/services/knowledge.ts')
]);
assert.match(source, /positionSopSummary/);
assert.match(source, /正式上岗资格/);
assert.match(knowledgeService, /\/api\/knowledge\/position-sop/);
});
test('员工画像明确参考等级与人工贡献分语义', async () => {
const source = await pageSource('../src/pages/user/profile/index.vue');
assert.match(source, /训练画像等级(参考)/);
assert.match(source, /人工贡献分/);
});
test('我的页面提供全局三档字号并持久化同步', async () => {
const [profileSource, accessibilitySource, appSource, baseSource] = await Promise.all([
pageSource('../src/pages/user/profile/index.vue'),
pageSource('../src/services/accessibility.ts'),
pageSource('../src/App.vue'),
pageSource('../src/styles/base.css')
]);
assert.match(accessibilitySource, /label:\s*'标准'/);
assert.match(accessibilitySource, /label:\s*'大字'/);
assert.match(accessibilitySource, /label:\s*'超大字'/);
assert.match(profileSource, /setAppFontSize/);
assert.match(accessibilitySource, /aihr_mobile_font_size/);
assert.match(accessibilitySource, /data-aihr-font-size/);
assert.match(appSource, /applyStoredFontSize/);
assert.match(baseSource, /data-aihr-font-size='large'\]\s+body[\s\S]*?zoom:\s*1\.12/);
assert.match(baseSource, /data-aihr-font-size='extra-large'\]\s+body[\s\S]*?zoom:\s*1\.25/);
});
test('员工岗位只能来自人力系统且个人页不再允许手动切换', async () => {
const [profileSource, positionSource, authSource] = await Promise.all([
pageSource('../src/pages/user/profile/index.vue'),
pageSource('../src/pages/auth/position/index.vue'),
pageSource('../src/services/auth.ts')
]);
assert.match(profileSource, /岗位来自人力系统组织同步/);
assert.doesNotMatch(profileSource, /changePosition/);
assert.match(positionSource, /员工端不支持手动切换岗位/);
assert.doesNotMatch(positionSource, /saveSelectedPosition|selectPosition/);
assert.match(authSource, /positionSourceKey/);
assert.match(authSource, /saveOrgPosition/);
});
test('主管专项从正式团队名单按姓名多选并一次批量派发', async () => {
const [source, practiceService] = await Promise.all([
pageSource('../src/pages/supervisor/assign/index.vue'),
pageSource('../src/services/practice.ts')
]);
assert.match(source, /selectedMemberIds/);
assert.match(source, /v-for="member in teamMembers"/);
assert.match(source, /member\.name \|\| '未命名员工'/);
assert.match(source, /snapshot\.members/);
assert.match(source, /targets: selectedMembers\.map/);
assert.match(source, /createPracticeAssignmentsBatch/);
assert.match(source, /当天/);
assert.match(source, /明天/);
assert.match(source, /3天内/);
assert.match(source, /7天内/);
assert.match(source, /dueDate: dueDate\.value/);
assert.match(source, /batchResultItems/);
assert.match(source, /const maxAssignmentTargets = 100/);
assert.match(source, /teamMembers\.value\.slice\(0, maxAssignmentTargets\)/);
assert.match(source, /persistPendingBatchDraft/);
assert.match(source, /restorePendingBatchDraft/);
assert.match(source, /persistedDraft\?\.fingerprint === fingerprint/);
assert.match(source, /:aria-pressed="isMemberSelected\(member\.extPartyId\)"/);
assert.match(practiceService, /\/api\/aihr\/mobile\/practice\/assignments\/batch/);
assert.doesNotMatch(source, /placeholder="输入手机号或外部ID"/);
});
test('问师傅支持现场媒体、答案播报和总结卡图片下载', async () => {
const [source, composerSource, knowledgeService, speechService, imageService] = await Promise.all([
pageSource('../src/pages/user/sop/index.vue'),
pageSource('../src/components/chat/ChatComposer.vue'),
pageSource('../src/services/knowledge.ts'),
pageSource('../src/services/speech.ts'),
pageSource('../src/services/summary-card-image.ts')
]);
assert.match(source, /@pick-media="pickMedia"/);
assert.match(composerSource, /图片\/视频/);
assert.match(composerSource, /仅用于本次现场分析/);
assert.match(source, /searchKnowledgeWithMedia/);
assert.match(source, /toggleAnswerSpeech/);
assert.match(source, /toggleSummarySpeech/);
assert.match(source, /downloadSummaryCardImage/);
assert.match(knowledgeService, /\/api\/knowledge\/query-media/);
assert.match(speechService, /\/api\/ai\/tts/);
assert.match(speechService, /SpeechSynthesisUtterance/);
assert.match(speechService, /playWithBrowserSpeech/);
assert.match(imageService, /toBlob/);
assert.match(imageService, /link\.download/);
});
test('问师傅恢复旧版语音提问并在录音不可用时回退选择音频', async () => {
const [source, composerSource, speechService] = await Promise.all([
pageSource('../src/pages/user/sop/index.vue'),
pageSource('../src/components/chat/ChatComposer.vue'),
pageSource('../src/services/speech.ts')
]);
assert.match(composerSource, /按住 说话/);
assert.match(composerSource, /startHold/);
assert.match(composerSource, /endHold/);
assert.match(composerSource, /navigator\.mediaDevices\?\.getUserMedia/);
assert.match(composerSource, /emit\('pick-audio'\)/);
assert.match(composerSource, /const teardown = \(\) =>/);
assert.match(source, /pickVoiceAudio/);
assert.match(source, /transcribeSpeechBlob/);
assert.match(source, /transcribeSpeechFile/);
assert.match(speechService, /\/api\/ai\/asr/);
});
test('生成的每日题和对练对话恢复语音播报入口', async () => {
const source = await pageSource('../src/pages/user/practice/index.vue');
assert.match(source, /daily-question-voice/);
assert.match(source, /turn\.role === 'customer'/);
assert.match(source, /turn\.role === 'coach'/);
assert.match(source, /playTurnVoice/);
assert.match(source, /speakGenerated/);
assert.match(source, /createSpeechPlaybackController/);
assert.match(source, /停止播报/);
});
test('每日题点评参考答案和训练结果均可语音播报', async () => {
const source = await pageSource('../src/pages/user/practice/index.vue');
assert.match(source, /daily-feedback-card/);
assert.match(source, /item\.feedback/);
assert.match(source, /item\.referenceAnswer/);
assert.match(source, /dailyFeedbackSpeech/);
assert.match(source, /practiceResultSpeech/);
assert.match(source, /result-voice-button/);
});
test('语音、媒体和私密画像在切页或退出后不会继续写回', async () => {
const [sopSource, practiceSource, profileSource, knowledgeService] = await Promise.all([
pageSource('../src/pages/user/sop/index.vue'),
pageSource('../src/pages/user/practice/index.vue'),
pageSource('../src/pages/user/profile/index.vue'),
pageSource('../src/services/knowledge.ts')
]);
assert.match(sopSource, /onHide\(\(\) => \{[\s\S]*speechPlayback\.stop\(\);[\s\S]*composerRef\.value\?\.teardown\(\);[\s\S]*cancelVoiceTranscription\(\);[\s\S]*cancelPendingMediaSearch\(\);/);
assert.match(sopSource, /activeMediaUploadTask\?\.abort\(\)/);
assert.match(sopSource, /generation !== knowledgeSearchGeneration/);
assert.match(sopSource, /abortVoiceTranscription\?\.\(\)/);
assert.match(sopSource, /generation !== voiceTranscriptionGeneration/);
assert.match(practiceSource, /onHide\(\(\) => \{[\s\S]*speechPlayback\.stop\(\);[\s\S]*cancelRecording\(\);/);
assert.match(practiceSource, /recordingCaptureGeneration \+= 1/);
assert.match(practiceSource, /abortTranscription\?\.\(\)/);
assert.match(practiceSource, /generation !== transcriptionGeneration/);
assert.match(profileSource, /let profileLoadGeneration = 0/);
assert.match(profileSource, /getAuth\(\)\.phone !== accountPhone/);
assert.match(profileSource, /v-if="loggedIn && profile"/);
assert.match(profileSource, /v-if="loggedIn && evidence"/);
assert.match(knowledgeService, /onUploadTask\?:/);
assert.match(knowledgeService, /onUploadTask\?\.\(task\)/);
});
test('播报复用已合成音频且总结图按内容高度生成并及时释放', async () => {
const [speechService, imageService] = await Promise.all([
pageSource('../src/services/speech.ts'),
pageSource('../src/services/summary-card-image.ts')
]);
assert.match(speechService, /const sourceCache = new Map<string, string>\(\)/);
assert.match(speechService, /const maxCachedSources = 4/);
assert.match(speechService, /const sourceKey = cacheKey\(value, profile\)/);
assert.match(speechService, /const cachedSource = sourceCache\.get\(sourceKey\)/);
assert.match(speechService, /sourceCache\.clear\(\)/);
assert.match(speechService, /registerAbort\?\.\(\(\) => task\.abort\(\)\)/);
assert.match(speechService, /registerAbort\?\.\(\(\) => xhr\.abort\(\)\)/);
assert.match(speechService, /const isCurrent = \(\) => currentGeneration === generation && audio === context && activeKey === key/);
assert.match(imageService, /canvas\.height = 1/);
assert.doesNotMatch(imageService, /12000/);
assert.match(imageService, /canvas\.width = 1;[\s\S]*canvas\.height = 1/);
assert.match(imageService, /setTimeout\(\(\) => URL\.revokeObjectURL\(url\), 1000\)/);
});
test('主管学员模式只允许查询本人训练概况且登录后保留待执行工具', async () => {
const source = await pageSource('../src/pages/user/sop/index.vue');
assert.match(source, /const supervisorLearner = ref\(isSupervisorLearnerMode\(\)\)/);
assert.match(source, /supervisorLearner\.value && toolCode === 'TEAM_PRACTICE_SUMMARY'/);
assert.match(source, /supervisorLearner\.value && toolCode === 'TEAM_PRACTICE_SUMMARY'[\s\S]*\? 'MY_PRACTICE_SUMMARY'/);
assert.match(source, /uni\.setStorageSync\(pendingToolKey, toolCode\)/);
assert.match(source, /const generation = \+\+knowledgeSearchGeneration/);
assert.match(source, /if \(!loggedIn\.value\) \{[\s\S]*goLogin\(\);[\s\S]*return;[\s\S]*\}[\s\S]*uni\.removeStorageSync\(pendingToolKey\)/);
});