Files
psu-schedule/test/directory.test.js

198 lines
11 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 { readFileSync } from "node:fs";
import { parseDocx, details } from "../server/parser.js";
import { parseWeeksDocx } from "../server/calendar.js";
import { mergeWeekTables } from "../server/source.js";
import {
peopleAndRooms,
buildIndex,
occurrence,
search,
dayOf,
freeRooms,
examsOf,
isPhysicalRoom,
} from "../server/directory.js";
const fixture = (name) =>
readFileSync(new URL(`./fixtures/${name}.docx`, import.meta.url));
const calendar = {
...mergeWeekTables([parseWeeksDocx(fixture("weeks"))]),
bells: [
{ pair: 1, start: "08:30", end: "09:55", halves: [] },
{ pair: 2, start: "10:05", end: "11:30", halves: [] },
{ pair: 3, start: "11:45", end: "13:10", halves: [] },
],
source: {},
};
const doc = (id, schedule) => ({ id, file: { id, name: `${id}.docx` }, schedule });
const fef = doc("fef", parseDocx(fixture("fef-23-ba"), { name: "23-ЛГ_23-БА.docx", faculty: "ФЭФ" }));
const rooms = (raw, room = "") => peopleAndRooms({ raw, room, teacher: "" }, "2026");
test("Аудитории: реальные формулировки из Word", () => {
assert.deepEqual(rooms("Охрана труда (л.з.) Лемачко С.Н. ауд.205;6,8н.- Горунова С.А.", "205").rooms, ["205"]);
const moved = rooms("История (л.) Сумко Е.В. ауд. 432 14.10. -ауд. 333", "432");
assert.deepEqual(moved.rooms, ["432"]);
assert.deepEqual(moved.overrides, [{ date: "2026-10-14", room: "333" }]);
assert.deepEqual(rooms("Материаловедение (л.) Фруцкий В.А. ауд. 106н 17.10. -210 мк").overrides, [
{ date: "2026-10-17", room: "210МК" },
]);
// A typo glues the room to the date: "22417.09." is room 224 and a move on 17 September.
const glued = rooms("Информационные технологии (л.) Моняк Т.М. ауд. 22417.09.- ауд.332", "22417");
assert.deepEqual([glued.rooms, glued.overrides[0].room], [["224"], "332"]);
assert.deepEqual(rooms("Единый день информирования 10.09;15.10;19.11;17.12", "12").rooms, []);
assert.deepEqual(rooms("Информационный час ауд. В0-05", "05").rooms, ["В0-05"]);
assert.deepEqual(rooms("Физкультура ауд. 526н325н502н").rooms, ["526Н", "325Н", "502Н"]);
assert.deepEqual(rooms("Предмет (л.) Иванов И.И. ауд.129, Петров П.П. ауд. 130").teachers, [
"Иванов И.И.",
"Петров П.П.",
]);
assert.equal(rooms("Линейная алгебра (л.)\nЗабелендик О.Н.\n200 (ДО)", "200 (ДО)").distance, true);
assert.equal(rooms("Сети (л.) Лукьянов А.О. 200ДО").rooms.length, 0);
assert.deepEqual(rooms("История (л) Емельянчик О.А. 21/28 (дискозал)").rooms, ["21/28"]);
assert.ok(isPhysicalRoom("207Г") && isPhysicalRoom("А1-15") && !isPhysicalRoom("3"));
});
test("Занятость по дате учитывает недели, цвет строки и семестр", () => {
const idx = buildIndex([fef]);
const find = (subject, day) =>
idx.entries.find((e) => e.lesson.subject === subject && e.lesson.day === day && e.lesson.groups.includes("23-БА"));
// Friday 16 October 2026 is week 7: "1-4,6-11н." runs, "12-15н." does not.
assert.equal(occurrence(find("PR-менеджмент", 5), "2026-10-16", calendar), "yes");
const later = idx.entries.find((e) => e.lesson.condition === "12-15н." && e.lesson.day === 5);
assert.equal(occurrence(later, "2026-10-16", calendar), "no");
assert.equal(occurrence(later, "2026-11-20", calendar), "yes");
// An autumn schedule does not run in March.
assert.equal(occurrence(find("PR-менеджмент", 5), "2027-03-05", calendar), "no");
// A spring document without a spring week table: nothing is guessed.
const spring = doc("spring", { ...fef.schedule, period: "Весенний семестр" });
const entry = { ...find("PR-менеджмент", 5), doc: spring };
assert.equal(occurrence(entry, "2027-03-05", calendar), "unknown");
});
test("Поиск, день преподавателя и свободные аудитории", () => {
const idx = buildIndex([fef]);
idx.docs = [fef];
idx.total = 1;
idx.failed = [];
idx.year = "2026–2027";
assert.equal(search(idx, "селез").teachers[0].name, "Селезнева А.А.");
assert.equal(search(idx, "4").teachers.length, 0);
const day = dayOf(idx, calendar, { teacher: "Селезнева А.А." }, "2026-10-16");
assert.deepEqual(day.lessons.map((l) => [l.start, l.subject, l.rooms.join()]), [
["08:30", "Инновационный менеджмент", "333"],
]);
const room = dayOf(idx, calendar, { room: "129" }, "2026-10-16");
assert.ok(room.lessons.some((l) => l.subject === "PR-менеджмент"));
const free = freeRooms(idx, calendar, "2026-10-16", 2);
const freeList = free.free.flatMap((g) => g.rooms);
assert.ok(!freeList.includes("129"), "PR-менеджмент занимает 129 на 2-й паре");
assert.ok(freeList.includes("333"), "333 занята только на 1-й паре");
assert.equal(free.coverage.documents, 1);
assert.equal(freeRooms(idx, calendar, "2026-10-16", 9), null);
});
test("Экзамены, зачёты и консультации распознаются и собираются по группе", () => {
assert.deepEqual(
["Экзамен. Финансы организаций Иванов И.И. ауд.129", "Финансы (экз.) Иванов И.И.", "Зачет: Физическая культура Петров П.П.", "Консультация Финансы Иванов И.И."]
.map((raw) => [details(raw).type, details(raw).subject]),
[
["Экзамен", "Финансы организаций"],
["Экзамен", "Финансы"],
["Зачёт", "Физическая культура"],
["Консультация", "Финансы"],
],
);
assert.equal(details("Психология профессионального консультирования (л.) Сидоров С.С.").type, "Лекция");
const exam = (date, start, type, subject) => ({
id: date + start,
day: 1,
date,
start,
end: null,
subject,
teacher: "Иванов И.И.",
room: "129",
type,
condition: "",
groups: ["23-БА"],
subgroups: [],
variant: "all",
raw: `${type} ${subject} Иванов И.И. ауд.129`,
});
const session = doc("session", {
lessons: [
exam("2027-01-14", "09:00", "Экзамен", "Финансы"),
exam("2027-01-13", "15:00", "Консультация", "Финансы"),
exam("2027-01-14", "09:00", "Экзамен", "Финансы"),
{ ...exam("2027-01-20", "09:00", "Экзамен", "Маркетинг"), groups: ["23-ЛГ"] },
],
source: { faculty: "ФЭФ" },
period: "Экзаменационная сессия",
});
const idx = buildIndex([fef, session]);
const list = examsOf(idx, "23-БА", "ФЭФ");
assert.deepEqual(list.map((e) => [e.date, e.type, e.subject, e.rooms.join()]), [
["2027-01-13", "Консультация", "Финансы", "129"],
["2027-01-14", "Экзамен", "Финансы", "129"],
]);
assert.equal(examsOf(idx, "23-БА", "ФИТ").length, 0);
});
test("Поиск группы: любое написание, без учёта фильтров, с фасетами каталога", async () => {
const { findGroups, groupKey } = await import("../server/directory.js");
const fit = parseDocx(fixture("fit-daily"), { name: "24-ИТ.docx" });
const docs = [
{ id: "fef", file: { id: "fef", name: "23-ЛГ_23-БА.docx", faculty: "ФЭФ", course: "4 курс", form: "Дневная форма" }, schedule: fef.schedule },
{ id: "fit", file: { id: "fit", name: "24-ИТ.docx", faculty: "ФИТ", course: "3 курс", form: "Дневная форма" }, schedule: fit },
];
const idx = { docs, year: "2026–2027" };
assert.equal(groupKey("23 ба"), groupKey("23-БА"));
for (const q of ["23-БА", "23 ба", "23ба"])
assert.deepEqual(findGroups(idx, { q }).results.map((r) => r.group), ["23-БА"]);
// The query ignores the faculty filter: a student must find their group anywhere.
assert.deepEqual(
findGroups(idx, { q: "24-ит", faculty: "ФЭФ" }).results.map((r) => r.group),
["24-ИТ-1", "24-ИТ-2"],
);
const browse = findGroups(idx, { faculty: "ФИТ" });
assert.deepEqual(browse.results.map((r) => [r.group, r.fileId]), [
["24-ИТ-1", "fit"],
["24-ИТ-2", "fit"],
]);
assert.deepEqual(browse.facets.faculties.map((f) => f.name), ["ФИТ", "ФЭФ"]);
assert.deepEqual(browse.facets.courses.map((c) => c.name), ["3 курс"]);
assert.equal(findGroups(idx, {}).results.length, 0);
assert.equal(findGroups(idx, {}).total, 4);
});
test("Выбор группы: зачёты не дублируют группу с основным расписанием", async () => {
const { findGroups } = await import("../server/directory.js");
const credits = parseDocx(fixture("isf-credits-26-arh"), { name: "26-АРХ.docx" });
const main = { ...credits, period: "Осенний семестр" };
const doc = (id, schedule) => ({ id, file: { id, name: "26-АРХ.docx", faculty: "ИСФ", course: "1 курс" }, schedule });
// With a semester timetable the credits stay in the session countdown, not in the picker.
const both = findGroups({ docs: [doc("main", main), doc("credits", credits)] }, { q: "26-АРХ" });
assert.deepEqual(both.results.map((r) => [r.group, r.fileId]), [["26-АРХ", "main"]]);
assert.equal(both.total, 1);
// A group with only a credit schedule can still open it.
const only = findGroups({ docs: [doc("credits", credits)] }, { q: "26-АРХ" });
assert.deepEqual(only.results.map((r) => [r.group, r.fileId, r.period]), [["26-АРХ", "credits", "Зачёты"]]);
});
test("Выбор группы: прошедший документ с датами не дублирует основное расписание", async () => {
const { findGroups } = await import("../server/directory.js");
// A document dated 8–27 June 2026, like an orientation week, beside the group's semester timetable.
const dated = parseDocx(fixture("itzs-summer-2026"), { name: "23-ИТзс.docx" });
const main = { ...dated, period: "Осенний семестр", lessons: dated.lessons.map(({ date, ...l }) => l) };
const doc = (id, schedule) => ({ id, file: { id, name: `${id}.docx` }, schedule });
const idx = { docs: [doc("main", main), doc("dated", dated)] };
const ids = (today) => findGroups(idx, { q: "23-ИТзс", today }).results.map((r) => r.fileId);
assert.deepEqual(ids("2026-10-19"), ["main"]);
// Before it is over, both are offered.
assert.deepEqual(ids("2026-06-20").sort(), ["dated", "main"]);
// Alone, even a finished one stays: it is all the group has.
assert.deepEqual(findGroups({ docs: [doc("dated", dated)] }, { q: "23-ИТзс", today: "2026-10-19" }).results.length, 1);
});