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 })); }); });