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)); });