import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import AdmZip from "adm-zip";
import { parseDocx } from "../server/parser.js";
import { placeNote, lessonPlace } from "../server/places.js";
import { placesOn, placesFor } from "../shared/places.js";
import { parseWeeksDocx, parseBellsDocx } from "../server/calendar.js";
import { mergeWeekTables } from "../server/source.js";
import { timetable } from "../server/timetable.js";
// "Занятия в Полоцке" over a day's first row, as 24-ИЯ (РА)и-4…6 write it: no lesson, but the
// town of that day's lessons in the column.
const fixture = (name) => readFileSync(new URL(`./fixtures/${name}.docx`, import.meta.url));
const calendar = {
...mergeWeekTables([parseWeeksDocx(fixture("weeks"))]),
bells: parseBellsDocx(fixture("bells")),
source: {},
};
const escape = (s) => s.replace(/&/g, "&").replace(/
`${span > 1 ? `` : ""}${text
.split("\n")
.map((line) => `${escape(line)}`)
.join("")}`;
const docx = (rows) => {
const zip = new AdmZip();
zip.addFile(
"word/document.xml",
Buffer.from(
`` +
`Расписание. Осенний семестр 2026/2027 учебного года` +
`${rows.map((r) => `${r.join("")}`).join("")}`,
),
);
return zip.toBuffer();
};
const schedule = parseDocx(
docx([
[cell("Дни"), cell("Часы"), cell("26-ИЯи-1", 2), cell("26-ИЯи-2", 2)],
[
cell("Вторник"),
cell("8:30-9:55"),
cell("Занятия в Полоцке"),
cell("Занятия в Полоцке\nФункциональная грамматика (п.з.) Иванова А.А. 249Ж"),
cell("Педагогика (л.) Петров П.П. 412н", 2),
],
[cell(""), cell("10:05-11:30"), cell("Педагогика (п.з.) Сидоров С.С. 205Г", 2), cell("Философия (п.з.) Козлов К.К. 510н", 2)],
[
cell("Среда"),
cell("8:30-9:55"),
cell("Латинский язык (п.з.) Нестер Н.В. 246(Новополоцк)", 2),
cell("г.Новополоцк\nРусский язык как иностранный (п.з.) Кириллова В.С. 533н"),
cell(""),
],
[cell("Четверг"), cell("8:30-9:55"), cell("История Полоцка (л.) Борун Е.Н. 100", 4)],
]),
{ name: "26-ИЯи.docx", year: "2026-2027" },
);
const file = { id: "places", name: "26-ИЯи.docx" };
const lesson = (day, start, group, subject) =>
schedule.lessons.find((l) => l.day === day && l.start === start && l.groups.includes(group) && l.subject.startsWith(subject));
test("Места: «Занятия в Полоцке» — не занятие, а город дня в столбце", () => {
assert.ok(!schedule.lessons.some((l) => /Занятия в/.test(l.subject)), "заметка не становится занятием");
assert.deepEqual(
schedule.places.map((p) => [p.day, p.groups.join(), p.subgroups.map((s) => s.label).join(), p.place, p.text]),
[
[2, "26-ИЯи-1", "Подгруппа 1", "Полоцк", "Занятия в Полоцке"],
[2, "26-ИЯи-1", "Подгруппа 2", "Полоцк", "Занятия в Полоцке"],
],
);
// The lesson under the note, in the same cell.
const grammar = lesson(2, "08:30", "26-ИЯи-1", "Функциональная грамматика");
assert.equal(grammar.place, "Полоцк");
assert.equal(grammar.room, "249Ж");
// A lesson of the whole group: both subgroups have the note.
assert.equal(lesson(2, "10:05", "26-ИЯи-1", "Педагогика").place, "Полоцк");
// Another group the same day stays where it was.
assert.equal(lesson(2, "08:30", "26-ИЯи-2", "Педагогика").place, undefined);
assert.equal(lesson(2, "10:05", "26-ИЯи-2", "Философия").place, undefined);
});
test("Места: город в ячейке занятия — отдельной строкой или после аудитории", () => {
const latin = lesson(3, "08:30", "26-ИЯи-1", "Латинский язык");
assert.equal(latin.room, "246");
assert.equal(latin.place, "Новополоцк");
const russian = lesson(3, "08:30", "26-ИЯи-2", "Русский язык");
assert.equal(russian.subject, "Русский язык как иностранный");
assert.equal(russian.place, "Новополоцк");
// A subject that names a town is no place.
const history = lesson(4, "08:30", "26-ИЯи-1", "История Полоцка");
assert.equal(history.subject, "История Полоцка");
assert.equal(history.place, undefined);
});
test("Места: разбор заметок и строк с городом", () => {
assert.deepEqual(placeNote("Занятия в Полоцке"), { text: "Занятия в Полоцке", place: "Полоцк", rest: "" });
assert.deepEqual(placeNote("Занятия в г. Новополоцк\nИнформационный час ауд.В0-02"), {
text: "Занятия в г. Новополоцк",
place: "Новополоцк",
rest: "Информационный час ауд.В0-02",
});
assert.equal(placeNote("Занятия в Междуречье.").place, "Междуречье");
assert.equal(placeNote("Занятия в Полоцке Философия (л.)").rest, "Философия (л.)");
assert.equal(placeNote("История Полоцка (л.)"), null);
assert.equal(placeNote("Занятия в ауд. 412н"), null);
assert.deepEqual(lessonPlace("121 (г.Новополоцк)"), { place: "Новополоцк", rest: "121" });
// A venue after the town stays in the text.
assert.deepEqual(lessonPlace("г. Полоцк, Дом офицеров"), { place: "Полоцк", rest: "г. Полоцк, Дом офицеров" });
assert.deepEqual(lessonPlace("Философия (л.) Борун Е.Н. 100"), { place: null, rest: "Философия (л.) Борун Е.Н. 100" });
});
test("Места: заметки дня для подгруппы — общие для сервера и PWA", () => {
const notes = [
{ day: 4, date: null, groups: ["26-ИЯи-2"], subgroups: [{ group: "26-ИЯи-2", label: "Подгруппа 1" }], place: "Новополоцк", text: "Занятия в Новополоцке" },
];
const labels = ["Подгруппа 1", "Подгруппа 2"];
const one = placesOn(notes, "26-ИЯи-2", labels, 4);
assert.deepEqual(one, [{ place: "Новополоцк", text: "Занятия в Новополоцке", subgroup: "Подгруппа 1" }]);
// All subgroups shown: the note says whose it is; the first subgroup: their own day.
assert.deepEqual(placesFor(one, null), one);
assert.deepEqual(placesFor(one, "Подгруппа 1"), [{ place: "Новополоцк", text: "Занятия в Новополоцке", subgroup: null }]);
assert.deepEqual(placesFor(one, "Подгруппа 2"), []);
assert.deepEqual(placesOn(notes, "26-ИЯи-1", labels, 4), []);
// Both subgroups of 26-ИЯи-1 on Tuesday: one note for the whole group.
assert.deepEqual(placesOn(schedule.places, "26-ИЯи-1", labels, 2), [{ place: "Полоцк", text: "Занятия в Полоцке", subgroup: null }]);
});
test("Места в /api/timetable: город дня и занятия", () => {
// Tuesday 13 and Wednesday 14 October 2026.
const one = timetable({ file, schedule, calendar, group: "26-ИЯи-1", from: "2026-10-13", days: 2 });
const [tuesday, wednesday] = one.days;
// The note in both subgroups is the whole group's.
assert.deepEqual(tuesday.places, [{ place: "Полоцк", text: "Занятия в Полоцке", subgroup: null }]);
assert.ok(tuesday.lessons.every((l) => l.place === "Полоцк"));
assert.equal(wednesday.places, undefined);
assert.equal(wednesday.lessons[0].place, "Новополоцк");
assert.deepEqual(one.weekly.find((d) => d.day === 2).places, tuesday.places);
const two = timetable({ file, schedule, calendar, group: "26-ИЯи-2", from: "2026-10-13", days: 1 });
assert.equal(two.days[0].places, undefined);
assert.ok(two.days[0].lessons.every((l) => l.place === null));
});