Some checks failed
Test and deploy staging / staging (push) Has been cancelled
Dutch and Korean (explained in English) and English (explained in Dutch) from A1 to C1, with a Hangul level for Korean. Lessons with notes, words and sentences generate multiple-choice, typing, sentence building, cloze, listening and matching exercises. Endless challenges: a seeded daily challenge, sprint, survival, mixed practice, level tests, spaced-repetition reviews and generated drills (numbers, clock times, de/het, plurals, Dutch and Korean conjugation, particles, Hangul reading, irregular verbs). Progress is stored in localStorage with export/import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { allEntries, allLessons } from './course';
|
|
import { lessonSession, reviewExercise } from './exercises';
|
|
import { seeded } from './random';
|
|
import { optionKey as normalize } from './text';
|
|
import en from './courses/en';
|
|
import ko from './courses/ko';
|
|
import nl from './courses/nl';
|
|
import { drills as nlDrills } from './drills/nl';
|
|
import { drills as koDrills } from './drills/ko';
|
|
import { drills as enDrills } from './drills/en';
|
|
import type { Exercise } from './types';
|
|
|
|
const courses = { nl, ko, en };
|
|
const drillSets = { nl: nlDrills, ko: koDrills, en: enDrills };
|
|
|
|
function checkExercise(ex: Exercise) {
|
|
if (ex.type === 'choose') {
|
|
expect(ex.options).toContain(ex.answer);
|
|
expect(
|
|
new Set(ex.options.map(normalize)).size,
|
|
`${ex.key}: duplicate options ${ex.options}`
|
|
).toBe(ex.options.length);
|
|
expect(ex.options.length).toBeGreaterThanOrEqual(2);
|
|
expect(ex.options.length, ex.key).toBeLessThanOrEqual(4);
|
|
}
|
|
if (ex.type === 'type') expect(ex.accept.length).toBeGreaterThan(0);
|
|
if (ex.type === 'build') expect(ex.tiles.length).toBeGreaterThan(0);
|
|
if (ex.type === 'match') {
|
|
expect(new Set(ex.pairs.map((p) => normalize(p.gloss))).size, ex.key).toBe(ex.pairs.length);
|
|
expect(new Set(ex.pairs.map((p) => normalize(p.target))).size, ex.key).toBe(ex.pairs.length);
|
|
}
|
|
}
|
|
|
|
describe.each(Object.entries(courses))('%s course', (code, course) => {
|
|
it('has unique lesson ids and content in every lesson', () => {
|
|
const ids = allLessons(course).map((l) => l.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
for (const lesson of allLessons(course)) {
|
|
expect(lesson.words.length, lesson.id).toBeGreaterThanOrEqual(4);
|
|
const ids = [...lesson.words, ...lesson.sentences].map((e) => e.id);
|
|
expect(
|
|
ids.filter((id, i) => ids.indexOf(id) !== i),
|
|
lesson.id
|
|
).toEqual([]);
|
|
expect(lesson.notes.length, lesson.id).toBeGreaterThan(50);
|
|
for (const drill of lesson.drills) {
|
|
expect(drill.wrong.map(normalize), `${lesson.id}: ${drill.prompt}`).not.toContain(
|
|
normalize(drill.answer)
|
|
);
|
|
expect(new Set(drill.wrong.map(normalize)).size, `${lesson.id}: ${drill.prompt}`).toBe(
|
|
drill.wrong.length
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('generates valid lesson sessions', () => {
|
|
for (const lesson of allLessons(course))
|
|
for (const opts of [
|
|
{ typing: true, audio: true },
|
|
{ typing: false, audio: false }
|
|
]) {
|
|
const session = lessonSession(course, lesson, seeded(lesson.id), opts);
|
|
expect(session.length, lesson.id).toBeGreaterThan(lesson.words.length);
|
|
session.forEach(checkExercise);
|
|
}
|
|
});
|
|
|
|
it('generates review exercises for every entry', () => {
|
|
const pool = allEntries(course);
|
|
const rng = seeded(code);
|
|
for (const entry of pool)
|
|
checkExercise(reviewExercise(entry, pool, rng, { typing: true, audio: true }, rng()));
|
|
});
|
|
|
|
it('generates endless drills at every level', () => {
|
|
const rng = seeded(`drills-${code}`);
|
|
for (const drill of drillSets[code as keyof typeof drillSets])
|
|
for (let level = 1; level <= 5; level++)
|
|
for (let i = 0; i < 60; i++)
|
|
for (const typing of [true, false])
|
|
checkExercise(drill.make({ rng, level, opts: { typing, audio: true }, course }));
|
|
});
|
|
});
|