Files
Schulcloud-MCP/test/homework-page.test.ts
MechaCat02 290b352b07 Scrape submitted text for past-due submissions, not just editable ones
Once a submission's due date passes, the legacy homework page renders the
student's text read-only in a `<div class="comment">` that is a sibling
*after* `</section id="submission">`, which then holds only the file list.
`parseHomeworkPage` searched for that div *inside* the submission section, so
for every past-due submission it silently returned no submitted text while
still reporting the grade and feedback — the reader would conclude the student
handed in nothing.

Search the whole page instead. `class="comment"` (quote right after the word)
stays specific: the teacher's feedback is `class="comment ckcontent"` and does
not match, and the editable-textarea branch is still tried first.

Found by standing up a local instance from the deployed images and reading a
real 33.40 page for a seeded past-due submission; the earlier test fixture had
nested the div inside the section, which is why the gap was invisible. The
fixtures now match the real DOM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 14:17:14 +02:00

95 lines
4.3 KiB
TypeScript

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { parseHomeworkPage } 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 }) => `
<html><body>
<nav class="nav tab-links"><a href="#activetabid=extended">Details</a></nav>
<section id="extended" class="tab-content">keine Beschreibung vorhanden</section>
<section id="submission" class="tab-content">${parts.submission ?? ''}</section>
${parts.afterSubmission ?? ''}
${parts.feedback === undefined ? '' : `<section id="feedback" class="tab-content">${parts.feedback}</section>`}
</body></html>`;
const fileCard = (id: string, name: string, size = 1234) =>
`<div class="card file " data-file-name="${name}" data-file-size="${size}" data-file-id="${id}"></div>`;
describe('parseHomeworkPage', () => {
it("reads the teacher's written feedback, which no API exposes", () => {
const html = page({
feedback: `<div class="comment ckcontent" data-testid="feedback-comment"><p>vollst&auml;ndig und nachvollziehbar</p></div>`,
});
assert.equal(parseHomeworkPage(html)?.gradeComment, 'vollständig und nachvollziehbar');
});
it('reads a percentage grade from the feedback tab', () => {
const html = page({ feedback: '<p>Du hast 85% erreicht</p>' });
assert.equal(parseHomeworkPage(html)?.gradePercent, 85);
});
it('reads the student\'s typed answer from the editable textarea', () => {
const html = page({
submission: `<textarea data-testid="submission-text" name="comment"> <p>Meine L&ouml;sung</p> </textarea>`,
});
assert.equal(parseHomeworkPage(html)?.submittedText, 'Meine Lösung');
});
it('reads the typed answer from the read-only form once submission closed', () => {
// The read-only `<div class="comment">` 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: `<section class="files" data-testid="submissions-section-files"></section>`,
afterSubmission: `<div class="comment"><p>Abgegebener Text</p></div>`,
});
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: `<div class="comment"><p>Meine Abgabe</p></div>`,
feedback: `<div class="comment ckcontent" data-testid="feedback-comment"><p>Gut gemacht</p></div>`,
});
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('<html><body>Anmelden</body></html>'), undefined);
});
it('decodes entities in file names', () => {
const html = page({ submission: fileCard('c'.repeat(24), 'A&amp;B &quot;final&quot;.pdf') });
assert.equal(parseHomeworkPage(html)?.submittedFiles[0]?.name, 'A&B "final".pdf');
});
});