APP-Backend / tests /iserv-parse.test.mjs
Luca448's picture
Claude Sonnet 5.5
feat(iserv): expired flag, attachment links and due text parser
b12cb35
Raw History Blame Contribute Delete
8.11 kB
import assert from 'node:assert/strict';
import test, { after, before } from 'node:test';
import puppeteer from 'puppeteer';
import { isoFromText, parseAttachmentLinks, parseListHtml, parseTaskHtml } from '../src/api/iserv-parse.js';
// Fictional pages in the layouts of IServ (older and current), the same the IServ filter project tests its Python
// parsers with. These parsers run inside a browser page, so the tests run them in one.
const LIST_OLDER = `<html><body><table id="crud-table"><thead><tr><th></th><th>Titel</th><th>Beginn</th><th>Abgabe</th><th>Tags</th></tr></thead><tbody>
<tr><td></td><td><a href="/iserv/exercise/show/101">Bruchrechnung üben</a></td><td data-sort="20261001">01.10.2026</td><td data-sort="20261010235900">10.10.2026 23:59</td><td>Mathe</td></tr>
<tr><td></td><td><a href="https://beispiel.iserv.eu/iserv/exercise/show/102">Referat</a></td><td data-sort="20261002">02.10.2026</td><td data-sort="20261015120000">15.10.2026 12:00</td><td>(keine)</td></tr>
<tr class="success"><td></td><td><a href="/iserv/exercise/show/103">Vokabeln Unit 4</a></td><td data-sort="20260928">28.09.2026</td><td data-sort="20261005080000">05.10.2026 08:00</td><td>Englisch</td><td><span title="Abgegeben">✓</span></td></tr>
</tbody></table></body></html>`;
// Current layout: status icon first, no start column, teacher feedback (Rückmeldungen) right after the due date,
// an empty tag as an italic placeholder.
const ROW = ({ status, id, title, due, tags }) => `<tr><td class="iserv-admin-list-field" data-sort="x">
<span class="text-center text-large" title="${status}"><span class="fal fa-circle-check"></span></span></td>
<td><a href="/iserv/exercise/show/${id}">${title}</a></td>
<td data-sort="${due}"><ul class="list-unstyled"><li>x</li><li><span class="timeAgo" data-date="2026-10-10T23:59:00+02:00">in 7 Tagen</span></li></ul></td>
<td data-sort="1"><i>Rückmeldung von Frau Zimmermann</i></td><td><div class="tags-width">${tags}</div></td><td></td></tr>`;
const LIST_CURRENT = `<html><body><table><thead><tr><th class="list-header"></th><th>Aufgabe</th><th>Abgabetermin</th><th>Rückmeldungen</th><th>Tags</th><th></th></tr></thead><tbody>
${ROW({ status: 'Erledigt', id: 201, title: 'Gedicht lernen', due: '20261010235900', tags: '<i>keine</i>' })}
${ROW({ status: 'Neu', id: 202, title: 'Versuchsprotokoll', due: '20261012080000', tags: 'Physik' })}
</tbody></table></body></html>`;
const DATES = `<table class="table"><thead><tr><th>Erstellt von</th><th>Starttermin</th><th>Abgabetermin</th></tr></thead><tbody><tr>
<td><a class="mailto" href="mailto:zimmermann@beispiel.iserv.eu">Frau Zimmermann</a></td><td>01.10.2026 08:00</td>
<td><ul><li>10.10.2026 23:59</li></ul></td></tr></tbody></table>`;
const TASK_PAGE = (left, right) => `<html><body><div role="navigation"><span class="profile-username">Erika Musterfrau</span></div>
<div id="content"><div class="panel panel-default"><div class="row"><div class="col-md-6"><div class="panel-body">${left}</div></div>
<div class="col-md-6"><div class="panel-body">${right}</div></div></div></div>
<div class="panel panel-default"><div class="panel-body"><form name="submission"><div class="text-break-word p-3">Eigene Abgabe Wolf</div></form></div></div>
<div class="panel panel-default"><div class="panel-body"><div class="text-break-word p-3">Rückmeldung von Kaiser: gut gemacht</div></div></div></div></body></html>`;
const DESCRIPTION = `<div><div class="text-break-word pb-0"><p>Lerne das Gedicht <strong>auswendig</strong> bis Freitag.</p><p>Frau Hofmann fragt ab.<br>Viel Erfolg!</p></div></div>`;
const FILES = `<form name="iserv_exercise_attachment"><table><tbody><tr><td><a class="text-break-word" href="/iserv/fs/file/exercise/1">Blatt.pdf</a></td></tr></tbody></table></form>`;
const PAGE_WITH_FILES = TASK_PAGE(DESCRIPTION + '<hr>' + DATES, FILES);
const PAGE_WITHOUT_FILES = TASK_PAGE(DESCRIPTION, '<hr>' + DATES); // no files: the date table sits in the other column
const PAGE_WITHOUT_DESCRIPTION = TASK_PAGE('', '<hr>' + DATES);
const PAGE_OLDER = `<html><body><table><tbody><tr><th>Erstellt von:</th><td>Frau Zimmermann</td></tr><tr><th>Teilnehmer:</th><td>Klasse 7b Ahrens</td></tr></tbody></table>
<div>Beschreibung:</div><div class="text-break-word">Vokabeln lernen.</div>
<div>Rückmeldungen</div><div class="text-break-word">Rückmeldung von Kaiser: gut gemacht</div>
<form name="submission"><div class="text-break-word">Eigene Abgabe Wolf</div></form></body></html>`;
let browser;
let page;
before(async () => {
browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
page = await browser.newPage();
});
after(async () => {
await browser?.close();
});
const list = (html) => page.evaluate(parseListHtml, html);
const task = (html) => page.evaluate(parseTaskHtml, html);
test('older list: tags, start and due by header and data-sort, done by the status title', async () => {
assert.deepEqual(await list(LIST_OLDER), {
101: { tags: 'Mathe', start: '2026-10-01', due: '2026-10-10T23:59', done: false, expired: false },
102: { tags: '', start: '2026-10-02', due: '2026-10-15T12:00', done: false, expired: false },
103: { tags: 'Englisch', start: '2026-09-28', due: '2026-10-05T08:00', done: true, expired: false },
});
});
test('current list: the column after the due date (teacher feedback) is never taken for tags', async () => {
assert.deepEqual(await list(LIST_CURRENT), {
201: { tags: '', start: null, due: '2026-10-10T23:59', done: true, expired: false },
202: { tags: 'Physik', start: null, due: '2026-10-12T08:00', done: false, expired: false },
});
});
test('task page: only the description, with its paragraphs and line breaks, and the start date', async () => {
const expected = { description: 'Lerne das Gedicht auswendig bis Freitag.\nFrau Hofmann fragt ab.\nViel Erfolg!', start: '2026-10-01T08:00' };
for (const html of [PAGE_WITH_FILES, PAGE_WITHOUT_FILES]) {
assert.deepEqual(await task(html), expected);
}
});
test('task page without a description gives "" and never submission or feedback', async () => {
assert.deepEqual(await task(PAGE_WITHOUT_DESCRIPTION), { description: '', start: '2026-10-01T08:00' });
});
test('older task page: the block after the "Beschreibung:" label, no start date', async () => {
assert.deepEqual(await task(PAGE_OLDER), { description: 'Vokabeln lernen.', start: null });
});
test('status icon "Abgelaufen" marks the task expired, not done', async () => {
const html = `<table><thead><tr><th></th><th>Aufgabe</th><th>Abgabetermin</th></tr></thead><tbody>
${ROW({ status: 'Abgelaufen', id: 301, title: 'Alt', due: '20260920120000', tags: 'Mathe' })}</tbody></table>`;
const result = await list(html);
assert.equal(result[301].expired, true);
assert.equal(result[301].done, false);
});
test('attachment links: the download link of each row, the name from the name link, menu toggles skipped', async () => {
const html = `<form name="iserv_exercise_attachment"><table><tbody>
<tr><td><a class="text-break-word" href="/iserv/fs/file/exercise/1">Blatt.pdf</a></td></tr>
<tr><td><a class="text-break-word" href="/iserv/fs/view/9">Bild.png</a></td><td><a data-toggle="dropdown" href="/">⋮</a>
<ul><li><a href="/iserv/fs/download/9">Herunterladen</a></li></ul></td></tr></tbody></table></form>
<form name="submission"><a href="/iserv/fs/file/own/2">Eigene.docx</a></form>`;
assert.deepEqual(await page.evaluate(parseAttachmentLinks, html), [
{ href: '/iserv/fs/file/exercise/1', filename: 'Blatt.pdf' },
{ href: '/iserv/fs/download/9', filename: 'Bild.png' },
]);
});
test('a page without the provided-files form has no attachment links', async () => {
assert.deepEqual(await page.evaluate(parseAttachmentLinks, PAGE_WITHOUT_FILES), []);
});
test('isoFromText reads dd.mm.yyyy with an optional time', () => {
assert.equal(isoFromText('Do, 24.09.2026 14:30 Uhr'), '2026-09-24T14:30');
assert.equal(isoFromText('5.10.2026'), '2026-10-05');
assert.equal(isoFromText('Morgen'), null);
assert.equal(isoFromText(undefined), null);
});