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

239 lines
11 KiB
JavaScript
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 test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { groupsIn, parseDocx, parseWord } from "../server/parser.js";
import { decodeListing, fileMetadata } from "../server/source.js";
const fixture = (name) =>
readFileSync(new URL(`./fixtures/${name}.docx`, import.meta.url));
const fef = () => parseDocx(fixture("fef-23-ba"), { name: "23-ЛГ_23-БА.docx" });
test("ФЭФ: 23-БА не получает дисциплины 23-ЛГ", () => {
const s = fef(),
ba = s.lessons.filter((l) => l.groups.includes("23-БА"));
assert.deepEqual(s.groups, ["23-ЛГ", "23-БА"]);
assert.equal(ba.length, 33);
assert.ok(ba.some((l) => l.subject === "Бизнес-анализ"));
assert.ok(
!ba.some((l) =>
/логистик|Экспедирование|Управление запасами/i.test(l.subject),
),
);
assert.ok(
s.lessons
.filter((l) => l.groups.includes("23-ЛГ"))
.some((l) => /Финансовая логистика/.test(l.subject)),
);
});
test("ФЭФ: общая ячейка принадлежит обеим группам", () => {
const shared = fef().lessons.filter((l) =>
l.subject.startsWith("Единый день информирования"),
);
assert.equal(shared.length, 1);
assert.deepEqual(shared[0].groups, ["23-ЛГ", "23-БА"]);
assert.match(shared[0].raw, /15 октября/);
assert.equal(shared[0].start, "13:40");
});
test("ФЭФ: варианты недель, цвет, преподаватель и аудитория сохранены", () => {
const s = fef();
const lessons = s.lessons.filter(
(l) =>
l.day === 4 &&
l.groups.includes("23-БА") &&
l.subject === "Бизнес-анализ",
);
assert.deepEqual(
lessons.map((l) => [l.condition, l.variant, l.room, l.teacher]),
[
["7,9,11н.", "upper", "430", "Войтенок Ж.Е."],
["4-12н", "lower", "430", "Войтенок Ж.Е."],
],
);
assert.equal(lessons[1].sourceColor, "CCFFCC");
assert.equal(lessons[0].start, "10:05");
assert.equal(lessons[0].end, "11:30");
const plan = s.lessons.find(
(l) =>
l.subject === "Бизнес-планирование" && l.start === "11:45" && l.day === 4,
);
assert.equal(plan.condition, "1-8,10-17");
assert.ok(s.warnings.length);
});
test("ФЭФ: кураторский час в 16.50 не становится невозможной датой", () => {
const l = fef().lessons.find(
(l) =>
l.groups.includes("23-БА") &&
l.day === 1 &&
l.subject.includes("кураторский"),
);
assert.equal(l.start, "16:50");
assert.equal(l.end, null);
assert.equal(l.date, null);
assert.equal(l.condition, "");
for (const lesson of fef().lessons)
if (lesson.date)
assert.ok(Number.isFinite(new Date(lesson.date).getTime()));
});
test("ФЭФ: объединённое занятие не дублируется на зелёной строке", () => {
const s = fef();
const l = s.lessons.filter(
(l) =>
l.day === 2 &&
l.groups.includes("23-БА") &&
l.subject === "Управление проектами" &&
l.start === "15:20",
);
assert.equal(l.length, 1);
assert.equal(l[0].variant, "all");
});
test("Дневные таблицы четырёх факультетов распознаются со своими группами", () => {
for (const [name, groups] of [
["fit-daily", ["24-ИТ-1", "24-ИТ-2"]],
["fkne-daily", ["24-КБ", "24-КФ"]],
["isf-daily", ["26-ПГС-1", "26-ПГС-2"]],
["gf-daily", ["25-РГФ-1", "25-РГФ-2"]],
]) {
const s = parseDocx(fixture(name), { name });
assert.deepEqual(s.groups, groups);
assert.ok(s.lessons.length > 20);
assert.ok(
s.lessons.every(
(l) => l.day >= 1 && l.day <= 7 && /^\d{2}:\d{2}$/.test(l.start),
),
);
assert.ok(
s.lessons.every((l) => l.groups.every((g) => groups.includes(g))),
);
}
});
test("Даты практики в заголовке группы не становятся группами", () => {
// Заголовки из каталога 2026–2027 (ГФ и ФКНЭ).
assert.deepEqual(
groupsIn("23-ДО\n(02.11-22.11-производственная практика)"),
["23-ДО"],
);
assert.deepEqual(
groupsIn("24-ПП\n(09.11 по 06.12-производственная практика)"),
["24-ПП"],
);
assert.deepEqual(
groupsIn("23-ТТиИ (16.11-29.11-учебная практика)"),
["23-ТТиИ"],
);
assert.deepEqual(groupsIn("25-ИЯ(РА) 23 ИНС-1 24ДОи"), [
"25-ИЯ(РА)",
"23-ИНС-1",
"24-ДОи",
]);
});
test("Сессии: отдельные колонки времени, аудитории и преподавателя", () => {
const s = parseDocx(fixture("fit"), { name: "26-ИТзс.docx" }),
l = s.lessons[0];
assert.deepEqual(s.groups, ["26-ИТзс"]);
assert.equal(l.date, "2026-09-17");
assert.equal(l.start, "11:00");
assert.equal(l.end, "11:30");
assert.equal(l.teacher, "Петрович О.Н.");
assert.equal(l.room, "220Д");
assert.deepEqual(l.subgroups, []);
assert.match(l.raw, /Петрович/);
assert.equal(s.period, "Установочная сессия");
const f = parseDocx(fixture("fkne"), {});
assert.deepEqual(f.groups, ["26-ЭЭТзс"]);
assert.ok(f.lessons.every((l) => l.date));
});
test("Импорт отклоняет повреждённые и посторонние файлы", async () => {
assert.throws(() => parseDocx(Buffer.from("broken")), /повреждён/);
await assert.rejects(
() => parseWord(Buffer.from("not a word file")),
/настоящий файл Word/,
);
});
test("DAV: метаданные вложенных документов и только доверенный каталог", () => {
const xml = `<d:multistatus xmlns:d="DAV:"><d:response><d:href>/public.php/dav/files/no5zKbMrns6j5jQ/2026-2027/</d:href><d:propstat><d:prop><d:resourcetype><d:collection/></d:resourcetype></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>https://evil.example/file.docx</d:href></d:response></d:multistatus>`;
const entries = decodeListing(xml);
assert.equal(entries.length, 1);
assert.equal(entries[0].folder, true);
const metadata = JSON.parse(
readFileSync(new URL("./fixtures/fef-23-ba.docx.json", import.meta.url)),
);
const file = fileMetadata(metadata);
assert.equal(file.faculty, "Финансово-экономический факультет");
assert.equal(file.course, "4 курс");
assert.equal(file.name, "23-ЛГ_23-БА.docx");
});
// Laboratory-examination sessions of correspondence students (23-ИТзс): weeks side by side,
// the group named only in the title, a date on every day.
const itzs = (name) => parseDocx(fixture(name), { name: `${name}.docx` });
const at = (s, date, start) =>
s.lessons
.filter((l) => l.date === date && l.start === start)
.map((l) => [l.type, l.subject, l.teacher, l.room, l.subgroups.map((g) => g.label).join()]);
test("Заочная сессия: группа из заголовка, недели рядом, даты из Word", () => {
const s = itzs("itzs-winter-2024");
assert.deepEqual(s.groups, ["23-ИТзс"]);
assert.equal(s.period, "Лабораторно-экзаменационная сессия");
// Every filled discipline cell of the three weeks is one lesson.
assert.equal(s.lessons.length, 36);
assert.ok(s.lessons.every((l) => l.date >= "2024-01-08" && l.date <= "2024-01-28"));
// Two columns under one heading are the subgroups' laboratory classes.
assert.deepEqual(at(s, "2024-01-08", "17:00"), [
["Лабораторная", "Основы информационных технологий и компьютерной графики", "Михнович О.В.", "217Д", "Подгруппа 1"],
["Лабораторная", "Основы алгоритмизации и программирования", "Конаплева Г.Ф.", "218Д", "Подгруппа 2"],
]);
assert.deepEqual(at(s, "2024-01-15", "17:00"), [
["Установочная лекция", "Математический анализ", "Забелендик О.Н.", "220Д", ""],
]);
// "Экзамен по учебной дисциплине «…»": the subject from the quotes, a room with a name.
assert.deepEqual(at(s, "2024-01-18", "08:30"), [
["Экзамен", "Основы алгоритмизации и программирования", "Чертков В.М.", "Медиа", ""],
]);
assert.deepEqual(at(s, "2024-01-16", "13:40"), [
["Защита курсовой работы", "Основы алгоритмизации и программирования", "Чертков В.М.", "254В", ""],
]);
});
test("Заочная сессия: неровное деление столбцов и экзамен без конца", () => {
const s = itzs("itzs-summer-2026");
assert.deepEqual(s.groups, ["23-ИТзс"]);
assert.equal(s.lessons.length, 58);
// The third week's heading spans three columns, split 2+1 or 1+2: left and right subgroup.
assert.deepEqual(at(s, "2026-06-24", "08:30").map((l) => [l[1], l[4]]), [
["Методы и алгоритмы принятия", "Подгруппа 1"],
["Распределенные вычисления", "Подгруппа 2"],
]);
// "8.30" alone: the exam starts then; its end comes from the bell table.
const exam = s.lessons.find((l) => l.date === "2026-06-18" && l.type === "Экзамен");
assert.deepEqual([exam.start, exam.end, exam.subject], ["08:30", null, "Базы данных"]);
assert.equal(s.lessons.filter((l) => l.type === "Консультация").length, 4);
});
test("Расписание зачетов: дата перед днём недели, одно время, зачёты по умолчанию", () => {
const s = parseDocx(fixture("isf-credits-26-arh"), { name: "26-АРХ.docx" });
assert.deepEqual(s.groups, ["26-АРХ"]);
assert.equal(s.period, "Зачёты");
assert.equal(s.lessons.length, 9);
const row = (l) => [l.date, l.start, l.end, l.type, l.subject, l.subgroups.map((g) => g.label).join()];
const sorted = [...s.lessons].sort((a, b) => (a.date + a.start).localeCompare(b.date + b.start));
assert.deepEqual(sorted.slice(0, 2).map(row), [
["2026-10-27", "17:00", null, "Консультация", "Введение в архитектурное проектирование", ""],
["2026-10-31", "08:30", null, "Зачёт", "Введение в архитектурное проектирование", ""],
]);
// An empty date cell continues the day above: the second consultation on 18 December.
assert.deepEqual(
s.lessons.filter((l) => l.date === "2026-12-18").map((l) => [l.start, l.subject]),
[
["08:30", "Проектирование небольшого архитектурного сооружения"],
["15:20", "Рисунок"],
],
);
// Two columns under the group: each subgroup takes its credit with its own teacher.
assert.deepEqual(
s.lessons.filter((l) => l.date === "2026-12-22").map((l) => [l.teacher, l.room, l.subgroups[0]?.label]),
[
["Кузякова О.Д.", "361", "Подгруппа 1"],
["Пискун А.А.", "370", "Подгруппа 2"],
],
);
});