Files
banner/web/src/lib/course.test.ts
Xevion 61f8bd9de7 refactor: consolidate menu snippets and strengthen type safety
Replaces duplicated dropdown/context menu code with parameterized snippet,
eliminates unsafe type casts, adds error handling for clipboard and API
calls, and improves accessibility annotations.
2026-01-29 11:40:55 -06:00

408 lines
13 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 {
formatTime,
formatTimeRange,
formatMeetingDays,
formatMeetingDaysVerbose,
formatMeetingTime,
formatMeetingTimeTooltip,
formatMeetingTimesTooltip,
abbreviateInstructor,
formatCreditHours,
getPrimaryInstructor,
isMeetingTimeTBA,
isTimeTBA,
formatDate,
formatDateShort,
formatMeetingDaysLong,
} from "$lib/course";
import type { DbMeetingTime, CourseResponse, InstructorResponse } from "$lib/api";
function makeMeetingTime(overrides: Partial<DbMeetingTime> = {}): DbMeetingTime {
return {
begin_time: null,
end_time: null,
start_date: "2024-08-26",
end_date: "2024-12-12",
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
sunday: false,
building: null,
building_description: null,
room: null,
campus: null,
meeting_type: "CLAS",
meeting_schedule_type: "LEC",
...overrides,
};
}
describe("formatTime", () => {
it("converts 0900 to 9:00 AM", () => expect(formatTime("0900")).toBe("9:00 AM"));
it("converts 1330 to 1:30 PM", () => expect(formatTime("1330")).toBe("1:30 PM"));
it("converts 0000 to 12:00 AM", () => expect(formatTime("0000")).toBe("12:00 AM"));
it("converts 1200 to 12:00 PM", () => expect(formatTime("1200")).toBe("12:00 PM"));
it("converts 2359 to 11:59 PM", () => expect(formatTime("2359")).toBe("11:59 PM"));
it("returns TBA for null", () => expect(formatTime(null)).toBe("TBA"));
it("returns TBA for empty string", () => expect(formatTime("")).toBe("TBA"));
it("returns TBA for short string", () => expect(formatTime("09")).toBe("TBA"));
});
describe("formatMeetingDays", () => {
it("returns MWF for mon/wed/fri", () => {
expect(
formatMeetingDays(makeMeetingTime({ monday: true, wednesday: true, friday: true }))
).toBe("MWF");
});
it("returns TTh for tue/thu", () => {
expect(formatMeetingDays(makeMeetingTime({ tuesday: true, thursday: true }))).toBe("TTh");
});
it("returns MW for mon/wed", () => {
expect(formatMeetingDays(makeMeetingTime({ monday: true, wednesday: true }))).toBe("MW");
});
it("returns MTWThF for all weekdays", () => {
expect(
formatMeetingDays(
makeMeetingTime({
monday: true,
tuesday: true,
wednesday: true,
thursday: true,
friday: true,
})
)
).toBe("MTWThF");
});
it("returns partial abbreviation for single day", () => {
expect(formatMeetingDays(makeMeetingTime({ monday: true }))).toBe("Mon");
expect(formatMeetingDays(makeMeetingTime({ thursday: true }))).toBe("Thu");
expect(formatMeetingDays(makeMeetingTime({ saturday: true }))).toBe("Sat");
});
it("concatenates codes for other multi-day combos", () => {
expect(formatMeetingDays(makeMeetingTime({ monday: true, friday: true }))).toBe("MF");
expect(formatMeetingDays(makeMeetingTime({ tuesday: true, saturday: true }))).toBe("TSa");
expect(
formatMeetingDays(makeMeetingTime({ wednesday: true, friday: true, sunday: true }))
).toBe("WFSu");
expect(
formatMeetingDays(
makeMeetingTime({ monday: true, tuesday: true, wednesday: true, thursday: true })
)
).toBe("MTWTh");
});
it("returns empty string when no days", () => {
expect(formatMeetingDays(makeMeetingTime())).toBe("");
});
});
describe("formatTimeRange", () => {
it("elides AM when both times are AM", () => {
expect(formatTimeRange("0900", "0950")).toBe("9:009:50 AM");
});
it("elides PM when both times are PM", () => {
expect(formatTimeRange("1315", "1430")).toBe("1:152:30 PM");
});
it("keeps both markers when crossing noon", () => {
expect(formatTimeRange("1130", "1220")).toBe("11:30 AM12:20 PM");
});
it("returns TBA for null begin", () => {
expect(formatTimeRange(null, "0950")).toBe("TBA");
});
it("returns TBA for null end", () => {
expect(formatTimeRange("0900", null)).toBe("TBA");
});
it("handles midnight and noon", () => {
expect(formatTimeRange("0000", "0050")).toBe("12:0012:50 AM");
expect(formatTimeRange("1200", "1250")).toBe("12:0012:50 PM");
});
});
describe("formatMeetingTime", () => {
it("formats a standard meeting time with elided AM/PM", () => {
expect(
formatMeetingTime(
makeMeetingTime({
monday: true,
wednesday: true,
friday: true,
begin_time: "0900",
end_time: "0950",
})
)
).toBe("MWF 9:009:50 AM");
});
it("keeps both markers when crossing noon", () => {
expect(
formatMeetingTime(
makeMeetingTime({
tuesday: true,
thursday: true,
begin_time: "1130",
end_time: "1220",
})
)
).toBe("TTh 11:30 AM12:20 PM");
});
it("returns TBA when no days", () => {
expect(formatMeetingTime(makeMeetingTime({ begin_time: "0900", end_time: "0950" }))).toBe(
"TBA"
);
});
it("returns days + TBA when no times", () => {
expect(formatMeetingTime(makeMeetingTime({ monday: true }))).toBe("Mon TBA");
});
});
describe("abbreviateInstructor", () => {
it("returns short names unabbreviated", () =>
expect(abbreviateInstructor("Li, Bo")).toBe("Li, Bo"));
it("returns names within budget unabbreviated", () =>
expect(abbreviateInstructor("Heaps, John")).toBe("Heaps, John"));
it("handles no comma", () => expect(abbreviateInstructor("Staff")).toBe("Staff"));
// Progressive abbreviation with multiple given names
it("abbreviates trailing given names first", () =>
expect(abbreviateInstructor("Ramirez, Maria Elena")).toBe("Ramirez, Maria E."));
it("abbreviates all given names when needed", () =>
expect(abbreviateInstructor("Ramirez, Maria Elena", 16)).toBe("Ramirez, M. E."));
it("falls back to first initial only", () =>
expect(abbreviateInstructor("Ramirez, Maria Elena", 12)).toBe("Ramirez, M."));
// Single given name that exceeds budget
it("abbreviates single given name when over budget", () =>
expect(abbreviateInstructor("Bartholomew, Christopher", 18)).toBe("Bartholomew, C."));
// Respects custom maxLen
it("keeps full name when within custom budget", () =>
expect(abbreviateInstructor("Ramirez, Maria Elena", 30)).toBe("Ramirez, Maria Elena"));
it("always abbreviates when budget is tiny", () =>
expect(abbreviateInstructor("Heaps, John", 5)).toBe("Heaps, J."));
});
describe("getPrimaryInstructor", () => {
it("returns primary instructor", () => {
const instructors: InstructorResponse[] = [
{
bannerId: "1",
displayName: "A",
email: null,
isPrimary: false,
rmpRating: null,
rmpNumRatings: null,
},
{
bannerId: "2",
displayName: "B",
email: null,
isPrimary: true,
rmpRating: null,
rmpNumRatings: null,
},
];
expect(getPrimaryInstructor(instructors)?.displayName).toBe("B");
});
it("returns first instructor when no primary", () => {
const instructors: InstructorResponse[] = [
{
bannerId: "1",
displayName: "A",
email: null,
isPrimary: false,
rmpRating: null,
rmpNumRatings: null,
},
];
expect(getPrimaryInstructor(instructors)?.displayName).toBe("A");
});
it("returns undefined for empty array", () => {
expect(getPrimaryInstructor([])).toBeUndefined();
});
});
describe("formatCreditHours", () => {
it("returns creditHours when set", () => {
expect(
formatCreditHours({
creditHours: 3,
creditHourLow: null,
creditHourHigh: null,
} as CourseResponse)
).toBe("3");
});
it("returns range when variable", () => {
expect(
formatCreditHours({
creditHours: null,
creditHourLow: 1,
creditHourHigh: 3,
} as CourseResponse)
).toBe("13");
});
it("returns dash when no credit info", () => {
expect(
formatCreditHours({
creditHours: null,
creditHourLow: null,
creditHourHigh: null,
} as CourseResponse)
).toBe("—");
});
});
describe("isMeetingTimeTBA", () => {
it("returns true when no days set", () => {
expect(isMeetingTimeTBA(makeMeetingTime())).toBe(true);
});
it("returns false when any day is set", () => {
expect(isMeetingTimeTBA(makeMeetingTime({ monday: true }))).toBe(false);
});
it("returns false when multiple days set", () => {
expect(isMeetingTimeTBA(makeMeetingTime({ tuesday: true, thursday: true }))).toBe(false);
});
});
describe("isTimeTBA", () => {
it("returns true when begin_time is null", () => {
expect(isTimeTBA(makeMeetingTime())).toBe(true);
});
it("returns true when begin_time is empty", () => {
expect(isTimeTBA(makeMeetingTime({ begin_time: "" }))).toBe(true);
});
it("returns true when begin_time is short", () => {
expect(isTimeTBA(makeMeetingTime({ begin_time: "09" }))).toBe(true);
});
it("returns false when begin_time is valid", () => {
expect(isTimeTBA(makeMeetingTime({ begin_time: "0900" }))).toBe(false);
});
});
describe("formatDate", () => {
it("formats standard date", () => {
expect(formatDate("2024-08-26")).toBe("August 26, 2024");
});
it("formats December date", () => {
expect(formatDate("2024-12-12")).toBe("December 12, 2024");
});
it("formats January 1st", () => {
expect(formatDate("2026-01-01")).toBe("January 1, 2026");
});
it("formats MM/DD/YYYY date", () => {
expect(formatDate("01/20/2026")).toBe("January 20, 2026");
});
it("formats MM/DD/YYYY with May", () => {
expect(formatDate("05/13/2026")).toBe("May 13, 2026");
});
it("returns original string for invalid input", () => {
expect(formatDate("bad-date")).toBe("bad-date");
});
});
describe("formatMeetingDaysLong", () => {
it("returns full plural for single day", () => {
expect(formatMeetingDaysLong(makeMeetingTime({ thursday: true }))).toBe("Thursdays");
});
it("returns full plural for Monday only", () => {
expect(formatMeetingDaysLong(makeMeetingTime({ monday: true }))).toBe("Mondays");
});
it("returns semi-abbreviated for multiple days", () => {
expect(
formatMeetingDaysLong(makeMeetingTime({ monday: true, wednesday: true, friday: true }))
).toBe("Mon, Wed, Fri");
});
it("returns semi-abbreviated for TR", () => {
expect(formatMeetingDaysLong(makeMeetingTime({ tuesday: true, thursday: true }))).toBe(
"Tue, Thur"
);
});
it("returns empty string when no days", () => {
expect(formatMeetingDaysLong(makeMeetingTime())).toBe("");
});
});
describe("formatDateShort", () => {
it("formats YYYY-MM-DD to short", () => {
expect(formatDateShort("2024-08-26")).toBe("Aug 26, 2024");
});
it("formats MM/DD/YYYY to short", () => {
expect(formatDateShort("12/12/2024")).toBe("Dec 12, 2024");
});
it("returns original for invalid", () => {
expect(formatDateShort("bad")).toBe("bad");
});
});
describe("formatMeetingDaysVerbose", () => {
it("returns plural for single day", () => {
expect(formatMeetingDaysVerbose(makeMeetingTime({ thursday: true }))).toBe("Thursdays");
});
it("joins two days with ampersand", () => {
expect(formatMeetingDaysVerbose(makeMeetingTime({ tuesday: true, thursday: true }))).toBe(
"Tuesdays & Thursdays"
);
});
it("uses Oxford-style ampersand for 3+ days", () => {
expect(
formatMeetingDaysVerbose(makeMeetingTime({ monday: true, wednesday: true, friday: true }))
).toBe("Mondays, Wednesdays & Fridays");
});
it("returns empty string when no days", () => {
expect(formatMeetingDaysVerbose(makeMeetingTime())).toBe("");
});
});
describe("formatMeetingTimeTooltip", () => {
it("formats full tooltip with location and dates", () => {
const mt = makeMeetingTime({
tuesday: true,
thursday: true,
begin_time: "1615",
end_time: "1730",
building_description: "Main Hall",
room: "2.206",
});
expect(formatMeetingTimeTooltip(mt)).toBe(
"Tuesdays & Thursdays, 4:155:30 PM\nMain Hall 2.206, Aug 26, 2024 Dec 12, 2024"
);
});
it("handles TBA days and times", () => {
expect(formatMeetingTimeTooltip(makeMeetingTime())).toBe("TBA\nAug 26, 2024 Dec 12, 2024");
});
it("handles days with TBA times", () => {
expect(formatMeetingTimeTooltip(makeMeetingTime({ monday: true }))).toBe(
"Mondays, TBA\nAug 26, 2024 Dec 12, 2024"
);
});
});
describe("formatMeetingTimesTooltip", () => {
it("returns TBA for empty array", () => {
expect(formatMeetingTimesTooltip([])).toBe("TBA");
});
it("joins multiple meetings with blank line", () => {
const mts = [
makeMeetingTime({
monday: true,
wednesday: true,
friday: true,
begin_time: "0900",
end_time: "0950",
}),
makeMeetingTime({
thursday: true,
begin_time: "1300",
end_time: "1400",
building_description: "Lab",
room: "101",
}),
];
const result = formatMeetingTimesTooltip(mts);
expect(result).toContain("Mondays, Wednesdays & Fridays, 9:009:50 AM");
expect(result).toContain("Thursdays, 1:002:00 PM\nLab 101");
expect(result).toContain("\n\n");
});
});