import { describe, it, expect } from "vitest" import { parseSchedule, isScheduleConflict, normalizeDay, buildLotteryRankCase, } from "@/modules/elective/data-access-operations" import { mapCourseRow, type CourseCoreRow } from "@/modules/elective/data-access" /** * Elective 模块纯函数单元测试(P2-18 新增)。 * * 覆盖范围: * - normalizeDay:中英文星期归一化 * - parseSchedule:时间段解析(单时段、多时段、中英文、非法输入) * - isScheduleConflict:冲突检测(同天重叠、跨天不冲突、归一化后冲突) * - buildLotteryRankCase:抽签 rank SQL 构建结构 * - mapCourseRow:数据库行 → 业务对象映射 * * 这些纯函数不依赖 DB,可在 vitest node 环境下独立运行。 */ describe("normalizeDay", () => { it("maps Chinese 周一-周天 to 1-7", () => { expect(normalizeDay("周一")).toBe("1") expect(normalizeDay("周二")).toBe("2") expect(normalizeDay("周三")).toBe("3") expect(normalizeDay("周四")).toBe("4") expect(normalizeDay("周五")).toBe("5") expect(normalizeDay("周六")).toBe("6") expect(normalizeDay("周日")).toBe("7") expect(normalizeDay("周天")).toBe("7") }) it("maps Chinese 星期一-星期日 to 1-7", () => { expect(normalizeDay("星期一")).toBe("1") expect(normalizeDay("星期二")).toBe("2") expect(normalizeDay("星期日")).toBe("7") expect(normalizeDay("星期天")).toBe("7") }) it("maps English full day names case-insensitively", () => { expect(normalizeDay("monday")).toBe("1") expect(normalizeDay("Tuesday")).toBe("2") expect(normalizeDay("WEDNESDAY")).toBe("3") expect(normalizeDay("Thursday")).toBe("4") expect(normalizeDay("friday")).toBe("5") expect(normalizeDay("Saturday")).toBe("6") expect(normalizeDay("sunday")).toBe("7") }) it("maps English abbreviations case-insensitively", () => { expect(normalizeDay("mon")).toBe("1") expect(normalizeDay("Tue")).toBe("2") expect(normalizeDay("WED")).toBe("3") expect(normalizeDay("thu")).toBe("4") expect(normalizeDay("fri")).toBe("5") expect(normalizeDay("sat")).toBe("6") expect(normalizeDay("sun")).toBe("7") }) it("returns input unchanged for unknown values", () => { expect(normalizeDay("foo")).toBe("foo") expect(normalizeDay("")).toBe("") }) }) describe("parseSchedule", () => { it("returns empty array for null/empty input", () => { expect(parseSchedule(null)).toEqual([]) expect(parseSchedule("")).toEqual([]) expect(parseSchedule(" ")).toEqual([]) }) it("parses single Chinese schedule", () => { const result = parseSchedule("周一 14:00-15:30") expect(result).toHaveLength(1) expect(result[0]).toEqual({ day: "周一", start: "14:00", end: "15:30", }) }) it("parses single English full day name", () => { const result = parseSchedule("Monday 09:00-10:30") expect(result).toHaveLength(1) expect(result[0]).toEqual({ day: "Monday", start: "09:00", end: "10:30", }) }) it("parses single English abbreviation", () => { const result = parseSchedule("Mon 14:00-15:30") expect(result).toHaveLength(1) expect(result[0]).toEqual({ day: "Mon", start: "14:00", end: "15:30", }) }) it("parses 星期一 format", () => { const result = parseSchedule("星期一 08:00-09:00") expect(result).toHaveLength(1) expect(result[0]).toEqual({ day: "星期一", start: "08:00", end: "09:00", }) }) it("parses multiple time slots separated by comma", () => { const result = parseSchedule("周一 14:00-15:30, Wed 16:00-17:30") expect(result).toHaveLength(2) expect(result[0]).toEqual({ day: "周一", start: "14:00", end: "15:30" }) expect(result[1]).toEqual({ day: "Wed", start: "16:00", end: "17:30" }) }) it("parses multiple time slots separated by Chinese semicolon", () => { const result = parseSchedule("周一 14:00-15:30;周三 16:00-17:30") expect(result).toHaveLength(2) expect(result[0]).toEqual({ day: "周一", start: "14:00", end: "15:30" }) expect(result[1]).toEqual({ day: "周三", start: "16:00", end: "17:30" }) }) it("parses multiple time slots separated by English semicolon", () => { const result = parseSchedule("Mon 14:00-15:30; Wed 16:00-17:30") expect(result).toHaveLength(2) }) it("supports ~ and ~ and 至 as time range separators", () => { expect(parseSchedule("周一 14:00~15:30")).toHaveLength(1) expect(parseSchedule("周一 14:00~15:30")).toHaveLength(1) expect(parseSchedule("周一 14:00至15:30")).toHaveLength(1) }) it("pads single-digit hour with leading zero", () => { const result = parseSchedule("周一 9:00-10:30") expect(result).toHaveLength(1) expect(result[0]?.start).toBe("09:00") expect(result[0]?.end).toBe("10:30") }) it("returns empty array for invalid format", () => { expect(parseSchedule("invalid")).toEqual([]) expect(parseSchedule("周一")).toEqual([]) expect(parseSchedule("周一 14:00")).toEqual([]) expect(parseSchedule("周一 invalid-invalid")).toEqual([]) }) it("skips invalid segments in multi-slot input but keeps valid ones", () => { const result = parseSchedule("周一 14:00-15:30, invalid, Wed 16:00-17:30") expect(result).toHaveLength(2) }) }) describe("isScheduleConflict", () => { it("returns false for different days", () => { const a = { day: "周一", start: "14:00", end: "15:30" } const b = { day: "周二", start: "14:00", end: "15:30" } expect(isScheduleConflict(a, b)).toBe(false) }) it("returns true for overlapping time on same day (Chinese)", () => { const a = { day: "周一", start: "14:00", end: "15:30" } const b = { day: "周一", start: "15:00", end: "16:00" } expect(isScheduleConflict(a, b)).toBe(true) }) it("returns false for adjacent non-overlapping on same day", () => { const a = { day: "周一", start: "14:00", end: "15:30" } const b = { day: "周一", start: "15:30", end: "17:00" } expect(isScheduleConflict(a, b)).toBe(false) }) it("normalizes day before comparison (周一 vs Mon)", () => { const a = { day: "周一", start: "14:00", end: "15:30" } const b = { day: "Mon", start: "15:00", end: "16:00" } expect(isScheduleConflict(a, b)).toBe(true) }) it("normalizes day before comparison (Monday vs 星期一)", () => { const a = { day: "Monday", start: "14:00", end: "15:30" } const b = { day: "星期一", start: "15:00", end: "16:00" } expect(isScheduleConflict(a, b)).toBe(true) }) it("returns false for non-overlapping on same day", () => { const a = { day: "周一", start: "08:00", end: "09:00" } const b = { day: "周一", start: "14:00", end: "15:30" } expect(isScheduleConflict(a, b)).toBe(false) }) it("handles contained intervals", () => { const a = { day: "周一", start: "14:00", end: "17:00" } const b = { day: "周一", start: "15:00", end: "16:00" } expect(isScheduleConflict(a, b)).toBe(true) }) }) describe("buildLotteryRankCase", () => { it("returns SQL object for empty input", () => { const sql = buildLotteryRankCase([], 1) expect(sql).toBeDefined() }) it("returns SQL object for non-empty input", () => { const sql = buildLotteryRankCase(["id1", "id2", "id3"], 5) expect(sql).toBeDefined() }) it("preserves order and start rank", () => { // SQL 对象本身不可直接断言内部结构,但应能成功构建 const ids = ["a", "b", "c", "d"] const sql = buildLotteryRankCase(ids, 10) expect(sql).toBeDefined() // 不抛异常即视为通过;实际 SQL 执行由集成测试覆盖 }) }) describe("mapCourseRow", () => { // 构造一个最小的 mock 行(仅包含 mapCourseRow 使用的字段) const mockRow: CourseCoreRow = { id: "course-1", name: "Python 入门", subjectId: "subj-1", teacherId: "user-1", gradeId: "grade-1", description: "Python 编程基础", capacity: 30, enrolledCount: 25, classroom: "A101", schedule: "周一 14:00-15:30", startDate: new Date("2026-09-01"), endDate: new Date("2027-01-15"), selectionStartAt: new Date("2026-08-20T08:00:00Z"), selectionEndAt: new Date("2026-08-25T18:00:00Z"), status: "open", selectionMode: "lottery", credit: "2.0", createdAt: new Date("2026-06-01T00:00:00Z"), updatedAt: new Date("2026-06-15T12:00:00Z"), } it("maps basic fields correctly", () => { const result = mapCourseRow( mockRow, new Map(), new Map(), new Map() ) expect(result.id).toBe("course-1") expect(result.name).toBe("Python 入门") expect(result.capacity).toBe(30) expect(result.enrolledCount).toBe(25) expect(result.status).toBe("open") expect(result.selectionMode).toBe("lottery") }) it("converts credit to string", () => { const result = mapCourseRow(mockRow, new Map(), new Map(), new Map()) expect(result.credit).toBe("2.0") expect(typeof result.credit).toBe("string") }) it("formats startDate as YYYY-MM-DD", () => { const result = mapCourseRow(mockRow, new Map(), new Map(), new Map()) expect(result.startDate).toBe("2026-09-01") }) it("converts timestamps to ISO strings", () => { const result = mapCourseRow(mockRow, new Map(), new Map(), new Map()) expect(result.createdAt).toBe("2026-06-01T00:00:00.000Z") expect(result.updatedAt).toBe("2026-06-15T12:00:00.000Z") expect(result.selectionStartAt).toBe("2026-08-20T08:00:00.000Z") }) it("resolves teacherName from map", () => { const teacherNames = new Map([["user-1", "张老师"]]) const result = mapCourseRow(mockRow, teacherNames, new Map(), new Map()) expect(result.teacherName).toBe("张老师") }) it("returns null teacherName when teacherId not in map", () => { const result = mapCourseRow(mockRow, new Map(), new Map(), new Map()) expect(result.teacherName).toBeNull() }) it("resolves subjectName and gradeName from maps", () => { const teacherNames = new Map([["user-1", "张老师"]]) const subjectNames = new Map([["subj-1", "计算机"]]) const gradeNames = new Map([["grade-1", "高一"]]) const result = mapCourseRow(mockRow, teacherNames, subjectNames, gradeNames) expect(result.subjectName).toBe("计算机") expect(result.gradeName).toBe("高一") }) it("returns null subjectName/gradeName when ids are null", () => { const rowWithNulls: CourseCoreRow = { ...mockRow, subjectId: null, gradeId: null, } const result = mapCourseRow(rowWithNulls, new Map(), new Map(), new Map()) expect(result.subjectName).toBeNull() expect(result.gradeName).toBeNull() expect(result.subjectId).toBeNull() expect(result.gradeId).toBeNull() }) })