All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m17s
- Bug gevonden en bevestigd (niet blind aangenomen): math.js riep
onProgress maar op ÉÉN plek aan, in endRound(), NA een heel blok van
10 sommen, met alleen het geaggregeerde totaal. Eén sessie van 10
sommen - "een groep opdrachten" - leverde dus precies 1 rij op in
progress_events, niet 10, en maakte "welke som ging fout" onmogelijk
te zien (alleen een totaaltelling per blok).
- Fix: onProgress verplaatst naar check(), bij elke afgeronde som (goed
op de eerste keer, of opgeven na 3 pogingen) - een sessie van 10
sommen levert nu 10 progress_events-rijen op, elk met een nieuw
optioneel detail-veld (welke som, gegeven antwoord, juist antwoord).
letters.js/ball.js blijven deze ronde ongemoeid (hun granulariteit
veroorzaakte niet deze bug).
- Nieuwe kolom progress_events.detail (JSONB, nullable, puur additief -
db/020_progress_detail.sql). POST /my/progress accepteert het
optioneel; te groot/geen plain object wordt stilzwijgend genegeerd
zonder de rest van de log-poging te laten falen (detail is decoratief,
geen kernfunctionaliteit).
- Voortgang-tab (admin.js) herontworpen, zelfde ontwerptaal als de
eerdere School-navigator-herbouw: vakgebied-tabs, een
klassenmanagement-infobadge (Groep {n} · Niveau {n}, opgehaald via de
bestaande /levels/pupil/:id-route) zodat resultaten meteen tegen het
toegewezen niveau afgezet kunnen worden, een met de hand getekende
SVG-nauwkeurigheidsring + staafdiagram (goed/fout per dag, laatste 14
dagen - geen library, zelfde aanpak als avatar.js) en een "recente
fouten"-lijst die precies toont welke som fout ging. De bestaande
ruwe tijdlijn blijft eronder staan, niets is verwijderd.
- Terzijde gevonden en gefixt: .am-btn.active bestond nergens in de
CSS, waardoor de vakgebied-tabs (hier én in Klassenmanagement,
v0.4.34-beta) geen enkele visuele aanduiding hadden welke actief was.
- Tests: test/progress-logging.test.js uitgebreid met 4 tests voor het
detail-veld (opgeslagen, te groot genegeerd, geen plain object
genegeerd, afwezig blijft null). Volledige testsuite: 132/132.
- Live geverifieerd: een math.js-sessie met een mix van goede/foute
sommen doorlopen en bevestigd dat er 3 aparte POST /my/progress-
aanroepen gebeuren voor 3 sommen (niet 1 aggregaat), elk met het
juiste detail; de herbouwde Voortgang-tab geopend met gemockte data
en met screenshots bevestigd dat de nauwkeurigheidsring (38% = 3/8,
rekenkundig geverifieerd), het klassenmanagement-infobadge, de
staafdiagram en de "recente fouten"-lijst (met de daadwerkelijke som)
correct tonen, én dat de active-tab-fix ook op de Klassenmanagement-
tab zichtbaar is.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
141 lines
7.9 KiB
JavaScript
141 lines
7.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import Fastify from 'fastify';
|
|
import cookie from '@fastify/cookie';
|
|
import rateLimit from '@fastify/rate-limit';
|
|
import api from '../src/api.js';
|
|
|
|
/* "de voortgangen werken niet" (gebruikersklacht) - dit pad had nog GEEN enkele
|
|
test (mocked-pool of anders), ondanks dat het de kern van de voortgangs-
|
|
feature is: een leerling logt een afgerond moment, en het datamodel bepaalt
|
|
zelf teacher_id/board_id uit de toewijzing (nooit van de client). Dekt de
|
|
drie manieren waarop een toewijzing gevonden kan worden (leerling-eigen,
|
|
klasbreed, thuiskind-via-ouder) plus de foutpaden. */
|
|
function makeApp(user, { assignmentRows = {}, parentRows = [] } = {}) {
|
|
const calls = [];
|
|
const query = async (sql, params = []) => {
|
|
calls.push({ sql, params });
|
|
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [user] };
|
|
if (sql.includes('FROM user_roles')) return { rows: [] };
|
|
if (sql === 'SELECT * FROM assignments WHERE pupil_id = $1') return { rows: assignmentRows.byPupil ? [assignmentRows.byPupil] : [] };
|
|
if (sql === 'SELECT * FROM assignments WHERE class_id = $1') return { rows: assignmentRows.byClass ? [assignmentRows.byClass] : [] };
|
|
if (sql.startsWith('SELECT parent_id FROM parent_children')) return { rows: parentRows };
|
|
if (sql.startsWith('INSERT INTO progress_events')) return { rows: [] };
|
|
return { rows: [] };
|
|
};
|
|
const pool = { query, async connect() { return { query, release() {} }; } };
|
|
return (async () => {
|
|
const app = Fastify({ trustProxy: 2 });
|
|
await app.register(cookie);
|
|
await app.register(rateLimit, { global: false });
|
|
app.decorate('pg', pool);
|
|
await app.register(api, { prefix: '/api' });
|
|
await app.ready();
|
|
return { app, calls };
|
|
})();
|
|
}
|
|
|
|
const cookies = { teach_session: 'x'.repeat(64) };
|
|
const validBody = { widgetId: 'letters:0', widgetType: 'letters', attempts: 1, correct: 1, stars: 1 };
|
|
function insertCall(calls) {
|
|
return calls.find(c => c.sql.startsWith('INSERT INTO progress_events'));
|
|
}
|
|
|
|
test('voortgang: leerling-eigen toewijzing wint en levert de juiste teacher_id/board_id op', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, {
|
|
assignmentRows: {
|
|
byPupil: { teacher_id: 3, board_id: 'bord-a' },
|
|
byClass: { teacher_id: 4, board_id: 'bord-b' }, // zou genegeerd moeten worden
|
|
},
|
|
});
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const ins = insertCall(calls);
|
|
assert.ok(ins, 'INSERT verwacht');
|
|
assert.equal(ins.params[1], 3);
|
|
assert.equal(ins.params[2], 'bord-a');
|
|
});
|
|
|
|
test('voortgang: valt terug op de klasbrede toewijzing als er geen eigen toewijzing is', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, {
|
|
assignmentRows: { byClass: { teacher_id: 4, board_id: 'bord-b' } },
|
|
});
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const ins = insertCall(calls);
|
|
assert.equal(ins.params[1], 4);
|
|
assert.equal(ins.params[2], 'bord-b');
|
|
});
|
|
|
|
test('voortgang: thuiskind zonder school valt terug op de gekoppelde ouder als "leerkracht"', async () => {
|
|
const homeChild = { id: 11, username: 'timo', role: 'pupil', school_id: null, class_id: null, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(homeChild, { parentRows: [{ parent_id: 7 }] });
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const ins = insertCall(calls);
|
|
assert.equal(ins.params[1], 7);
|
|
assert.equal(ins.params[2], 'thuis');
|
|
});
|
|
|
|
test('voortgang: leerling op school zonder enige toewijzing krijgt 400, geen stille silent-fail insert', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, {});
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 400, res.body);
|
|
assert.ok(!insertCall(calls), 'er mag geen INSERT gebeurd zijn');
|
|
});
|
|
|
|
test('voortgang: niet-leerling (bv. leerkracht) krijgt 403', async () => {
|
|
const teacher = { id: 3, username: 'juf', role: 'teacher', school_id: 2, class_id: null, data: {}, data_rev: 0 };
|
|
const { app } = await makeApp(teacher, {});
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 403, res.body);
|
|
});
|
|
|
|
test('voortgang: ontbrekende widgetId/widgetType levert 400 op, niet een kapotte insert', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, { assignmentRows: { byPupil: { teacher_id: 3, board_id: 'bord-a' } } });
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { attempts: 1, correct: 1 } });
|
|
assert.equal(res.statusCode, 400, res.body);
|
|
assert.ok(!insertCall(calls));
|
|
});
|
|
|
|
/* "1 opdracht i.p.v. een groep" - de fix zelf zit in math.js (per som loggen
|
|
i.p.v. per blok van 10), maar het datamodel/de server moeten het optionele
|
|
detail (welke som/welk antwoord) wel kunnen bewaren zonder de rest van de
|
|
voortgang-logging te breken bij rommelige/te grote input. */
|
|
test('voortgang: detail (welke opdracht) wordt meegegeven aan de insert', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, { assignmentRows: { byPupil: { teacher_id: 3, board_id: 'bord-a' } } });
|
|
const detail = { question: '7 + 8 =', answer: 14, expected: 15, correct: false };
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { ...validBody, detail } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.deepEqual(insertCall(calls).params[8], detail);
|
|
});
|
|
|
|
test('voortgang: een te grote detail wordt genegeerd zonder de hele log-poging te laten falen', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, { assignmentRows: { byPupil: { teacher_id: 3, board_id: 'bord-a' } } });
|
|
const detail = { question: 'x'.repeat(2000) };
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { ...validBody, detail } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.equal(insertCall(calls).params[8], null);
|
|
});
|
|
|
|
test('voortgang: een detail dat geen plain object is (array/string) wordt genegeerd', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, { assignmentRows: { byPupil: { teacher_id: 3, board_id: 'bord-a' } } });
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { ...validBody, detail: ['niet', 'plain'] } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.equal(insertCall(calls).params[8], null);
|
|
});
|
|
|
|
test('voortgang: zonder detail-veld blijft alles zoals voorheen (null, geen fout)', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, { assignmentRows: { byPupil: { teacher_id: 3, board_id: 'bord-a' } } });
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.equal(insertCall(calls).params[8], null);
|
|
});
|