import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { parseHomeworkPage, parseTeacherGrading } from '../src/core/homework-page.ts'; /** * Fixtures mirror the legacy client's templates (feedback.hbs, submission.hbs) * as actually served — including the parts that are absent for a student, which * is what makes every field optional. */ const page = (parts: { submission?: string; feedback?: string; afterSubmission?: string }) => `
keine Beschreibung vorhanden
${parts.submission ?? ''}
${parts.afterSubmission ?? ''} ${parts.feedback === undefined ? '' : `
${parts.feedback}
`} `; const fileCard = (id: string, name: string, size = 1234) => `
`; describe('parseHomeworkPage', () => { it("reads the teacher's written feedback, which no API exposes", () => { const html = page({ feedback: `

vollständig und nachvollziehbar

`, }); assert.equal(parseHomeworkPage(html)?.gradeComment, 'vollständig und nachvollziehbar'); }); it('reads a percentage grade from the feedback tab', () => { const html = page({ feedback: '

Du hast 85% erreicht

' }); assert.equal(parseHomeworkPage(html)?.gradePercent, 85); }); it('reads the student\'s typed answer from the editable textarea', () => { const html = page({ submission: ``, }); assert.equal(parseHomeworkPage(html)?.submittedText, 'Meine Lösung'); }); it('reads the typed answer from the read-only form once submission closed', () => { // The read-only `
` is a sibling *after* the closed // submission section, not a child of it — verified against a real 33.40 // page for a past-due submission. A fixture that nested it inside the // section (as an earlier one did) hid a bug where every past-due // submission lost its text. See src/core/homework-page.ts. const html = page({ submission: `
`, afterSubmission: `

Abgegebener Text

`, }); assert.equal(parseHomeworkPage(html)?.submittedText, 'Abgegebener Text'); }); it('does not confuse the submitted text with the teacher feedback comment', () => { // Both are comment divs, distinguished only by the exact class: the // student's is `class="comment"`, the teacher's `class="comment ckcontent"`. const html = page({ afterSubmission: `

Meine Abgabe

`, feedback: `

Gut gemacht

`, }); const detail = parseHomeworkPage(html); assert.equal(detail?.submittedText, 'Meine Abgabe'); assert.equal(detail?.gradeComment, 'Gut gemacht'); }); it('separates submitted files from files the teacher returned', () => { const html = page({ submission: fileCard('a'.repeat(24), 'meine-abgabe.pdf'), feedback: fileCard('b'.repeat(24), 'korrektur.pdf'), }); const detail = parseHomeworkPage(html); assert.deepEqual(detail?.submittedFiles, [{ id: 'a'.repeat(24), name: 'meine-abgabe.pdf' }]); assert.deepEqual(detail?.gradingFiles, [{ id: 'b'.repeat(24), name: 'korrektur.pdf' }]); }); it('returns a result with nothing set when the page carries no submission', () => { // The common case for an ungraded task: sections present but empty. const detail = parseHomeworkPage(page({})); assert.ok(detail); assert.equal(detail.submittedText, undefined); assert.equal(detail.gradeComment, undefined); assert.deepEqual(detail.gradingFiles, []); }); it('gives up rather than guessing when the markup is not a homework page', () => { assert.equal(parseHomeworkPage('Anmelden'), undefined); }); it('decodes entities in file names', () => { const html = page({ submission: fileCard('c'.repeat(24), 'A&B "final".pdf') }); assert.equal(parseHomeworkPage(html)?.submittedFiles[0]?.name, 'A&B "final".pdf'); }); }); /** * The teacher's grading form, as the legacy client renders it. * * Anchored on the `name=` attributes the POST handler reads rather than on * layout: one hidden `submissionId` per block, `teamMembers` naming who handed * it in, a `grade` number input whose `value` is empty when ungraded (the * `placeholder` is a hint, not a grade), and a `gradeComment` textarea whose * body arrives HTML-escaped. */ const gradingBlock = ( submissionId: string, submitterId: string, grade: string, comment: string, ) => `
`; describe('parseTeacherGrading', () => { const a = 'a'.repeat(24); const b = 'b'.repeat(24); const student1 = '1'.repeat(24); const student2 = '2'.repeat(24); it('reads every submission on the form, with its submitter', () => { const html = `
${gradingBlock(a, student1, '', '<p>Alles richtig!</p>')}${gradingBlock(b, student2, '100', '<p>Alles korrekt!</p>')}
`; const grading = parseTeacherGrading(html); assert.equal(grading.length, 2); assert.deepEqual(grading[0]?.submitterIds, [student1]); assert.deepEqual(grading[1]?.submitterIds, [student2]); }); it('distinguishes a feedback-only grade from a percentage', () => { const html = gradingBlock(a, student1, '', '<p>Alles richtig!</p>'); const [entry] = parseTeacherGrading(html); // An empty value is ungraded; the placeholder "95" must not be read as one. assert.equal(entry?.gradePercent, undefined); assert.equal(entry?.gradeComment, 'Alles richtig!'); }); it('reads a percentage when one was given', () => { const [entry] = parseTeacherGrading(gradingBlock(b, student2, '100', '<p>Gut</p>')); assert.equal(entry?.gradePercent, 100); assert.equal(entry?.gradeComment, 'Gut'); }); it('returns nothing for the student view, which has no grading form', () => { assert.deepEqual(parseTeacherGrading(page({ feedback: '
Gut
' })), []); }); });