import { describe, it, expect } from "vitest" import { toWeekday, countStudentAssignments, sortUpcomingAssignments, filterTodaySchedule, computeTeacherMetrics, getGreetingKey, type Weekday, } from "@/modules/dashboard/lib/dashboard-utils" import type { StudentHomeworkAssignmentListItem } from "@/modules/homework/types" import type { ClassScheduleItem, TeacherClass } from "@/modules/classes/types" import type { HomeworkAssignmentListItem, HomeworkSubmissionListItem, TeacherGradeTrendItem, } from "@/modules/homework/types" describe("toWeekday", () => { it("returns 1 for Monday", () => { const monday = new Date("2026-06-22T00:00:00") // 2026-06-22 is Monday expect(toWeekday(monday)).toBe(1 as Weekday) }) it("returns 7 for Sunday", () => { const sunday = new Date("2026-06-28T00:00:00") // 2026-06-28 is Sunday expect(toWeekday(sunday)).toBe(7 as Weekday) }) it("returns 6 for Saturday", () => { const saturday = new Date("2026-06-27T00:00:00") // 2026-06-27 is Saturday expect(toWeekday(saturday)).toBe(6 as Weekday) }) it("returns 2 for Tuesday", () => { const tuesday = new Date("2026-06-23T00:00:00") // 2026-06-23 is Tuesday expect(toWeekday(tuesday)).toBe(2 as Weekday) }) }) describe("getGreetingKey", () => { it("returns morning before 12:00", () => { expect(getGreetingKey(new Date("2026-06-22T08:00:00"))).toBe("morning") expect(getGreetingKey(new Date("2026-06-22T11:59:59"))).toBe("morning") }) it("returns afternoon between 12:00 and 18:00", () => { expect(getGreetingKey(new Date("2026-06-22T12:00:00"))).toBe("afternoon") expect(getGreetingKey(new Date("2026-06-22T17:59:59"))).toBe("afternoon") }) it("returns evening at 18:00 or later", () => { expect(getGreetingKey(new Date("2026-06-22T18:00:00"))).toBe("evening") expect(getGreetingKey(new Date("2026-06-22T23:59:59"))).toBe("evening") }) }) describe("countStudentAssignments", () => { const now = new Date("2026-06-22T10:00:00") it("returns zero counts for empty list", () => { expect(countStudentAssignments([], now)).toEqual({ dueSoonCount: 0, overdueCount: 0, gradedCount: 0, }) }) it("counts graded assignments", () => { const assignments = [ makeAssignment("1", "graded", null), makeAssignment("2", "graded", null), ] expect(countStudentAssignments(assignments, now)).toEqual({ dueSoonCount: 0, overdueCount: 0, gradedCount: 2, }) }) it("counts due soon (within 7 days)", () => { const in3Days = new Date(now) in3Days.setDate(in3Days.getDate() + 3) const assignments = [ makeAssignment("1", "in_progress", in3Days.toISOString()), ] expect(countStudentAssignments(assignments, now).dueSoonCount).toBe(1) }) it("counts overdue", () => { const past = new Date(now) past.setDate(past.getDate() - 1) const assignments = [ makeAssignment("1", "submitted", past.toISOString()), ] expect(countStudentAssignments(assignments, now).overdueCount).toBe(1) }) it("ignores assignments without dueAt (non-graded)", () => { const assignments = [ makeAssignment("1", "in_progress", null), ] expect(countStudentAssignments(assignments, now)).toEqual({ dueSoonCount: 0, overdueCount: 0, gradedCount: 0, }) }) it("respects custom dueSoonWindowDays", () => { const in15Days = new Date(now) in15Days.setDate(in15Days.getDate() + 15) const assignments = [ makeAssignment("1", "in_progress", in15Days.toISOString()), ] // default 7-day window: not due soon expect(countStudentAssignments(assignments, now).dueSoonCount).toBe(0) // 30-day window: due soon expect(countStudentAssignments(assignments, now, 30).dueSoonCount).toBe(1) }) }) describe("sortUpcomingAssignments", () => { it("returns empty array for empty input", () => { expect(sortUpcomingAssignments([])).toEqual([]) }) it("sorts by dueAt ascending", () => { const assignments = [ makeAssignment("1", "in_progress", "2026-06-25T00:00:00"), makeAssignment("2", "in_progress", "2026-06-23T00:00:00"), makeAssignment("3", "in_progress", "2026-06-24T00:00:00"), ] const sorted = sortUpcomingAssignments(assignments) expect(sorted.map((a) => a.id)).toEqual(["2", "3", "1"]) }) it("puts assignments without dueAt at the end", () => { const assignments = [ makeAssignment("1", "in_progress", null), makeAssignment("2", "in_progress", "2026-06-23T00:00:00"), ] const sorted = sortUpcomingAssignments(assignments) expect(sorted.map((a) => a.id)).toEqual(["2", "1"]) }) it("respects limit parameter", () => { const assignments = Array.from({ length: 10 }, (_, i) => makeAssignment(`${i}`, "in_progress", `2026-06-${23 + i}T00:00:00`), ) expect(sortUpcomingAssignments(assignments, 3)).toHaveLength(3) }) it("does not mutate original array", () => { const assignments = [ makeAssignment("1", "in_progress", "2026-06-25T00:00:00"), makeAssignment("2", "in_progress", "2026-06-23T00:00:00"), ] sortUpcomingAssignments(assignments) expect(assignments.map((a) => a.id)).toEqual(["1", "2"]) }) }) describe("filterTodaySchedule", () => { const weekday = 1 as Weekday // Monday it("returns empty array for empty schedule", () => { expect(filterTodaySchedule([], weekday)).toEqual([]) }) it("filters by weekday", () => { const schedule: ClassScheduleItem[] = [ makeScheduleItem("1", 1, "08:00"), makeScheduleItem("2", 2, "09:00"), makeScheduleItem("3", 1, "07:00"), ] const result = filterTodaySchedule(schedule, weekday) expect(result).toHaveLength(2) expect((result as Array<{ id: string }>).map((s) => s.id)).toEqual(["3", "1"]) }) it("sorts by startTime ascending", () => { const schedule: ClassScheduleItem[] = [ makeScheduleItem("1", 1, "10:00"), makeScheduleItem("2", 1, "08:00"), makeScheduleItem("3", 1, "09:00"), ] const result = filterTodaySchedule(schedule, weekday) as Array<{ id: string }> expect(result.map((s) => s.id)).toEqual(["2", "3", "1"]) }) it("uses classNameById map when provided", () => { const schedule: ClassScheduleItem[] = [ makeScheduleItem("1", 1, "08:00"), ] const classNameById = new Map([["class-1", "Math 101"]]) const result = filterTodaySchedule(schedule, weekday, classNameById) as Array<{ className: string }> expect(result[0].className).toBe("Math 101") }) it("falls back to 'Class' when classNameById missing", () => { const schedule: ClassScheduleItem[] = [ makeScheduleItem("1", 1, "08:00"), ] const result = filterTodaySchedule(schedule, weekday) as Array<{ className: string }> expect(result[0].className).toBe("Class") }) }) describe("computeTeacherMetrics", () => { const now = new Date("2026-06-22T10:00:00") // Monday it("returns zero metrics for empty inputs", () => { const metrics = computeTeacherMetrics([], [], [], [], [], now) expect(metrics.toGradeCount).toBe(0) expect(metrics.activeAssignmentsCount).toBe(0) expect(metrics.averageScore).toBe(0) expect(metrics.submissionRate).toBe(0) expect(metrics.todayScheduleItems).toEqual([]) expect(metrics.submissionsToGrade).toEqual([]) }) it("counts toGrade from submitted (non-graded) submissions", () => { const submissions: HomeworkSubmissionListItem[] = [ makeSubmission("1", "submitted"), makeSubmission("2", "submitted"), makeSubmission("3", "graded"), ] const metrics = computeTeacherMetrics([], [], [], submissions, [], now) expect(metrics.toGradeCount).toBe(2) }) it("counts active assignments (published)", () => { const assignments: HomeworkAssignmentListItem[] = [ makeAssignmentTeacher("1", "published"), makeAssignmentTeacher("2", "published"), makeAssignmentTeacher("3", "draft"), ] const metrics = computeTeacherMetrics([], [], assignments, [], [], now) expect(metrics.activeAssignmentsCount).toBe(2) }) it("computes averageScore from gradeTrends", () => { const trends: TeacherGradeTrendItem[] = [ makeTrend("1", 80), makeTrend("2", 90), ] const metrics = computeTeacherMetrics([], [], [], [], trends, now) expect(metrics.averageScore).toBe(85) }) it("computes submissionRate as percentage", () => { const trends: TeacherGradeTrendItem[] = [ makeTrendWithSubmissions("1", 8, 10), makeTrendWithSubmissions("2", 6, 10), ] const metrics = computeTeacherMetrics([], [], [], [], trends, now) // (8 + 6) / (10 + 10) * 100 = 70 expect(metrics.submissionRate).toBe(70) }) it("returns submissionRate 0 when no students", () => { const trends: TeacherGradeTrendItem[] = [ makeTrendWithSubmissions("1", 0, 0), ] const metrics = computeTeacherMetrics([], [], [], [], trends, now) expect(metrics.submissionRate).toBe(0) }) it("filters today schedule by current weekday", () => { const classes: TeacherClass[] = [ makeClass("c1", "Math 101"), ] const schedule: ClassScheduleItem[] = [ makeScheduleItem("s1", 1, "08:00"), // Monday makeScheduleItem("s2", 2, "09:00"), // Tuesday ] const metrics = computeTeacherMetrics(classes, schedule, [], [], [], now) expect(metrics.todayScheduleItems).toHaveLength(1) }) it("limits submissionsToGrade to 6 items", () => { const submissions: HomeworkSubmissionListItem[] = Array.from({ length: 10 }, (_, i) => makeSubmission(`${i}`, "submitted"), ) const metrics = computeTeacherMetrics([], [], [], submissions, [], now) expect(metrics.submissionsToGrade).toHaveLength(6) }) }) // ============ Helpers ============ function makeAssignment( id: string, progressStatus: string, dueAt: string | null, ): StudentHomeworkAssignmentListItem { return { id, title: `Assignment ${id}`, subjectName: null, dueAt, availableAt: null, maxAttempts: 1, attemptsUsed: 0, progressStatus: progressStatus as StudentHomeworkAssignmentListItem["progressStatus"], latestSubmissionId: null, latestSubmittedAt: null, latestScore: null, } } function makeScheduleItem( id: string, weekday: number, startTime: string, ): ClassScheduleItem { return { id, classId: `class-${id}`, weekday: weekday as ClassScheduleItem["weekday"], startTime, endTime: "09:00", course: `Course ${id}`, location: null, } as ClassScheduleItem } function makeSubmission( id: string, status: string, ): HomeworkSubmissionListItem { return { id, assignmentId: `assignment-${id}`, assignmentTitle: `Assignment ${id}`, studentName: `Student ${id}`, status: status as HomeworkSubmissionListItem["status"], submittedAt: "2026-06-22T10:00:00", isLate: false, score: null, } } function makeAssignmentTeacher( id: string, status: string, ): HomeworkAssignmentListItem { return { id, sourceExamId: null, sourceExamTitle: null, title: `Assignment ${id}`, status: status as HomeworkAssignmentListItem["status"], availableAt: null, dueAt: null, allowLate: false, lateDueAt: null, maxAttempts: 1, createdAt: "2026-06-01T00:00:00", updatedAt: "2026-06-01T00:00:00", targetCount: 0, submittedCount: 0, gradedCount: 0, averageScore: null, overdueCount: 0, } } function makeTrend(id: string, averageScore: number): TeacherGradeTrendItem { return { id, title: `Trend ${id}`, averageScore, maxScore: 100, submissionCount: 10, totalStudents: 10, createdAt: "2026-06-01T00:00:00", } } function makeTrendWithSubmissions( id: string, submissionCount: number, totalStudents: number, ): TeacherGradeTrendItem { return { id, title: `Trend ${id}`, averageScore: 80, maxScore: 100, submissionCount, totalStudents, createdAt: "2026-06-01T00:00:00", } } function makeClass(id: string, name: string): TeacherClass { return { id, name, grade: "G1", homeroom: null, room: null, studentCount: 0, } as TeacherClass }