import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { parseLessonTaskLinks, withScrapedIds } from '../src/core/lesson-page.ts';
/**
* Fixtures mirror the legacy topic page as actually served — the task cards it
* renders under `
`, which is the only place a topic-attached
* task's id is exposed at all.
*/
const taskCard = (id: string, name: string) =>
`
` +
`Fällig: 30.07.2026 15:23
`;
const page = (cards: string) =>
`
Aufgaben (2)
` +
`
` +
``;
const id = (c: string) => c.repeat(24);
describe('parseLessonTaskLinks', () => {
it('recovers the ids the API withholds, with their names', () => {
const html = page(taskCard(id('a'), 'Bestandteile einer Kamera') + taskCard(id('b'), 'Eigenschaften einer Sammellinse'));
assert.deepEqual(parseLessonTaskLinks(html), [
{ id: id('a'), name: 'Bestandteile einer Kamera' },
{ id: id('b'), name: 'Eigenschaften einer Sammellinse' },
]);
});
it('decodes entities in task names', () => {
const html = page(taskCard(id('c'), 'Größe & Form'));
assert.equal(parseLessonTaskLinks(html)[0]?.name, 'Größe & Form');
});
it('reports each task once even when the page links it twice', () => {
// The card title and its action button both point at the same task.
const html = page(taskCard(id('d'), 'Würfelspiel') + taskCard(id('d'), 'Würfelspiel'));
assert.equal(parseLessonTaskLinks(html).length, 1);
});
it('returns nothing rather than guessing when the markup is not a topic page', () => {
assert.deepEqual(parseLessonTaskLinks('Anmelden'), []);
});
it('ignores homework links that carry no task name', () => {
// A bare link with no aria-label cannot be paired with an API task, so it
// is no use; taking it would produce a task named after nothing.
assert.deepEqual(parseLessonTaskLinks(page(`
Aufgabe`)), []);
});
});
describe('withScrapedIds', () => {
it('gives the API tasks the ids from the page, matching on name', () => {
const tasks = [{ name: 'Würfelspiel' }, { name: 'Sammellinse' }];
const links = [
{ id: id('a'), name: 'Sammellinse' },
{ id: id('b'), name: 'Würfelspiel' },
];
assert.deepEqual(
withScrapedIds(tasks, links).map((t) => t.id),
[id('b'), id('a')],
);
});
it('leaves a task the page does not account for without an id', () => {
// Still worth reporting — the user can see it — but the id-taking tools
// cannot open it, so it must not be handed a fabricated id.
const merged = withScrapedIds([{ name: 'Nur in der API' }], [{ id: id('a'), name: 'Etwas anderes' }]);
assert.equal(merged[0]?.id, undefined);
assert.equal(merged[0]?.name, 'Nur in der API');
});
it('does not overwrite an id the task already has', () => {
const merged = withScrapedIds([{ id: id('f'), name: 'Würfelspiel' }], [{ id: id('a'), name: 'Würfelspiel' }]);
assert.equal(merged[0]?.id, id('f'));
});
it('tolerates surrounding whitespace in the API name', () => {
const merged = withScrapedIds([{ name: ' Würfelspiel ' }], [{ id: id('a'), name: 'Würfelspiel' }]);
assert.equal(merged[0]?.id, id('a'));
});
});