测试考勤:覆盖参训状态签到守卫与真实前端策略
This commit is contained in:
1 parent
e2c15e6216
commit
08b8abe778
2 files changed
+344
-151
No files matched your search
+227
-2
@@ -196,16 +196,18 @@ public class AttendanceActionContractTest
|
||||
lesson.setPlanCourseId(10L);
|
||||
lessons.put(1001L, lesson);
|
||||
|
||||
// 学员 E_A 属于 PLAN_A
|
||||
// 学员 E_A 属于 PLAN_A (1:已报到在训)
|
||||
TrainEnrollment ea = new TrainEnrollment();
|
||||
ea.setEnrollmentId(201L);
|
||||
ea.setPlanId("PLAN_A");
|
||||
ea.setEnrollmentStatus(1);
|
||||
enrollments.put(201L, ea);
|
||||
|
||||
// 学员 E_B 属于 PLAN_B (跨班)
|
||||
// 学员 E_B 属于 PLAN_B (跨班, 1:已报到在训)
|
||||
TrainEnrollment eb = new TrainEnrollment();
|
||||
eb.setEnrollmentId(202L);
|
||||
eb.setPlanId("PLAN_B");
|
||||
eb.setEnrollmentStatus(1);
|
||||
enrollments.put(202L, eb);
|
||||
}
|
||||
|
||||
@@ -594,4 +596,227 @@ public class AttendanceActionContractTest
|
||||
assertEquals(AttendanceErrorCode.LESSON_NOT_FOUND, (int) e.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignInLifecycle_Status0_WaitingReport_Rejected()
|
||||
{
|
||||
// status=0 待报到学员:严禁直接签到绕过报到生命周期
|
||||
TrainEnrollment e0 = new TrainEnrollment();
|
||||
e0.setEnrollmentId(300L);
|
||||
e0.setPlanId("PLAN_A");
|
||||
e0.setEnrollmentStatus(0); // 0: 待报到
|
||||
enrollments.put(300L, e0);
|
||||
|
||||
AttendanceSignInDTO dto = new AttendanceSignInDTO();
|
||||
dto.setEnrollmentId(300L);
|
||||
dto.setLessonId(1001L);
|
||||
|
||||
try
|
||||
{
|
||||
appService.signIn(dto, "admin");
|
||||
fail("待报到学员必须被拦截禁止签到");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_REQUIRES_REPORTED_ENROLLMENT, (int) e.getCode());
|
||||
assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_REQUIRES_REPORTED_ENROLLMENT_MESSAGE, e.getMessage());
|
||||
}
|
||||
// 保证无任何写产生
|
||||
assertNull(checkinsByEnrollLesson.get("300_1001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignInLifecycle_Status1_Reported_Allowed()
|
||||
{
|
||||
// status=1 已报到学员:允许签到
|
||||
TrainEnrollment e1 = new TrainEnrollment();
|
||||
e1.setEnrollmentId(301L);
|
||||
e1.setPlanId("PLAN_A");
|
||||
e1.setEnrollmentStatus(1); // 1: 已报到
|
||||
enrollments.put(301L, e1);
|
||||
|
||||
AttendanceSignInDTO dto = new AttendanceSignInDTO();
|
||||
dto.setEnrollmentId(301L);
|
||||
dto.setLessonId(1001L);
|
||||
|
||||
TrainStudentCheckin c = appService.signIn(dto, "admin");
|
||||
assertNotNull(c);
|
||||
assertNotNull(c.getSignInTime());
|
||||
assertEquals(Integer.valueOf(1), c.getAttendanceStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignInLifecycle_Status2_Leave_Allowed_PreviewDefault()
|
||||
{
|
||||
// status=2 请假学员:Preview Default 允许签到 (LEAVE_SIGNIN_POLICY: PREVIEW_DEFAULT_ALLOWED)
|
||||
TrainEnrollment e2 = new TrainEnrollment();
|
||||
e2.setEnrollmentId(302L);
|
||||
e2.setPlanId("PLAN_A");
|
||||
e2.setEnrollmentStatus(2); // 2: 请假
|
||||
enrollments.put(302L, e2);
|
||||
|
||||
AttendanceSignInDTO dto = new AttendanceSignInDTO();
|
||||
dto.setEnrollmentId(302L);
|
||||
dto.setLessonId(1001L);
|
||||
|
||||
TrainStudentCheckin c = appService.signIn(dto, "admin");
|
||||
assertNotNull(c);
|
||||
assertNotNull(c.getSignInTime());
|
||||
assertEquals(Integer.valueOf(1), c.getAttendanceStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignInLifecycle_Status3_Abandoned_Rejected()
|
||||
{
|
||||
// status=3 放弃学员:严禁直接签到
|
||||
TrainEnrollment e3 = new TrainEnrollment();
|
||||
e3.setEnrollmentId(303L);
|
||||
e3.setPlanId("PLAN_A");
|
||||
e3.setEnrollmentStatus(3); // 3: 放弃
|
||||
enrollments.put(303L, e3);
|
||||
|
||||
AttendanceSignInDTO dto = new AttendanceSignInDTO();
|
||||
dto.setEnrollmentId(303L);
|
||||
dto.setLessonId(1001L);
|
||||
|
||||
try
|
||||
{
|
||||
appService.signIn(dto, "admin");
|
||||
fail("已放弃学员必须被拦截禁止签到");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_ABANDONED, (int) e.getCode());
|
||||
assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_ABANDONED_MESSAGE, e.getMessage());
|
||||
}
|
||||
assertNull(checkinsByEnrollLesson.get("303_1001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignInLifecycle_Status4_Completed_Rejected()
|
||||
{
|
||||
// status=4 已完成学员:严禁直接签到
|
||||
TrainEnrollment e4 = new TrainEnrollment();
|
||||
e4.setEnrollmentId(304L);
|
||||
e4.setPlanId("PLAN_A");
|
||||
e4.setEnrollmentStatus(4); // 4: 已完成
|
||||
enrollments.put(304L, e4);
|
||||
|
||||
AttendanceSignInDTO dto = new AttendanceSignInDTO();
|
||||
dto.setEnrollmentId(304L);
|
||||
dto.setLessonId(1001L);
|
||||
|
||||
try
|
||||
{
|
||||
appService.signIn(dto, "admin");
|
||||
fail("已结训学员必须被拦截禁止签到");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_COMPLETED, (int) e.getCode());
|
||||
assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_COMPLETED_MESSAGE, e.getMessage());
|
||||
}
|
||||
assertNull(checkinsByEnrollLesson.get("304_1001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignInLifecycle_UnknownOrNullStatus_Rejected()
|
||||
{
|
||||
// 未知或 null 参训状态:fail closed 严禁签到
|
||||
TrainEnrollment eNull = new TrainEnrollment();
|
||||
eNull.setEnrollmentId(305L);
|
||||
eNull.setPlanId("PLAN_A");
|
||||
eNull.setEnrollmentStatus(null);
|
||||
enrollments.put(305L, eNull);
|
||||
|
||||
AttendanceSignInDTO dto = new AttendanceSignInDTO();
|
||||
dto.setEnrollmentId(305L);
|
||||
dto.setLessonId(1001L);
|
||||
|
||||
try
|
||||
{
|
||||
appService.signIn(dto, "admin");
|
||||
fail("null 状态学员必须被 fail closed 拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID, (int) e.getCode());
|
||||
}
|
||||
|
||||
// 状态 99
|
||||
eNull.setEnrollmentStatus(99);
|
||||
try
|
||||
{
|
||||
appService.signIn(dto, "admin");
|
||||
fail("非法状态 99 学员必须被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID, (int) e.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignInLifecycle_Status4_WithExistingCheckin_IdempotentRetainsFact()
|
||||
{
|
||||
// 场景:历史已经合法签到的学员,后来结训归档变为 status=4。重复调用 signIn 保持幂等返回已有事实,不产生新写
|
||||
TrainEnrollment e4 = new TrainEnrollment();
|
||||
e4.setEnrollmentId(306L);
|
||||
e4.setPlanId("PLAN_A");
|
||||
e4.setEnrollmentStatus(4); // 4: 已完成
|
||||
enrollments.put(306L, e4);
|
||||
|
||||
TrainStudentCheckin historical = new TrainStudentCheckin();
|
||||
historical.setCheckinId(6001L);
|
||||
historical.setEnrollmentId(306L);
|
||||
historical.setLessonId(1001L);
|
||||
Date historicalTime = new Date(System.currentTimeMillis() - 86400000L);
|
||||
historical.setSignInTime(historicalTime);
|
||||
historical.setAttendanceStatus(1);
|
||||
checkinsById.put(6001L, historical);
|
||||
checkinsByEnrollLesson.put("306_1001", historical);
|
||||
|
||||
AttendanceSignInDTO dto = new AttendanceSignInDTO();
|
||||
dto.setEnrollmentId(306L);
|
||||
dto.setLessonId(1001L);
|
||||
|
||||
TrainStudentCheckin returned = appService.signIn(dto, "admin");
|
||||
assertNotNull(returned);
|
||||
assertEquals(Long.valueOf(6001L), returned.getCheckinId());
|
||||
assertEquals(historicalTime, returned.getSignInTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRosterLifecycleEligibility_Status0And4_CannotSignIn_Status1And2_CanSignIn()
|
||||
{
|
||||
// 测试名册对于 status 0, 1, 2, 4 的 canSignIn 权限对齐:
|
||||
// 310 (status=0 待报到), 311 (status=1 已报到), 312 (status=2 请假), 314 (status=4 已完成)
|
||||
enrollments.clear();
|
||||
checkinsById.clear();
|
||||
checkinsByEnrollLesson.clear();
|
||||
|
||||
TrainEnrollment e0 = new TrainEnrollment(); e0.setEnrollmentId(310L); e0.setPlanId("PLAN_A"); e0.setEnrollmentStatus(0); enrollments.put(310L, e0);
|
||||
TrainEnrollment e1 = new TrainEnrollment(); e1.setEnrollmentId(311L); e1.setPlanId("PLAN_A"); e1.setEnrollmentStatus(1); enrollments.put(311L, e1);
|
||||
TrainEnrollment e2 = new TrainEnrollment(); e2.setEnrollmentId(312L); e2.setPlanId("PLAN_A"); e2.setEnrollmentStatus(2); enrollments.put(312L, e2);
|
||||
TrainEnrollment e4 = new TrainEnrollment(); e4.setEnrollmentId(314L); e4.setPlanId("PLAN_A"); e4.setEnrollmentStatus(4); enrollments.put(314L, e4);
|
||||
|
||||
List<com.ruoyi.train.domain.vo.AttendanceLessonRosterVO> roster = appService.getLessonRoster(1001L);
|
||||
assertEquals(4, roster.size());
|
||||
|
||||
com.ruoyi.train.domain.vo.AttendanceLessonRosterVO r0 = roster.stream().filter(r -> r.getEnrollmentId().equals(310L)).findFirst().orElse(null);
|
||||
assertNotNull(r0);
|
||||
assertFalse("status=0 待报到学员名册 canSignIn 必须为 false", Boolean.TRUE.equals(r0.getCanSignIn()));
|
||||
|
||||
com.ruoyi.train.domain.vo.AttendanceLessonRosterVO r1 = roster.stream().filter(r -> r.getEnrollmentId().equals(311L)).findFirst().orElse(null);
|
||||
assertNotNull(r1);
|
||||
assertTrue("status=1 已报到学员名册 canSignIn 必须为 true", Boolean.TRUE.equals(r1.getCanSignIn()));
|
||||
|
||||
com.ruoyi.train.domain.vo.AttendanceLessonRosterVO r2 = roster.stream().filter(r -> r.getEnrollmentId().equals(312L)).findFirst().orElse(null);
|
||||
assertNotNull(r2);
|
||||
assertTrue("status=2 请假学员名册 canSignIn 必须为 true (Preview Default)", Boolean.TRUE.equals(r2.getCanSignIn()));
|
||||
|
||||
com.ruoyi.train.domain.vo.AttendanceLessonRosterVO r4 = roster.stream().filter(r -> r.getEnrollmentId().equals(314L)).findFirst().orElse(null);
|
||||
assertNotNull(r4);
|
||||
assertFalse("status=4 已结训学员名册 canSignIn 必须为 false", Boolean.TRUE.equals(r4.getCanSignIn()));
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,15 @@
|
||||
import assert from 'node:assert';
|
||||
import {
|
||||
formatAttendanceTag,
|
||||
formatEnrollmentStatusTag,
|
||||
isEnrollmentEligibleForSignIn,
|
||||
computeRosterActionEligibility,
|
||||
resolveSignInFeedback
|
||||
} from '../../src/views/train/checkin/attendancePolicy.js';
|
||||
|
||||
console.log('=== Testing Attendance Sign-in Entry & Roster Semantics ===');
|
||||
console.log('=== Testing Real Production Attendance Policy Helper ===');
|
||||
|
||||
// 1. 测试考勤状态语义映射:未签到 (NO_CHECKIN) 必须严格与缺勤 (0) 区分
|
||||
function formatAttendanceTag(status) {
|
||||
if (status === null || status === undefined) {
|
||||
return { text: '未签到', type: 'info' };
|
||||
}
|
||||
switch (status) {
|
||||
case 0:
|
||||
return { text: '缺勤', type: 'danger' };
|
||||
case 1:
|
||||
return { text: '正常', type: 'success' };
|
||||
case 2:
|
||||
return { text: '迟到', type: 'warning' };
|
||||
case 3:
|
||||
return { text: '早退', type: 'warning' };
|
||||
case 4:
|
||||
return { text: '请假', type: 'primary' };
|
||||
default:
|
||||
return { text: '-', type: '' };
|
||||
}
|
||||
}
|
||||
|
||||
// A. 验证未打卡学员渲染为“未签到”,绝不等于“缺勤”
|
||||
// 1. 考勤状态语义映射:未签到 (NO_CHECKIN) 必须严格与缺勤 (0) 区分
|
||||
{
|
||||
const tagNull = formatAttendanceTag(null);
|
||||
assert.strictEqual(tagNull.text, '未签到');
|
||||
@@ -31,160 +17,142 @@ function formatAttendanceTag(status) {
|
||||
|
||||
const tagUndefined = formatAttendanceTag(undefined);
|
||||
assert.strictEqual(tagUndefined.text, '未签到');
|
||||
assert.strictEqual(tagUndefined.type, 'info');
|
||||
|
||||
const tagAbsent = formatAttendanceTag(0);
|
||||
assert.strictEqual(tagAbsent.text, '缺勤');
|
||||
assert.strictEqual(tagAbsent.type, 'danger');
|
||||
|
||||
// 红线:未签到和缺勤完全不同
|
||||
// 严格区分未签到与缺勤事实
|
||||
assert.notStrictEqual(tagNull.text, tagAbsent.text);
|
||||
assert.notStrictEqual(tagNull.type, tagAbsent.type);
|
||||
console.log('✔ PASS: NO_CHECKIN is strictly distinct from ABSENT (0)');
|
||||
console.log('✔ PASS: Production formatAttendanceTag distinguishes NO_CHECKIN from ABSENT (0)');
|
||||
}
|
||||
|
||||
// 2. 按钮可见性与防重复规则
|
||||
function getRowActionButtons(row) {
|
||||
return {
|
||||
signIn: !!row.canSignIn,
|
||||
signOut: !!row.canSignOut,
|
||||
adjust: !!row.canAdjust
|
||||
};
|
||||
}
|
||||
|
||||
// A. 无 checkin 记录学员:签到按钮可见,签退不可见
|
||||
// 2. 学员参训状态展示标签
|
||||
{
|
||||
const uncheckRow = {
|
||||
enrollmentId: 101,
|
||||
assert.strictEqual(formatEnrollmentStatusTag(0).text, '待报到');
|
||||
assert.strictEqual(formatEnrollmentStatusTag(1).text, '已报到');
|
||||
assert.strictEqual(formatEnrollmentStatusTag(2).text, '请假');
|
||||
assert.strictEqual(formatEnrollmentStatusTag(3).text, '放弃');
|
||||
assert.strictEqual(formatEnrollmentStatusTag(4).text, '已完成');
|
||||
console.log('✔ PASS: Production formatEnrollmentStatusTag maps all lifecycle statuses correctly');
|
||||
}
|
||||
|
||||
// 3. 参训生命周期首次签到资格规则断言
|
||||
{
|
||||
assert.strictEqual(isEnrollmentEligibleForSignIn(0), false, 'status=0 待报到严禁签到');
|
||||
assert.strictEqual(isEnrollmentEligibleForSignIn(1), true, 'status=1 已报到在训允许签到');
|
||||
assert.strictEqual(isEnrollmentEligibleForSignIn(2), true, 'status=2 请假学员当前 Preview Default 允许签到');
|
||||
assert.strictEqual(isEnrollmentEligibleForSignIn(3), false, 'status=3 放弃学员严禁签到');
|
||||
assert.strictEqual(isEnrollmentEligibleForSignIn(4), false, 'status=4 已完成学员严禁签到');
|
||||
assert.strictEqual(isEnrollmentEligibleForSignIn(null), false, 'null 状态 fail closed');
|
||||
assert.strictEqual(isEnrollmentEligibleForSignIn(undefined), false, 'undefined 状态 fail closed');
|
||||
assert.strictEqual(isEnrollmentEligibleForSignIn(99), false, '未知状态 fail closed');
|
||||
console.log('✔ PASS: Production isEnrollmentEligibleForSignIn strictly enforces lifecycle guard');
|
||||
}
|
||||
|
||||
// 4. 生产环境操作按钮可用性 (computeRosterActionEligibility) 覆盖
|
||||
{
|
||||
// A. status=0 待报到 + 无打卡记录 -> 签到按钮不存在
|
||||
const row0 = {
|
||||
enrollmentId: 100,
|
||||
enrollmentStatus: 0,
|
||||
checkinId: null,
|
||||
attendanceStatus: null,
|
||||
signInTime: null,
|
||||
signOutTime: null,
|
||||
canSignIn: true,
|
||||
canSignOut: false,
|
||||
canAdjust: false
|
||||
canSignIn: false
|
||||
};
|
||||
const actions = getRowActionButtons(uncheckRow);
|
||||
assert.strictEqual(actions.signIn, true, '无签到记录学员签到按钮必须可见');
|
||||
assert.strictEqual(actions.signOut, false, '未签到学员不能显示签退');
|
||||
assert.strictEqual(actions.adjust, false, '无考勤记录学员不能显示调整');
|
||||
console.log('✔ PASS: Unchecked student has visible signIn button, no duplicate path');
|
||||
}
|
||||
const act0 = computeRosterActionEligibility(row0);
|
||||
assert.strictEqual(act0.canSignIn, false, 'status=0 待报到学员签到按钮必须不存在');
|
||||
assert.strictEqual(act0.canSignOut, false);
|
||||
assert.strictEqual(act0.canAdjust, false);
|
||||
|
||||
// B. 已有签到记录:签到按钮不可重复执行
|
||||
{
|
||||
const signedInRow = {
|
||||
// B. status=1 已报到 + NO_CHECKIN -> 签到按钮存在
|
||||
const row1 = {
|
||||
enrollmentId: 101,
|
||||
checkinId: 8001,
|
||||
enrollmentStatus: 1,
|
||||
checkinId: null,
|
||||
attendanceStatus: null,
|
||||
signInTime: null,
|
||||
signOutTime: null,
|
||||
canSignIn: true
|
||||
};
|
||||
const act1 = computeRosterActionEligibility(row1);
|
||||
assert.strictEqual(act1.canSignIn, true, 'status=1 已报到学员未签到时签到按钮必须存在');
|
||||
assert.strictEqual(act1.canSignOut, false);
|
||||
assert.strictEqual(act1.canAdjust, false);
|
||||
|
||||
// C. status=2 请假 + NO_CHECKIN -> 签到按钮存在 (Preview Default)
|
||||
const row2 = {
|
||||
enrollmentId: 102,
|
||||
enrollmentStatus: 2,
|
||||
checkinId: null,
|
||||
attendanceStatus: null,
|
||||
signInTime: null,
|
||||
signOutTime: null,
|
||||
canSignIn: true
|
||||
};
|
||||
const act2 = computeRosterActionEligibility(row2);
|
||||
assert.strictEqual(act2.canSignIn, true, 'status=2 请假学员未签到时签到按钮必须存在 (Preview Default)');
|
||||
assert.strictEqual(act2.canSignOut, false);
|
||||
assert.strictEqual(act2.canAdjust, false);
|
||||
|
||||
// D. status=4 已完成 + NO_CHECKIN -> 签到按钮不存在
|
||||
const row4 = {
|
||||
enrollmentId: 104,
|
||||
enrollmentStatus: 4,
|
||||
checkinId: null,
|
||||
attendanceStatus: null,
|
||||
signInTime: null,
|
||||
signOutTime: null,
|
||||
canSignIn: false
|
||||
};
|
||||
const act4 = computeRosterActionEligibility(row4);
|
||||
assert.strictEqual(act4.canSignIn, false, 'status=4 已完成学员签到按钮必须不存在');
|
||||
assert.strictEqual(act4.canSignOut, false);
|
||||
assert.strictEqual(act4.canAdjust, false);
|
||||
|
||||
// E. 已有合法签到记录 (无论 status 为何) -> 签到按钮禁止重复存在
|
||||
const rowSignedIn = {
|
||||
enrollmentId: 101,
|
||||
enrollmentStatus: 1,
|
||||
checkinId: 8888,
|
||||
attendanceStatus: 1,
|
||||
signInTime: '2026-09-21 08:30:00',
|
||||
signInTime: '2026-09-22 08:30:00',
|
||||
signOutTime: null,
|
||||
canSignIn: false,
|
||||
canSignOut: true,
|
||||
canAdjust: true
|
||||
};
|
||||
const actions = getRowActionButtons(signedInRow);
|
||||
assert.strictEqual(actions.signIn, false, '已有合法签到记录学员不得显示签到按钮');
|
||||
assert.strictEqual(actions.signOut, true, '已签到学员允许签退');
|
||||
assert.strictEqual(actions.adjust, true, '已有打卡记录允许人工调整');
|
||||
console.log('✔ PASS: Signed-in student cannot duplicate sign-in');
|
||||
const actSigned = computeRosterActionEligibility(rowSignedIn);
|
||||
assert.strictEqual(actSigned.canSignIn, false, '已签到学员绝对不能再次显示签到按钮');
|
||||
assert.strictEqual(actSigned.canSignOut, true, '已签到未签退学员激活签退');
|
||||
assert.strictEqual(actSigned.canAdjust, true, '已有打卡记录激活调整');
|
||||
console.log('✔ PASS: Production computeRosterActionEligibility covers all status button visibility');
|
||||
}
|
||||
|
||||
// 3. 模拟 signIn 闭环与名册状态刷新
|
||||
// 5. 后端生命周期报错反馈解析真实性 (resolveSignInFeedback)
|
||||
{
|
||||
// 初始名册
|
||||
let roster = [
|
||||
{
|
||||
enrollmentId: 201,
|
||||
studentName: '张三',
|
||||
checkinId: null,
|
||||
attendanceStatus: null,
|
||||
signInTime: null,
|
||||
signOutTime: null,
|
||||
canSignIn: true,
|
||||
canSignOut: false,
|
||||
canAdjust: false
|
||||
}
|
||||
];
|
||||
const errWaiting = { code: 40085, msg: '学员尚未完成开班报到,禁止录入课次考勤' };
|
||||
const fbWaiting = resolveSignInFeedback(errWaiting, '张三');
|
||||
assert.strictEqual(fbWaiting.isSuccess, false);
|
||||
assert.strictEqual(fbWaiting.message.includes('学员尚未完成开班报到'), true);
|
||||
assert.strictEqual(fbWaiting.message.includes('成功'), false);
|
||||
|
||||
assert.strictEqual(roster[0].canSignIn, true);
|
||||
assert.strictEqual(roster[0].attendanceStatus, null);
|
||||
const errCompleted = { code: 40087, msg: '学员参训档案已结训完成,禁止录入课次考勤' };
|
||||
const fbCompleted = resolveSignInFeedback(errCompleted, '李四');
|
||||
assert.strictEqual(fbCompleted.isSuccess, false);
|
||||
assert.strictEqual(fbCompleted.message.includes('学员参训档案已结训完成'), true);
|
||||
assert.strictEqual(fbCompleted.message.includes('成功'), false);
|
||||
|
||||
// 模拟执行 signIn 成功
|
||||
function mockSignIn(enrollmentId, lessonId) {
|
||||
return Promise.resolve({
|
||||
checkinId: 5001,
|
||||
enrollmentId: enrollmentId,
|
||||
lessonId: lessonId,
|
||||
attendanceStatus: 1,
|
||||
signInTime: '2026-09-21 09:00:00',
|
||||
signOutTime: null
|
||||
});
|
||||
}
|
||||
const errAbandoned = { code: 40086, msg: '学员参训状态已放弃,禁止录入课次考勤' };
|
||||
const fbAbandoned = resolveSignInFeedback(errAbandoned, '王五');
|
||||
assert.strictEqual(fbAbandoned.isSuccess, false);
|
||||
assert.strictEqual(fbAbandoned.message.includes('学员参训状态已放弃'), true);
|
||||
|
||||
// 模拟刷新 roster
|
||||
function mockRefreshRoster(updatedRecord) {
|
||||
roster = roster.map(item => {
|
||||
if (item.enrollmentId === updatedRecord.enrollmentId) {
|
||||
return {
|
||||
...item,
|
||||
checkinId: updatedRecord.checkinId,
|
||||
attendanceStatus: updatedRecord.attendanceStatus,
|
||||
signInTime: updatedRecord.signInTime,
|
||||
signOutTime: updatedRecord.signOutTime,
|
||||
canSignIn: false, // 已签到禁止再签到
|
||||
canSignOut: true,
|
||||
canAdjust: true
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
mockSignIn(201, 1001).then(res => {
|
||||
mockRefreshRoster(res);
|
||||
assert.strictEqual(roster[0].checkinId, 5001);
|
||||
assert.strictEqual(roster[0].attendanceStatus, 1);
|
||||
assert.strictEqual(roster[0].canSignIn, false, '刷新后签到按钮必须消失');
|
||||
assert.strictEqual(roster[0].canSignOut, true, '刷新后签退按钮必须激活');
|
||||
assert.strictEqual(roster[0].canAdjust, true, '刷新后调整按钮必须激活');
|
||||
console.log('✔ PASS: signIn success refreshes roster to current attendance fact');
|
||||
});
|
||||
console.log('✔ PASS: Production resolveSignInFeedback faithfully reports errors without false success');
|
||||
}
|
||||
|
||||
// 4. 后端错误真实展示,不得出现假成功
|
||||
{
|
||||
function handleSignInError(err) {
|
||||
// 页面逻辑:
|
||||
let displayedMsg = '';
|
||||
let isSuccess = false;
|
||||
if (err && (err.msg || err.message)) {
|
||||
displayedMsg = err.msg || err.message;
|
||||
} else {
|
||||
displayedMsg = '签到操作失败';
|
||||
}
|
||||
return { displayedMsg, isSuccess };
|
||||
}
|
||||
|
||||
const backendError = { code: 400, msg: '课次未关联有效的培训课程计划' };
|
||||
const feedback = handleSignInError(backendError);
|
||||
|
||||
assert.strictEqual(feedback.isSuccess, false);
|
||||
assert.strictEqual(feedback.displayedMsg, '课次未关联有效的培训课程计划');
|
||||
assert.strictEqual(feedback.displayedMsg.includes('成功'), false);
|
||||
console.log('✔ PASS: Backend error is truthfully reported without false success');
|
||||
}
|
||||
|
||||
// 5. 权限可达性检查:train_admin 具备 checkin 权限与 list 权限
|
||||
{
|
||||
const trainAdminPerms = ['train:attendance:list', 'train:attendance:checkin', 'train:attendance:edit', 'train:attendance:export'];
|
||||
const canAccessList = trainAdminPerms.includes('train:attendance:list');
|
||||
const canPerformSignIn = trainAdminPerms.includes('train:attendance:checkin');
|
||||
const canPerformAdjust = trainAdminPerms.includes('train:attendance:edit');
|
||||
|
||||
assert.strictEqual(canAccessList, true, 'train_admin 必须有查看考勤名册权限');
|
||||
assert.strictEqual(canPerformSignIn, true, 'train_admin 必须有课次签到权限');
|
||||
assert.strictEqual(canPerformAdjust, true, 'train_admin 必须有课次调整权限');
|
||||
console.log('✔ PASS: train_admin has full permission reachability');
|
||||
}
|
||||
|
||||
console.log('ALL FRONTEND ATTENDANCE SIGN-IN ENTRY TESTS PASSED!');
|
||||
console.log('ALL PRODUCTION ATTENDANCE POLICY TESTS PASSED!');
|
||||
Reference in new issue
Block a user