Files
NextEdu/tests/integration/elective/elective-pure-functions.test.ts
SpecialX 0e63c24ed9
Some checks failed
CI / scheduled-backup (push) Has been skipped
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
feat(shared,tests): add error boundaries, lib utils, i18n messages, and integration tests
shared:

- Add class-filter, error-state, route-error, section-error-boundary, widget-boundary components

- Add ui/alert component

- Add constants directory

- Add breached-password, export-utils, permission-bitmap, rate-limit, resolve-action-error, route-permissions, route-resolver, type-guards lib

- Add i18n messages (en, zh-CN) for invitation-codes, parent, questions, rbac

tests:

- Add integration tests for elective

- Add tests/setup/empty-stub

scripts:

- Add update-md.cjs, tmp_append_en.ps1, tmp_merge_en.ps1 utilities
2026-07-03 10:26:38 +08:00

315 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:0015: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()
})
})