239 lines
11 KiB
JavaScript
239 lines
11 KiB
JavaScript
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"],
|
||
],
|
||
);
|
||
});
|