All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m56s
Gewijzigde functionaliteiten:
- Letterblokken logt per afgerond woord hoeveel pogingen het kostte en welke
letter op welke plek fout ging (beide invoerwijzen: blokken slepen en typen).
- Ballonnenwoord logt nu ook een verloren woord; dat leverde tot nu toe geen
enkele voortgang op. Een gewonnen woord telt de fout geraden letters mee.
- Flitswoorden stuurt de bestaande zelfcheck ("Gelukt" / "Nog oefenen") door
naar de voortgang; die uitkomst werd eerder weggegooid.
- Hakken & plakken krijgt dezelfde zelfcheck en daarmee als laatste taalwidget
een goed/fout-moment; de oefening zelf verandert niet.
- Voortgang-tab toont de fouten van taal: naast fout afgesloten opdrachten ook
"na N pogingen goed" (letterblokken kent geen faalmoment) en zelfchecks.
- Reeksstappen met flitswoorden of hakken & plakken wachten nu op een echt
afgerond moment in plaats van meteen te completeren.
Tests: 192 geslaagd (7 nieuw), plus live nagelopen in de leerlingweergave met
echte interactie per widget en een screenshot van de Voortgang-tab.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126hjBPD2ggZ2LEoDYZSW7S
147 lines
7.3 KiB
JavaScript
147 lines
7.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { runInNewContext } from 'node:vm';
|
|
import Fastify from 'fastify';
|
|
import cookie from '@fastify/cookie';
|
|
import rateLimit from '@fastify/rate-limit';
|
|
import api from '../src/api.js';
|
|
|
|
/* Voortgang bij taal. math.js logt sinds v0.4.35-beta per som mét detail; de
|
|
taalwidgets leverden alleen een kale telling op, waardoor de leerkracht bij
|
|
taal nooit zag WELK woord moeite kostte. De payloadvorm zit nu in één pure
|
|
functie, die hier echt uitgevoerd wordt (niet alleen op tekst gecontroleerd),
|
|
zelfde vm-techniek als de SLO-getalgrenzen in test/pupil-levels.test.js. */
|
|
async function loadPayloadHelper() {
|
|
const source = await readFile('public/js/learning-progressions.js', 'utf8');
|
|
const cut = source.indexOf('let SELF_LEARNING_LEVELS'); // daaronder hangt alles aan de DOM/api
|
|
const context = { LANG: 'nl', document: undefined };
|
|
runInNewContext(`${source.slice(0, cut)};globalThis.__payload = languageProgressPayload`, context);
|
|
// objecten uit een vm-context delen onze prototypes niet: platslaan zodat
|
|
// deepEqual over de wáárden gaat en niet over de realm
|
|
return (options) => JSON.parse(JSON.stringify(context.__payload(options)));
|
|
}
|
|
|
|
test('taalvoortgang: een foutloos woord levert één schone poging op', async () => {
|
|
const payload = await loadPayloadHelper();
|
|
const result = payload({ word: 'boom', mistakes: [], solved: true, stars: 1 });
|
|
assert.deepEqual(result, {
|
|
attempts: 1, correct: 1, stars: 1,
|
|
detail: { question: 'boom', expected: 'boom', answer: 'boom', correct: true },
|
|
});
|
|
assert.ok(!('mistakes' in result.detail), 'zonder missers geen mistakes-lijst');
|
|
});
|
|
|
|
test('taalvoortgang: goed na fouten telt de pogingen en bewaart welke letter fout ging', async () => {
|
|
const payload = await loadPayloadHelper();
|
|
const result = payload({
|
|
word: 'boom', stars: 1, solved: true,
|
|
mistakes: [{ expected: 'o', given: 'a' }, { expected: 'm', given: 'n' }],
|
|
});
|
|
assert.equal(result.attempts, 3, '1 goede plaatsing + 2 missers');
|
|
assert.equal(result.correct, 1);
|
|
// de grafiek rekent wrong = attempts - correct: 2 fouten, 1 goed
|
|
assert.equal(result.attempts - result.correct, 2);
|
|
assert.equal(result.detail.correct, false, 'niet foutloos: hoort in de foutenlijst');
|
|
assert.deepEqual(result.detail.mistakes, [
|
|
{ expected: 'o', given: 'a' }, { expected: 'm', given: 'n' },
|
|
]);
|
|
assert.equal(result.detail.answer, 'a', 'eerste misser vult het gegeven antwoord');
|
|
assert.equal(result.detail.expected, 'o');
|
|
});
|
|
|
|
test('taalvoortgang: een verloren ballonwoord logt een echte fout', async () => {
|
|
const payload = await loadPayloadHelper();
|
|
const result = payload({
|
|
word: 'bloem', solved: false, expected: 'bloem', answer: 'b_o__',
|
|
mistakes: ['k', 's', 'r', 'a', 'd', 'p'].map((given) => ({ given, expected: 'bloem' })),
|
|
});
|
|
assert.equal(result.correct, 0);
|
|
assert.equal(result.stars, 0, 'geen ster voor een verloren woord');
|
|
assert.equal(result.attempts, 6);
|
|
assert.equal(result.detail.correct, false);
|
|
assert.equal(result.detail.answer, 'b_o__', 'wat de leerling nog open had staan');
|
|
assert.equal(result.detail.expected, 'bloem');
|
|
});
|
|
|
|
test('taalvoortgang: een zelfcheck zonder ingetypt antwoord blijft één poging', async () => {
|
|
const payload = await loadPayloadHelper();
|
|
const nietGelukt = payload({ word: 'schaap', solved: false, selfCheck: true });
|
|
assert.equal(nietGelukt.attempts, 1, 'ook zonder missers telt dit als één poging');
|
|
assert.equal(nietGelukt.correct, 0);
|
|
assert.equal(nietGelukt.detail.answer, null, 'er is geen antwoord om te tonen');
|
|
assert.equal(nietGelukt.detail.selfCheck, true);
|
|
const gelukt = payload({ word: 'schaap', solved: true, stars: 1, selfCheck: true });
|
|
assert.equal(gelukt.correct, 1);
|
|
assert.equal(gelukt.detail.correct, true);
|
|
});
|
|
|
|
test('taalvoortgang: het detail blijft binnen de servergrens van 1000 tekens', async () => {
|
|
const payload = await loadPayloadHelper();
|
|
const result = payload({
|
|
word: 'w'.repeat(300), solved: true, stars: 1,
|
|
mistakes: Array.from({ length: 20 }, (_, i) => ({ expected: 'x'.repeat(80), given: String(i) })),
|
|
});
|
|
assert.equal(result.detail.mistakes.length, 6, 'hooguit zes missers bewaren');
|
|
assert.equal(result.detail.question.length, 80, 'woord afgekapt');
|
|
assert.ok(JSON.stringify(result.detail).length < 1000,
|
|
'anders gooit sanitizeProgressDetail het hele detail weg');
|
|
});
|
|
|
|
/* De vier taalwidgets moeten de gedeelde helper ook echt gebruiken; de werking
|
|
in de widget zelf (drag & drop, ballonnen, zelfcheck) is in de browser
|
|
nagelopen met echte interactie. */
|
|
test('taalwidgets gebruiken de gedeelde payload en ontvangen opts', async () => {
|
|
const [letters, ball, flits, hak] = await Promise.all([
|
|
readFile('public/js/widgets/letters.js', 'utf8'),
|
|
readFile('public/js/widgets/ball.js', 'utf8'),
|
|
readFile('public/js/widgets/flits.js', 'utf8'),
|
|
readFile('public/js/widgets/hak.js', 'utf8'),
|
|
]);
|
|
for (const [name, source] of [['letters', letters], ['ball', ball], ['flits', flits], ['hak', hak]]) {
|
|
assert.match(source, /onProgress\(languageProgressPayload\(/, `${name} logt via de gedeelde vorm`);
|
|
assert.doesNotMatch(source, /onProgress\(\s*\{\s*attempts/, `${name} heeft geen losse payload meer`);
|
|
}
|
|
assert.match(flits, /function mountFlits\(root, initState, opts\)/);
|
|
assert.match(hak, /function mountHak\(root, initState, opts\)/);
|
|
// ballon logt op twee plekken: gewonnen én verloren woord
|
|
assert.equal((ball.match(/languageProgressPayload\(/g) || []).length, 2);
|
|
// letterblokken telt missers in beide invoerwijzen (toetsenbord en blokken)
|
|
assert.equal((letters.match(/S\.wordMistakes\.push\(/g) || []).length, 2);
|
|
});
|
|
|
|
test('reeks: een flitsstap wacht nu op een echt afgerond moment', async () => {
|
|
const teacher = {
|
|
id: 3, username: 'juf', role: 'teacher', school_id: 2, class_id: null,
|
|
data: {}, data_rev: 0,
|
|
};
|
|
const calls = [];
|
|
const query = async (sql, params = []) => {
|
|
calls.push({ sql, params });
|
|
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [teacher] };
|
|
if (sql.includes('FROM user_roles')) return { rows: [] };
|
|
if (sql.startsWith('SELECT * FROM classes WHERE id')) return { rows: [{ id: 50, school_id: 2 }] };
|
|
if (sql.includes('FROM class_teachers')) return { rows: [{ class_id: 50 }] };
|
|
if (sql.includes('INSERT INTO assignments')) return { rows: [{ id: 90 }] };
|
|
return { rows: [] };
|
|
};
|
|
const app = Fastify({ trustProxy: 2 });
|
|
await app.register(cookie);
|
|
await app.register(rateLimit, { global: false });
|
|
app.decorate('pg', { query });
|
|
await app.register(api, { prefix: '/api' });
|
|
await app.ready();
|
|
const response = await app.inject({
|
|
method: 'PUT', url: '/api/assignments/class/50', cookies: { teach_session: 'x'.repeat(64) },
|
|
payload: {
|
|
title: 'Taalroute',
|
|
sequence: [{ widgetType: 'flits', mode: 'werken' }, { widgetType: 'hak', mode: 'werken' }],
|
|
},
|
|
});
|
|
assert.equal(response.statusCode, 200, response.body);
|
|
const insert = calls.find((call) => call.sql.includes('INSERT INTO assignments'));
|
|
const sequence = JSON.parse(insert.params[6]);
|
|
assert.equal(sequence[0].requiresProgress, true, 'flits heeft nu een zelfcheck als bewijs');
|
|
assert.equal(sequence[1].requiresProgress, true, 'hakken & plakken ook');
|
|
await app.close();
|
|
});
|