Files
psu-schedule/test/formats.test.js
Zakhar Timoshenko 297091cb0e feat: Add Settings and Subjects screens with theme reveal functionality
- Implemented SettingsScreen for user preferences including theme selection, reminders, and notifications.
- Created SubjectsScreen to display subjects with upcoming lessons and associated details.
- Introduced ThemeReveal for a smooth transition between light and dark themes.
- Added support for managing notes and tasks related to subjects.
- Enhanced UI components for better user experience and accessibility.
2026-10-24 02:47:37 +03:00

189 lines
16 KiB
JavaScript
Raw Permalink 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 { test } from "node:test";
import assert from "node:assert/strict";
import { groupsIn, alternatives, entries, details, leadingTime, parallel, audienceOf, clean } from "../server/parser.js";
import { placeOf } from "../server/places.js";
import { roomMoves, weekPlan, onlyPlace } from "../server/rooms.js";
import { peopleAndRooms, roomsOn, teachersOn, movedOn } from "../server/directory.js";
// Cell formats found by the catalog's diagnostics (October 2026).
test("Группы с направлением в скобках: 24-ИЯ (РА)и-1 … и-6 — разные группы", () => {
assert.deepEqual(groupsIn("24-ИЯ (РА)и-1 | 24-ИЯ (РА)и-2 | 24-ИЯ (РА)и-3"), ["24-ИЯ (РА)и-1", "24-ИЯ (РА)и-2", "24-ИЯ (РА)и-3"]);
assert.deepEqual(groupsIn("25 ИЯ (РА)и-1"), ["25-ИЯ (РА)и-1"]);
assert.deepEqual(groupsIn("24-РГФи-1 26-ИТ-2"), ["24-РГФи-1", "26-ИТ-2"]);
});
test("Перенос аудитории на дату, и с годом: «412н 26.10.2026-333»", () => {
assert.deepEqual(roomMoves("Типология (л.) Полякова Г.В. 412н 26.10.2026-333", 2026).moves, [{ date: "2026-10-26", room: "333" }]);
assert.deepEqual(roomMoves("Язык (п.з.) Горбач М.Н. 510н\n28.10.2026-504н", 2026).moves, [{ date: "2026-10-28", room: "504Н" }]);
assert.deepEqual(roomMoves("ауд. 432 14.10. -ауд. 333", 2026).moves, [{ date: "2026-10-14", room: "333" }]);
// The parser keeps the usual room; the move goes to that date only.
const lesson = { raw: "Типология (л.) Полякова Г.В. 412н 26.10.2026-333", room: "412н", teacher: "Полякова Г.В." };
const entry = peopleAndRooms(lesson, "2026");
assert.deepEqual(roomsOn(entry, "2026-10-26"), ["333"]);
assert.equal(movedOn(entry, "2026-10-26"), true);
assert.deepEqual(roomsOn(entry, "2026-10-19"), ["412Н"]);
assert.equal(movedOn(entry, "2026-10-19"), false);
});
test("Строка с аудиторией по неделям продолжает занятие, а не становится новым", () => {
const [one, ...rest] = entries("Язык профессионального общения (англ) (п.з.) Шабунина О.А.\n1-9н. 502н\nс 10н -359");
assert.equal(rest.length, 0);
assert.equal(details(one.main).subject, "Язык профессионального общения (англ)");
assert.deepEqual(one.plan, [
{ weeks: [[1, 9]], color: null, rooms: ["502Н"], teachers: [] },
{ weeks: [[10, null]], color: null, rooms: ["359"], teachers: [] },
]);
const entry = peopleAndRooms({ raw: one.text, room: "", teacher: "Шабунина О.А.", plan: one.plan }, "2026");
assert.deepEqual(roomsOn(entry, "2026-10-20", { n: 8, color: "green" }), ["502Н"]);
assert.deepEqual(roomsOn(entry, "2026-11-03", { n: 10, color: "green" }), ["359"]);
});
test("Преподаватель по неделям и аудитория по цвету недели", () => {
const teachers = weekPlan("2-9н.-Клос О.Ю., 10-14н.-Конторов В.Э. 319Д");
// The room at the end is the room of both parts.
assert.deepEqual(teachers, [
{ weeks: [[2, 9]], color: null, rooms: ["319Д"], teachers: ["Клос О.Ю."] },
{ weeks: [[10, 14]], color: null, rooms: ["319Д"], teachers: ["Конторов В.Э."] },
]);
const entry = peopleAndRooms({ raw: "2-14н. История литературы (п.з.)\n2-9н.-Клос О.Ю., 10-14н.-Конторов В.Э. 319Д", room: "", teacher: "", plan: teachers }, "2026");
assert.deepEqual(teachersOn(entry, { n: 3, color: "white" }), ["Клос О.Ю."]);
assert.deepEqual(teachersOn(entry, { n: 12, color: "green" }), ["Конторов В.Э."]);
const [lesson] = entries("с 10н. Дискурсивная практика (рус) (п.з.) Булах А.С.\nбел.н.504н/зел.н.524н");
const byColor = peopleAndRooms({ raw: lesson.text, room: "", teacher: "", plan: lesson.plan }, "2026");
assert.deepEqual(roomsOn(byColor, "2026-11-10", { n: 11, color: "white" }), ["504Н"]);
assert.deepEqual(roomsOn(byColor, "2026-11-17", { n: 12, color: "green" }), ["524Н"]);
// The colour after each room (25-ДО): "ауд.533н-б.н.,420н-з.н.".
const [after] = entries("Теоретические основы воспитания и развития речи детей раннего возраста (п.з.) Савейко В.Л. ауд.533н-б.н.,420н-з.н.");
assert.deepEqual(after.plan, [
{ weeks: null, color: "white", rooms: ["533Н"], teachers: ["Савейко В.Л."] },
{ weeks: null, color: "green", rooms: ["420Н"], teachers: ["Савейко В.Л."] },
]);
const roomAfter = peopleAndRooms({ raw: after.text, room: "533н", teacher: "Савейко В.Л.", plan: after.plan }, "2026");
assert.deepEqual(roomsOn(roomAfter, "2026-10-30", { n: 9, color: "white" }), ["533Н"]);
assert.deepEqual(roomsOn(roomAfter, "2026-10-23", { n: 8, color: "green" }), ["420Н"]);
assert.deepEqual(weekPlan("ауд. 420н з.н., 533н б.н.").map((p) => [p.color, p.rooms]), [["white", ["533Н"]], ["green", ["420Н"]]]);
assert.equal(onlyPlace("533н-б.н.,420н-з.н."), true);
// "32н" before its colour is a room, not week 32.
assert.deepEqual(weekPlan("Коррекционная дошкольная педагогика (л.) Остапчук С.В. ауд.64н -б.н., 32н-з.н.").map((p) => [p.color, p.rooms]), [["white", ["64Н"]], ["green", ["32Н"]]]);
assert.equal(onlyPlace("1-9н. 502н"), true);
assert.equal(onlyPlace("13,15 н. РБ (л.з) Сивацкий Д.С.209н"), false);
});
test("Строка с видом и неделями без предмета — тот же предмет в другие недели", () => {
const list = entries("2-6н. Потребительское право (л.) Савицкая К.Д. ауд. А1-06\n7,9,11н. – (п.з.) Савицкая К.Д. ауд. А1-06 8,12,14н. – (п.з.) Савицкая К.Д. ауд. А1-06");
assert.deepEqual(list.map((e) => [details(e.main).condition, details(e.main).subject, details(e.main).type]), [
["2-6н.", "Потребительское право", "Лекция"],
["7,9,11н.", "Потребительское право", "Практика"],
["8,12,14н.", "Потребительское право", "Практика"],
]);
assert.equal(details(list[0].main).room, "А1-06");
});
test("Дисциплины по выбору: заголовок, предмет перед неделями, виды по неделям", () => {
const read = (raw) => entries(raw).map((e) => ((d) => `${d.condition} ${d.subject} · ${d.type}`)(details(e.main)));
assert.deepEqual(read("Дисциплины по выбору: 1-16 н. Логика (л./п.з.) Магалинский И.В. ауд. С0-05\nСоциология 1-10 н. (л.) Шидловский С.О. 11-16 н. (п.з.) Лапацуева А.С.С0-03"), [
"1-16 н. Логика · Лекция и практика",
"1-10 н. Социология · Лекция",
"11-16 н. Социология · Практика",
]);
assert.deepEqual(read("Дисциплина по выбору: Социология ауд. С0-03\n1-10 н. (л.) Шидловский С.О.\n11-18 н. (п.з.) Лапацуева А.С."), [
"1-10 н. Социология · Лекция",
"11-18 н. Социология · Практика",
]);
assert.deepEqual(read("Дисциплина сцециализации: 4-14 н. Преступления в сфере информационных технологий (л.) Чижик Е.В. ауд. В0-05"), [
"4-14 н. Преступления в сфере информационных технологий · Лекция",
]);
assert.deepEqual(read("1-11н. - л., 12-14 – п.з. Защита населения Дмитриченко Н.С. ауд. 210м/к"), [
"1-11н. Защита населения · Лекция",
"12-14н. Защита населения · Практика",
]);
});
test("Время в начале ячейки: интервал с дефисом, заголовок со временем", () => {
assert.deepEqual(leadingTime("17:00-17:20-информационный час-Клос О.Ю. 319Д").starts, ["17:00"]);
assert.equal(leadingTime("17:00-17:20-информационный час").end, "17:20");
assert.deepEqual(alternatives("18.35+20.10 Новополоцк:\n1-5 н. Судебно-следственная психология (л.) Колесникова Е.В. ауд. 246"), [
"18.35+20.10 1-5 н. Судебно-следственная психология (л.) Колесникова Е.В. ауд. 246",
]);
});
test("Подгруппа в тексте ячейки, точка вместо занятия, заголовок заглавными", () => {
const own = details("12н.2п/гр. Экономика организаций (л.з.) Лисиченок Е.П. ауд.331");
assert.equal(own.subject, "Экономика организаций");
assert.equal(own.ownSubgroup, 2);
assert.deepEqual(entries("."), []);
assert.equal(details("ВВЕДЕНИЕ В ВЫСШЕЕ ОБРАЗОВАНИЕ\nг. Полоцк, Полоцкий коллегиум\n26-ГЕО, 26-ИТ-1 Ректорский вход\n26-ИТ-2, 26-СТ обзорная экскурсия").subject, "ВВЕДЕНИЕ В ВЫСШЕЕ ОБРАЗОВАНИЕ");
// A two-line subject stays whole.
assert.equal(details("Интеллектуальная собственность - драйвер инновационного развития\nна пространстве Евразии").subject.includes("Евразии"), true);
});
test("Немецкий или французский: два занятия в одной ячейке на всю группу", () => {
const cell = "Немецкий язык (п.з) – Найверт Ю.О. 208А,\nФранцузский язык (п.з.) – Глобенко М.В. 336Е";
const list = entries(cell);
assert.deepEqual(list.map((e) => details(e.main).room), ["208А", "336Е"]);
assert.deepEqual(list.map((e) => details(e.main).teacher), ["Найверт Ю.О.", "Глобенко М.В."]);
assert.deepEqual(list[0].choice, ["Немецкий язык", "Французский язык"]);
// A room on its own line stays with its lesson.
assert.deepEqual(
parallel("Теория языка (англ.) (л.) (кроме иностр.студ.)-Чеботарева Е.М.305Г\nИстория языка (англ.) (л) (для иностр.студ.) Третьяк З.И.\n321Д").at(-1),
"История языка (англ.) (л) (для иностр.студ.) Третьяк З.И.\n321Д",
);
// A lesson with its teacher on the next line is one lesson.
assert.equal(entries("Русский язык как иностранный (п.з.)\nНемирская В.А. 305Г").length, 1);
// The comma before the next week's lesson is no part of the room.
assert.equal(details(entries("2-13н. Белорусский язык (п.з.) - Третьяк З.И. 331Е,\n2-13н. Стилистика (п.з.) – Коротких А.В. 301Г")[0].main).room, "331Е");
});
test("Пары для иностранных студентов и для остальных", () => {
assert.equal(audienceOf("История языка (п.з.) (кроме иностр.студ.) Ковшик О.С. 305Г"), "local");
assert.equal(audienceOf("Русский язык как иностранный (п.з.) Немирская В.А. 305Г"), "foreign");
assert.equal(audienceOf("Современный разговорный иностранный язык (на базе второго иностранного языка)"), null);
// Russian for foreign students while the others have physical education, in every spelling.
const pe = (text) => entries(text).map((e) => [details(e.main).subject, e.audience]);
assert.deepEqual(pe("Ф и з и ч е с к а я к у л ь т у р а\nРусский язык как иностранный (для иностранных студентов) Семченко Е.Д. ауд.313н"), [
["Ф и з и ч е с к а я к у л ь т у р а", "local"],
["Русский язык как иностранный (для иностранных студентов)", "foreign"],
]);
assert.deepEqual(pe("со 2 нед. Русский язык как иностранный (п.з.) Немирская В.А. 331Е со 2 нед. ФИЗИЧЕСКАЯ КУЛЬТУРА").map((p) => p[1]), ["foreign", "local"]);
assert.equal(details(entries("Русский язык как иностранный (п.з.) Немирская В.А. 331Е ФИЗИЧЕСКАЯ КУЛЬТУРА")[0].main).room, "331Е");
// The town or the teacher on a line of its own is no lesson for the others.
assert.equal(entries("17.00 Новополоцк\nРусский язык как иностранный (п.з.)\nКириллова В.С. ауд. 537н").length, 1);
assert.equal(entries("Русский язык как иностранный (для иностранных студентов)\nХаткевич Т.В. ауд. 323н").length, 1);
});
test("Вид занятия после преподавателя: фамилия не входит в название", () => {
const after = details("Русский язык как иностранный Козыренко А.Н.(п.з.) ауд.100");
assert.equal(after.subject, "Русский язык как иностранный");
assert.equal(after.type, "Практика");
assert.equal(details("Русский язык как иностранный\nХаткевич Т.В. (п.з.) ауд.504н").subject, "Русский язык как иностранный");
// No subject at all: the cell's text stays visible.
assert.equal(details("(л.з.) Алексеева Т.А. ауд. 33н").subject, "(л.з.) Алексеева Т.А. ауд. 33н");
});
test("Разрядка, «со 2 по 14 нед.», город с середины дня", () => {
// Letters spaced out for show, words apart by several spaces.
assert.equal(clean("Ф и з и ч е с к а я к у л ь т у р а"), "Физическая культура");
assert.equal(clean("З а н я т и я в П о л о ц к е"), "Занятия в Полоцке");
assert.equal(clean("со 2 по 14 нед. М о д у л ь « М а й н о р »"), "2-14 нед. Модуль «Майнор»");
// Initials and short words stay as they are.
assert.equal(clean("Борун Е.Н. ауд. 100"), "Борун Е.Н. ауд. 100");
// "Занятия в Полоцке" in the 13:40 row: the morning is not in Polotsk.
const places = [{ day: 5, date: null, groups: ["25-ИТ-1"], subgroups: [], place: "Полоцк", from: "13:40" }];
const at = (start) => placeOf({ day: 5, date: null, start, groups: ["25-ИТ-1"], subgroups: [] }, places, () => []);
assert.equal(at("08:30"), null);
assert.equal(at("15:20"), "Полоцк");
});
test("Старое имя разделённой группы: понятная ошибка, другое написание — та же группа", async () => {
const { timetable } = await import("../server/timetable.js");
const schedule = { groups: ["24-ИЯ (РА)и-1", "24-ИЯ (РА)и-2"], lessons: [], notes: [], period: "Осенний семестр", source: {} };
const file = { id: "doc", name: "24-ИЯ (РА)и-1_24-ИЯ (РА)и-2.docx" };
assert.throws(
() => timetable({ file, schedule, calendar: null, group: "24-ИЯ(РА)", from: "2026-10-19" }),
(e) => e.code === "group_not_found" && /разделена: 24-ИЯ \(РА\)и-1, 24-ИЯ \(РА\)и-2/.test(e.message),
);
assert.equal(timetable({ file, schedule, calendar: null, group: "24-ИЯ(РА)и-1", from: "2026-10-19" }).group, "24-ИЯ (РА)и-1");
});