122 lines
3.9 KiB
TypeScript
122 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
canPullAttendance,
|
||
filterLessonAttendanceRecords,
|
||
getAttendanceExperience,
|
||
getPunchDisplayInfo,
|
||
getSchedulePhase,
|
||
summarizeAttendance,
|
||
summarizeLessonCheckins,
|
||
} from './attendance-workspace';
|
||
|
||
describe('attendance role experience', () => {
|
||
it('routes class-scoped teachers to the teaching workspace', () => {
|
||
expect(
|
||
getAttendanceExperience(
|
||
['attendance:view', 'attendance:create', 'schedule:create'],
|
||
['老师'],
|
||
),
|
||
).toBe('teacher');
|
||
});
|
||
|
||
it('routes users with attendance administration permission to history management', () => {
|
||
expect(getAttendanceExperience(['attendance:view', 'attendance:edit'], ['教务管理员'])).toBe(
|
||
'admin',
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('teacher schedule phase', () => {
|
||
it('marks a finished lesson as ready for attendance review', () => {
|
||
expect(getSchedulePhase('08:00', '09:00', new Date('2026-07-11T10:00:00'))).toBe('ended');
|
||
});
|
||
|
||
it('keeps future lessons read-only', () => {
|
||
expect(getSchedulePhase('14:00', '15:00', new Date('2026-07-11T10:00:00'))).toBe('upcoming');
|
||
});
|
||
|
||
it('allows attendance pulls once the lesson starts', () => {
|
||
expect(canPullAttendance('ongoing')).toBe(true);
|
||
expect(canPullAttendance('ended')).toBe(true);
|
||
expect(canPullAttendance('upcoming')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('attendance summary', () => {
|
||
it('summarizes status counts for an administrator history view', () => {
|
||
expect(
|
||
summarizeAttendance([
|
||
{ status: 'present' },
|
||
{ status: 'present' },
|
||
{ status: 'late' },
|
||
{ status: 'absent' },
|
||
]),
|
||
).toEqual({ total: 4, present: 2, late: 1, absent: 1, leave: 0, pending: 0 });
|
||
});
|
||
});
|
||
|
||
describe('lesson check-in summary', () => {
|
||
it('counts late punches as checked in and missing punches as not checked in', () => {
|
||
expect(
|
||
summarizeLessonCheckins([
|
||
{ status: 'present' },
|
||
{ status: 'late' },
|
||
{ status: 'pending' },
|
||
{ status: 'absent' },
|
||
]),
|
||
).toEqual({ total: 4, checkedIn: 2, notCheckedIn: 2 });
|
||
});
|
||
});
|
||
|
||
|
||
describe('lesson attendance filters', () => {
|
||
const records = [
|
||
{ id: 1, student: { name: '张三' }, status: 'present' },
|
||
{ id: 2, student: { name: '李四' }, status: 'late' },
|
||
{ id: 3, student: { name: '王五' }, status: 'pending' },
|
||
{ id: 4, student: { name: '赵六' }, status: 'absent' },
|
||
];
|
||
|
||
it('searches students by name and ignores surrounding whitespace', () => {
|
||
expect(filterLessonAttendanceRecords(records, ' 张 ', 'all').map((item) => item.id)).toEqual([1]);
|
||
});
|
||
|
||
it('groups present and late as checked in', () => {
|
||
expect(filterLessonAttendanceRecords(records, '', 'checked_in').map((item) => item.id)).toEqual([1, 2]);
|
||
});
|
||
|
||
it('groups pending and absent as not checked in and combines with search', () => {
|
||
expect(filterLessonAttendanceRecords(records, '王', 'not_checked_in').map((item) => item.id)).toEqual([3]);
|
||
});
|
||
});
|
||
|
||
describe('lesson punch device display', () => {
|
||
it('labels attendance machine punches with the machine name and id', () => {
|
||
expect(
|
||
getPunchDisplayInfo({
|
||
status: 'present',
|
||
source: 'dingtalk',
|
||
punchSource: 'ATM',
|
||
punchDeviceName: '东门考勤机',
|
||
punchDeviceId: 'ATM-01',
|
||
punchTime: '2026-07-11T00:55:00.000Z',
|
||
}),
|
||
).toEqual({
|
||
label: '考勤机打卡',
|
||
machine: true,
|
||
detail: '东门考勤机(ATM-01)',
|
||
time: '2026-07-11T00:55:00.000Z',
|
||
});
|
||
});
|
||
|
||
it('distinguishes mobile punches and manual teacher markings', () => {
|
||
expect(
|
||
getPunchDisplayInfo({ status: 'present', source: 'dingtalk', punchSource: 'USER' }),
|
||
).toEqual({ label: '手机打卡', machine: false, detail: undefined, time: undefined });
|
||
expect(getPunchDisplayInfo({ status: 'present', source: 'manual' })).toEqual({
|
||
label: '老师手动标记',
|
||
machine: false,
|
||
});
|
||
});
|
||
});
|