fix(aihr): harden broadcast and mobile assistant flows
This commit is contained in:
@@ -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 避免返回栈重复', () => {
|
||||
|
||||
Reference in New Issue
Block a user