fix(aihr): harden broadcast and mobile assistant flows

This commit is contained in:
2026-07-23 03:35:36 +08:00
parent 62c8b41672
commit 4b31848e3d
30 changed files with 1127 additions and 208 deletions
+5 -3
View File
@@ -20,7 +20,8 @@ test('问师傅支持显式新对话、版本回写和受控原文件或视频
]);
assert.match(page, /@new-conversation="startNewConversation"/);
assert.match(header, /@click="emit\('newConversation'\)">新对话/);
assert.match(header, /@click="emit\('newConversation'\)"/);
assert.match(header, /class="new-chat-text">新对话/);
assert.match(page, /const contextVersion = ref\(0\)/);
assert.match(page, /applyConversationState\(nextResult\)/);
assert.match(page, /authenticatedFileUrl\(resource\.contentUrl\)/);
@@ -37,7 +38,8 @@ test('工作助手按项目名称选择并为文字、媒体和工具请求携
read('../src/services/auth.ts')
]);
assert.match(page, />当前项目</);
assert.match(page, /class="compact-project-strip"/);
assert.match(page, /projectContextLabel/);
assert.match(page, /选择当前项目/);
assert.match(page, /搜索项目名称/);
assert.match(page, /saveCurrentProject\(project\)/);
@@ -64,7 +66,7 @@ test('问师傅在真正开始会话后收起首次使用引导,保留对话
const page = await read('../src/pages/user/sop/index.vue');
assert.match(page, /const conversationStarted = computed\(\(\) => messages\.value\.some/);
assert.match(page, /v-if="loggedIn && !conversationStarted" class="project-context-bar"/);
assert.match(page, /v-if="loggedIn && !conversationStarted" class="compact-project-strip"/);
assert.match(page, /v-if="!conversationStarted" class="bubble-row left"/);
assert.match(page, /:quick-questions="conversationStarted \? \[\] : quickQuestions"/);
});
+1 -1
View File
@@ -17,7 +17,7 @@ test('银城大喇叭仅使用认证后的真实广播接口', async () => {
test('首页喇叭只在确认有未读消息时显示红点,未登录保留回跳', async () => {
const page = await read('../src/pages/user/today/index.vue');
assert.match(page, /v-if="hasUnreadBroadcast" class="bell-dot"/);
assert.match(page, /v-if="hasUnreadBroadcast" class="bell-dot(?:\s+[^\"]*)?"/);
assert.match(page, /getBroadcastUnreadCount/);
assert.match(page, /const unreadBroadcastCount = ref<number \| null>\(null\)/);
assert.match(page, /const hasUnreadBroadcast = computed\(\(\) => typeof unreadBroadcastCount\.value === 'number'/);
@@ -52,7 +52,8 @@ test('员工考试与主管组卷路由、题目播报、真实人员多选和
assert.match(employee, /播报题目/);
assert.match(employee, /submitMyExam/);
assert.match(edit, /getPracticeTeamSnapshot/);
assert.match(edit, /最多100人/);
assert.match(edit, /selectedMemberIds\.value\.length < 100/);
assert.match(edit, /members\.value\.slice\(0, 100\)/);
assert.match(edit, /requestId/);
assert.match(service, /\/api\/aihr\/mobile\/exams\/supervisor/);
});
+49 -36
View File
@@ -25,9 +25,11 @@ test('SOP 查询框不再覆盖 textarea 的 box-sizing', async () => {
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, /class="voice-card employee-card"/);
assert.match(source, /\{\{ voiceTitle \}\}/);
assert.match(source, /class="tool-cell tool-questions"/);
assert.match(source, /<text class="tool-name">问题榜<\/text>/);
assert.match(source, /const ask = \(query: string\) =>/);
assert.match(source, /const goPractice = \(\) =>/);
assert.match(source, /const openQuestions = \(\) =>/);
});
@@ -47,9 +49,10 @@ test('训练结果回到顶部,SOP 答案生成后定位到回答区', async (
pageSource('../src/services/navigation.ts')
]);
assert.match(practiceSource, /result\.value = await finishPractice\([\s\S]*?resetPageScroll\(\)/);
assert.match(sopSource, /import \{ resetPageScroll, scrollPageToSelector \} from '@\/services\/navigation';/);
assert.match(sopSource, /result\.value = await searchKnowledge\(value\);\s*scrollPageToSelector\('\.answer-panel'/);
assert.match(practiceSource, /const nextResult = await finishPractice\([\s\S]*?result\.value = nextResult;\s*resetPageScroll\(\)/);
assert.match(sopSource, /const scrollToBottom = \(\) =>/);
assert.match(sopSource, /replaceMsg\(pending\.id/);
assert.match(sopSource, /threadScrollTop\.value = 100000/);
assert.match(navigationSource, /export const scrollPageToSelector = \(selector: string/);
});
@@ -65,9 +68,9 @@ test('训练历史按摘要、逐句复盘、SOP 依据和行动建议分层', a
test('案例页为无权限和无案例提供明确空状态', async () => {
const source = await pageSource('../src/pages/user/cases/index.vue');
assert.match(source, /class="case-empty-state"/);
assert.match(source, /暂时还没有可学习案例/);
assert.match(source, /提交权限由组织角色决定/);
assert.match(source, /<StateBlock v-else type="empty" :card="false" title="暂无入库案例"/);
assert.match(source, /当前岗位可查看本项目已入库案例/);
assert.match(source, /提交案例素材需要主管或项目负责人权限/);
});
test('manifest 明确声明为 uni-app Vue3 CLI 工程', async () => {
@@ -89,7 +92,7 @@ test('员工端 tabBar 继续保留四个固定入口', async () => {
test('登录页按高保真原型保留品牌结构和紧凑验证码行', async () => {
const source = await pageSource('../src/pages/auth/login/index.vue');
assert.match(source, /高效协同 · 贴心服务 · 让工作更简单/);
assert.match(source, /手机号登录,立刻上手/);
assert.match(source, /<uni-icons[^>]+type="phone"/);
assert.match(source, /<uni-icons[^>]+type="locked"/);
assert.doesNotMatch(source, /class="logo-badge"/);
@@ -133,9 +136,9 @@ test('工作助手与查全网使用一致的来源切换头部', async () => {
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, /<text class="brand-name">数字师傅<\/text>/);
assert.match(headerSource, /\.source-option\s*\{[^}]*height:\s*28px/);
assert.match(headerSource, /\.new-chat-btn\s*\{[^}]*height:\s*28px/);
assert.match(headerSource, /@click="emit\('newConversation'\)"/);
assert.doesNotMatch(sopSource, /(?:^|\n)\s*\.source-switch\s*\{/);
assert.doesNotMatch(sopSource, /(?:^|\n)\s*\.source-option(?:\.active)?\s*\{/);
@@ -179,12 +182,12 @@ test('主管可切换到学员显示模式且组织角色保持不变', async ()
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, /hero-immersive/);
assert.match(source, /voice-card/);
assert.match(source, /今日安排/);
assert.match(source, /问题榜/);
assert.match(source, /brand-bar/);
assert.match(source, /ask-card/);
assert.match(source, /mentor-chips/);
assert.match(source, /小贴士/);
});
@@ -192,24 +195,24 @@ 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, /type="chatboxes-filled"/);
assert.match(source, /type="paperplane-filled"/);
assert.match(source, /type="staff-filled"/);
assert.match(source, /const ask = \(query: string\) =>/);
assert.match(source, /const openPrimarySchedule = \(\) =>/);
assert.match(source, /getMobileHome\('user'\)/);
assert.match(source, /getPracticeAssignments\(true\)/);
assert.match(source, /src="\/static\/mentor-hifi\.png"/);
assert.match(source, /src="\/static\/mentor-female\.png"/);
});
test('员工首页今日安排保留派发任务来源', async () => {
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /primaryAssignmentSourceLabel/);
assert.match(source, /primaryAssignmentTagText/);
assert.match(source, /source === 'retry'[\s\S]*错题再练/);
assert.match(source, /source === 'camp'[\s\S]*训练营/);
assert.match(source, /class="schedule-source"/);
assert.match(source, /今天先完成每日三题/);
assert.match(source, /class="schedule-tag"/);
assert.match(source, /提交后显示反馈与参考答案/);
});
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
@@ -231,8 +234,14 @@ test('SOP 没解决先收集结构化原因再提交', async () => {
assert.match(source, /feedbackReasonOptions/);
assert.match(source, /negative-feedback-panel/);
assert.match(source, /submitNegativeFeedback/);
assert.match(source, /reasonCodes:\s*selectedFeedbackReasons\.value/);
assert.match(source, /comment:\s*feedbackComment\.value\.trim\(\)/);
for (const code of ['NO_ANSWER', 'NOT_SPECIFIC', 'WRONG_REFERENCE', 'NOT_APPLICABLE']) {
assert.match(source, new RegExp(`code: '${code}'`));
}
assert.doesNotMatch(source, /MISSING_POLICY|ANSWER_INCORRECT/);
assert.match(source, /:disabled="msg\.feedbackSubmitting \|\| !msg\.feedbackReasonCodes\.length"/);
assert.match(source, /if \(!msg\.feedbackReasonCodes\.length\) \{[\s\S]*请至少选择一项未解决原因/);
assert.match(source, /reasonCodes: verdict === 'down' \? msg\.feedbackReasonCodes/);
assert.match(source, /comment: verdict === 'down' \? msg\.feedbackComment\.trim\(\)/);
assert.match(service, /reasonCodes\?:\s*string\[\]/);
assert.match(service, /comment\?:\s*string/);
});
@@ -250,8 +259,9 @@ test('训练页保留任务卡、对话区、回复区和结果区并互斥切
assert.match(source, /practiceView === 'prep'/);
assert.match(source, /practiceView === 'active'/);
assert.match(source, /practiceView\.value === 'result'/);
assert.match(source, /result\.score\?\.length/);
assert.match(source, /class="result-dimensions"/);
assert.match(source, /result\.totalScore/);
assert.match(source, /result\.aiComment/);
assert.match(source, /result\.mentorRewrite/);
});
test('底部固定导航页面复用安全操作区', async () => {
@@ -283,7 +293,8 @@ test('员工问师傅页显示当前岗位而不是固定生活顾问', async ()
assert.match(source, /getSelectedPosition/);
assert.match(source, /const positionLabel = ref\(getSelectedPosition\(\)\)/);
assert.match(source, /:context="`\$\{positionLabel\} · 住宅项目`"/);
assert.match(source, /const headerContext = computed\(\(\) =>/);
assert.match(source, /\$\{positionLabel\.value\} · \$\{currentProject\.value\?\.projectName \|\| '住宅项目'\}/);
assert.doesNotMatch(source, /sop-context">管家(生活顾问) · 住宅项目/);
});
@@ -331,14 +342,15 @@ test('学习页显示岗位 SOP 适配状态而不冒充上岗资格', async ()
]);
assert.match(source, /positionSopSummary/);
assert.match(source, /正式上岗资格/);
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, /AI训练综合分仅作参考/);
assert.match(source, /参考等级/);
assert.match(source, /人工贡献分/);
});
@@ -368,7 +380,7 @@ test('员工岗位只能来自人力系统且个人页不再允许手动切换',
pageSource('../src/services/auth.ts')
]);
assert.match(profileSource, /岗位来自人力系统组织同步/);
assert.match(profileSource, /岗位由人力系统组织同步/);
assert.doesNotMatch(profileSource, /changePosition/);
assert.match(positionSource, /员工端不支持手动切换岗位/);
assert.doesNotMatch(positionSource, /saveSelectedPosition|selectPosition/);
@@ -388,12 +400,13 @@ test('主管专项从正式团队名单按姓名多选并一次批量派发', as
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, /3天后/);
assert.match(source, /7天后/);
assert.match(source, /dueDate: dueDate\.value/);
assert.match(source, /batchResultItems/);
assert.match(source, /const resultItems: PracticeAssignmentBatchItemResult\[\] = result\.results \|\| \[\]/);
assert.match(source, /assignResultStorageKey/);
assert.match(source, /uni\.redirectTo\(\{ url: '\/pages\/supervisor\/assign\/result' \}\)/);
assert.match(source, /const maxAssignmentTargets = 100/);
assert.match(source, /teamMembers\.value\.slice\(0, maxAssignmentTargets\)/);
assert.match(source, /persistPendingBatchDraft/);
+2 -2
View File
@@ -62,8 +62,8 @@ test('工作助手页面展示任务过期后不会消费待执行查询或数
const page = await read('../src/pages/user/sop/index.vue');
assert.match(page, /const isCurrentShow = \(\) => generation === showGeneration && accountKey === currentAccountKey\(\)/);
assert.match(page, /if \(!isCurrentShow\(\)\) return;\s*const pending = uni\.getStorageSync\(pendingSopQueryKey\)/);
assert.match(page, /if \(!isCurrentShow\(\)\) return;\s*const pendingTool = uni\.getStorageSync\(pendingToolKey\)/);
assert.match(page, /const pending = uni\.getStorageSync\(pendingSopQueryKey\);\s*if \(!isCurrentShow\(\)\) return;/);
assert.match(page, /const pendingTool = uni\.getStorageSync\(pendingToolKey\);[\s\S]*?if \(!isCurrentShow\(\)\) return;/);
assert.match(page, /if \(!isCurrentShow\(\)\) return;\s*uni\.removeStorageSync\(pendingToolKey\)/);
assert.match(page, /if \(!isCurrentShow\(\)\) return;\s*uni\.removeStorageSync\(pendingSopQueryKey\)/);
assert.match(page, /const requestShowGeneration = showGeneration/);
+120 -44
View File
@@ -20,6 +20,7 @@ const supervisorView = await vite.ssrLoadModule('/src/services/supervisor-view.t
const {
actionablePracticeAlerts,
buildSupervisorMetrics,
buildTeamProfileRollup,
buildTeamProfiles,
buildTrainingTrend,
createAsyncGenerationGuard,
@@ -167,8 +168,11 @@ test('待复盘列表支持全部、低分和待处理筛选', () => {
assert.equal(filterReviewRecords(rows, 'all').length, 3);
assert.deepEqual(filterReviewRecords(rows, 'low').map((item) => item.id), [1]);
assert.deepEqual(filterReviewRecords(rows, 'pending').map((item) => item.id), [1, 2]);
assert.match(reviewsPageSource, /reviewFilters/);
assert.match(reviewsPageSource, /visibleReviews/);
assert.match(reviewsPageSource, /filterTabs/);
assert.match(reviewsPageSource, /activeFilter/);
assert.match(reviewsPageSource, /displayedReviews/);
assert.match(reviewsPageSource, /below60/);
assert.match(reviewsPageSource, /repeatLow/);
});
test('主管页面共享三个业务场景及文本推荐规则', () => {
@@ -178,8 +182,8 @@ test('主管页面共享三个业务场景及文本推荐规则', () => {
'repair-elevator-delay'
]);
assert.match(practiceServiceSource, /getPracticeScenarios/);
assert.match(assignPageSource, /v-for="item in filteredScenarioOptions"/);
assert.match(assignPageSource, /scenarioSearch/);
assert.match(assignPageSource, /v-for="item in scenarioOptions"/);
assert.match(assignPageSource, /assignmentPosition/);
assert.match(assignPageSource, /暂无可派发的已启用场景/);
assert.match(reviewPageSource, /scenarioOptions/);
assert.match(reviewPageSource, /scenarioOptions\.value = \[\]/);
@@ -193,8 +197,9 @@ test('专项派发使用后端批量接口并保留逐人结果和截止日期',
assert.match(assignPageSource, /targets: selectedMembers\.map/);
assert.match(assignPageSource, /requestId: pendingRequestId\.value/);
assert.match(assignPageSource, /dueDate: dueDate\.value/);
assert.match(assignPageSource, /batchResultItems\.value = result\.results/);
assert.match(assignPageSource, /item\.status !== 'FAILED'/);
assert.match(assignPageSource, /const resultItems: PracticeAssignmentBatchItemResult\[\] = result\.results \|\| \[\]/);
assert.match(assignPageSource, /uni\.setStorageSync\(assignResultStorageKey/);
assert.match(assignPageSource, /uni\.redirectTo\(\{ url: '\/pages\/supervisor\/assign\/result' \}\)/);
assert.match(assignPageSource, /const maxAssignmentTargets = 100/);
assert.match(assignPageSource, /persistPendingBatchDraft/);
assert.match(assignPageSource, /restorePendingBatchDraft/);
@@ -288,13 +293,90 @@ test('团队画像搜索和分页不把 2316 名成员一次性渲染', () => {
assert.equal(paginateTeamProfiles(profiles, 1, 20).items.length, 20);
assert.equal(paginateTeamProfiles(profiles, 1, 20).pageCount, 116);
const afterLoadingMore = paginateTeamProfiles(profiles, 2, 20);
assert.equal(afterLoadingMore.items.length, 40);
assert.equal(afterLoadingMore.items[0].key, 'member-0');
assert.equal(afterLoadingMore.items.at(-1).key, 'member-39');
assert.deepEqual(
filterTeamProfiles(profiles, '员工 2315').map((item) => item.key),
['member-2315']
);
assert.match(teamPageSource, /v-for="member in visibleTeamProfiles"/);
assert.match(teamPageSource, /v-if="selectedMember" class="panel member-detail-panel"/);
assert.match(teamPageSource, /<template v-else>[\s\S]*class="panel team-panel"/);
assert.match(teamPageSource, /filterTeamProfiles\(teamProfiles\.value, teamSearch\.value\)/);
assert.match(teamPageSource, /paginateTeamProfiles\(filteredTeamProfiles\.value, teamPage\.value, teamPageSize\)/);
assert.match(teamPageSource, /class="load-more-members"/);
});
test('全员汇总覆盖截断明细,较早成员的待复盘低分不能被降为正常', () => {
const truncatedRecords = Array.from({ length: 200 }, (_, index) => review({
id: index + 1,
extPartyId: `recent-${index}`,
trainee: `近期员工${index}`,
score: 95,
status: '已复盘'
}));
const profiles = buildTeamProfiles(
truncatedRecords,
[],
[
{ extPartyId: 'recent-0', name: '近期员工0' },
{ extPartyId: 'older-risk', name: '较早成员' }
],
[{
extPartyId: 'older-risk',
latestScore: 62,
latestScene: '投诉接待',
lastPracticeAt: '07-01 09:00',
pendingReview: 1,
pendingLowScore: 62,
pendingAssignment: 1
}]
);
const olderRisk = profiles.find((item) => item.extPartyId === 'older-risk');
assert.equal(olderRisk?.risk, 'high');
assert.equal(olderRisk?.pendingReview, 1);
assert.equal(olderRisk?.pendingAssignment, 1);
assert.equal(olderRisk?.latestScore, 62);
assert.match(teamPageSource, /snapshot\.profiles/);
assert.match(dashboardPageSource, /snapshot\.profiles/);
});
test('主管首页从全员汇总计算今日完训和待训练,不受 200 条明细上限影响', () => {
const members = Array.from({ length: 201 }, (_, index) => ({
extPartyId: `member-${index}`,
name: `员工${index}`
}));
const profiles = buildTeamProfiles(
Array.from({ length: 200 }, (_, index) => review({
id: index + 1,
extPartyId: `member-${index}`,
trainee: `员工${index}`,
time: '07-22 10:00',
score: 92,
status: '已复盘'
})),
[],
members,
members.map((member, index) => ({
extPartyId: member.extPartyId,
latestScore: index === 200 ? 88 : 92,
latestScene: '投诉接待',
lastPracticeAt: '07-22 10:00',
completedToday: index === 200,
pendingReview: 0,
pendingLowScore: null,
pendingAssignment: index === 200 ? 1 : 0
}))
);
assert.deepEqual(buildTeamProfileRollup(profiles, '07-23'), {
teamTotal: 201,
pendingReview: 0,
pendingAssignment: 1,
todayDone: 1
});
assert.match(dashboardPageSource, /buildTeamProfileRollup/);
});
test('员工和主管个人页只展示脱敏手机号', async () => {
@@ -305,23 +387,22 @@ test('员工和主管个人页只展示脱敏手机号', async () => {
assert.equal(maskPhone('181368856126'), '181****6126');
assert.equal(maskPhone('invalid'), 'invalid');
assert.match(employeeProfile, /displayPhone/);
assert.match(supervisorProfile, /displayPhone/);
assert.match(employeeProfile, /maskedPhone/);
assert.match(supervisorProfile, /maskedPhone/);
assert.doesNotMatch(employeeProfile, /\{\{ phone \|\|/);
assert.doesNotMatch(supervisorProfile, /\{\{ phone \|\|/);
});
test('主管个人工作台展示真实指标和三项业务入口', async () => {
test('主管个人页展示当前账号范围、团队成果和学员模式入口', async () => {
const source = await readFile(new URL('../src/pages/supervisor/profile/index.vue', import.meta.url), 'utf8');
assert.match(source, /\{\{ loading \? '--' : teamCount \}\}/);
assert.match(source, /\{\{ loading \? '--' : pendingReviewCount \}\}/);
assert.match(source, /\{\{ loading \? '--' : alertCount \}\}/);
assert.match(source, /主管业务入口/);
assert.match(source, /\/pages\/supervisor\/team\/index/);
assert.match(source, /\/pages\/supervisor\/reviews\/index/);
assert.match(source, /\/pages\/supervisor\/assign\/index/);
assert.match(source, /getPracticeTeamSnapshot/);
assert.match(source, /maskedPhone/);
assert.match(source, /主管端团队数据仅对当前项目范围可见/);
assert.match(source, /团队工作成果/);
assert.match(source, /\/pages\/supervisor\/work-results\/index/);
assert.match(source, /进入学员模式/);
assert.match(source, /enterSupervisorLearnerMode/);
assert.match(source, /<SupervisorTabBar active="profile"/);
});
test('组织快照姓名不可读时用稳定身份标识展示', () => {
@@ -376,15 +457,15 @@ test('复盘详情只允许待复盘提交并保留未保存草稿', () => {
assert.match(reviewPageSource, /v-else class="panel review-complete"/);
assert.match(reviewPageSource, /if \(!editable\.value\)[\s\S]*该记录已完成复盘/);
assert.match(reviewPageSource, /onShow\(\(\) => \{[\s\S]*refreshAuthState\(\);[\s\S]*if \(!loggedIn\.value\) \{[\s\S]*accessDenied\.value = false;[\s\S]*clearDetail\(\);\s*return;\s*\}[\s\S]*if \(!detail\.value && !loading\.value\)[\s\S]*loadDetail\(\)/);
assert.match(reviewPageSource, /训练摘要/);
assert.match(reviewPageSource, /录音与逐句复盘/);
assert.match(reviewPageSource, /SOP 依据与改进建议/);
assert.match(reviewPageSource, /下一步行动/);
assert.match(reviewPageSource, /AI 摘要/);
assert.match(reviewPageSource, /对话与录音/);
assert.match(reviewPageSource, /标注与建议/);
assert.match(reviewPageSource, /复盘后建议补练/);
});
test('复盘详情登录和完成导航不堆叠页面', () => {
assert.match(reviewPageSource, /rememberLoginRedirect\(currentPath\(\)\);\s*uni\.redirectTo/);
assert.match(reviewPageSource, /reviewCompletionAction\(previousRoute\)/);
assert.match(reviewPageSource, /const goBack = \(\) => \{[\s\S]*getCurrentPages\(\)\.length <= 1/);
assert.match(reviewPageSource, /uni\.navigateBack\(\{ delta: 1, fail: goToReviews \}\)/);
assert.match(reviewPageSource, /const goToReviews = \(\) => \{\s*uni\.redirectTo/);
});
@@ -428,7 +509,7 @@ test('五个主管页面优先显示统一无权限状态并清理私密数据',
pages.forEach(({ source, clear }) => {
assert.match(source, /const accessDenied = ref\(false\)/);
assert.match(source, /<view v-if="accessDenied" class="panel supervisor-access-panel">[\s\S]*无主管权限[\s\S]*当前账号不是主管或项目经理,无法查看团队训练数据。[\s\S]*@click="goToEmployee"[\s\S]*返回员工端[\s\S]*@click="switchAccount"[\s\S]*切换账号[\s\S]*<\/view>\s*<view v-else-if="!loggedIn"/);
assert.match(source, /<StateBlock\s+v-if="accessDenied"[\s\S]*type="access"[\s\S]*action-text="切换账号"[\s\S]*secondary-action-text="返回员工端"[\s\S]*@action="switchAccount"[\s\S]*@secondary-action="goToEmployee"/);
assert.match(source, new RegExp(`if \\(isSupervisorAccessDenied\\(error\\)\\) \\{[\\s\\S]*${clear}\\(\\);[\\s\\S]*accessDenied\\.value = true;[\\s\\S]*return;`));
assert.match(source, /if \(!loggedIn\.value\) \{[\s\S]*accessDenied\.value = false;/);
assert.match(source, /accessDenied\.value = false;[\s\S]*(?:snapshot|nextReviews|detail\.value|nextAssignments)/);
@@ -444,18 +525,11 @@ test('指派页通过团队快照在进入时校验主管权限', () => {
assert.doesNotMatch(assignPageSource, /getPracticeAssignments/);
});
test('指派表单和派发进度使用同页分段视图避免堆成长页', () => {
assert.match(assignPageSource, /assignView/);
assert.match(assignPageSource, /v-if="assignView === 'create'" class="panel assignment-form"/);
assert.match(assignPageSource, /v-if="assignView === 'progress'" class="panel history-panel"/);
assert.match(assignPageSource, /class="assign-tabs"/);
assert.match(assignPageSource, /memberSuggestions/);
assert.match(assignPageSource, /selectTarget/);
assert.match(assignPageSource, /class="target-suggestions"/);
assert.match(assignPageSource, /assignmentDueTime/);
assert.match(assignPageSource, /assignmentGoal/);
assert.match(assignPageSource, /assignmentRequirements/);
assert.match(assignPageSource, /item\.dueTime/);
test('指派页将逐人派发结果交由独立结果页呈现,避免在表单页堆叠', () => {
assert.match(assignPageSource, /class="panel form-panel"/);
assert.match(assignPageSource, /class="panel progress-panel"/);
assert.match(assignPageSource, /assignResultStorageKey/);
assert.match(assignPageSource, /\/pages\/supervisor\/assign\/result/);
});
test('指派页保留 H5 查询参数二次解码', () => {
@@ -463,14 +537,16 @@ test('指派页保留 H5 查询参数二次解码', () => {
});
test('主管 dashboard 和 team 使用团队快照,reviews 继续使用待复盘接口', () => {
assert.match(practiceServiceSource, /export const getPracticeTeamSnapshot = \(\) =>/);
assert.match(practiceServiceSource, /url: '\/api\/aihr\/mobile\/practice\/team'/);
assert.match(dashboardPageSource, /getPracticeTeamSnapshot/);
assert.match(dashboardPageSource, /buildTeamProfiles\(records\.value, assignments\.value, members\.value\)/);
assert.match(teamPageSource, /getPracticeTeamSnapshot/);
assert.match(teamPageSource, /buildTeamProfiles\(records\.value, assignments\.value, members\.value\)/);
assert.match(practiceServiceSource, /export const getPracticeTeamSnapshot = \(includeProfiles = false\) =>/);
assert.match(practiceServiceSource, /'\/api\/aihr\/mobile\/practice\/team\?includeProfiles=true'/);
assert.match(practiceServiceSource, /: '\/api\/aihr\/mobile\/practice\/team'/);
assert.match(practiceServiceSource, /includeProfiles=true/);
assert.match(dashboardPageSource, /getPracticeTeamSnapshot\(true\)/);
assert.match(dashboardPageSource, /buildTeamProfiles\(records\.value, assignments\.value, members\.value, teamProfileSummaries\.value\)/);
assert.match(teamPageSource, /getPracticeTeamSnapshot\(true\)/);
assert.match(teamPageSource, /buildTeamProfiles\(records\.value, assignments\.value, members\.value, teamProfileSummaries\.value\)/);
assert.match(reviewsPageSource, /getPracticeReviews/);
assert.doesNotMatch(reviewsPageSource, /getPracticeTeamSnapshot/);
assert.match(reviewsPageSource, /getPracticeTeamSnapshot\(\)\.catch/);
});
test('五个主管页面登录都使用 redirectTo 避免返回栈重复', () => {
+6 -6
View File
@@ -7,10 +7,10 @@ const source = (path) => readFile(new URL(path, import.meta.url), 'utf8');
test('工作上报首屏是语音优先会话,不再暴露传统表单主路径', async () => {
const page = await source('../src/pages/user/report/index.vue');
assert.match(page, /工作上报/);
assert.match(page, /数字师傅帮你整理/);
assert.match(page, /我的上报/);
assert.match(page, /今天处理了什么?按住说,也可以发照片或视频。/);
assert.match(page, /成果投稿/);
assert.match(page, /把好经验沉淀给公司复用/);
assert.match(page, /我的投稿/);
assert.match(page, /哪段处置经验值得公司复用?按住说,也可以发照片或视频,我会整理成案例、SOP 或知识投稿。/);
assert.match(page, /<ChatComposer/);
assert.doesNotMatch(page, /选择上报类型/);
assert.doesNotMatch(page, /class="type-grid"/);
@@ -71,7 +71,7 @@ test('确认卡允许修改最终类型标题正文且不承诺自动脱敏', as
assert.doesNotMatch(page, /privacyReviewed/);
assert.match(page, /editingDraft \? '完成修改' : '修改'/);
assert.match(page, /@click="confirmSubmit"/);
assert.match(page, /'确认上报'/);
assert.match(page, /'确认投稿'/);
assert.doesNotMatch(page, /自动隐藏/);
assert.doesNotMatch(page, />继续补充<\/button>/);
});
@@ -122,7 +122,7 @@ test('AI 长草稿不被前端静默截断,改由确认时明确校验', async
assert.doesNotMatch(page, /String\(result\.content[\s\S]*?\.slice\(0, 2000\)/);
assert.match(page, /content\.length > 2000/);
assert.match(page, /上报内容不能超过2000字/);
assert.match(page, /投稿内容不能超过2000字/);
});
test('工作上报服务声明整理契约并在正式提交携带 requestId', async () => {