Daily Practice per section: mixed drills, stricter unlock, tick-and-skip one-tap

- One-tap mode no longer shows the answer when you're right: a green tick
  flashes and it skips straight to the next question. Wrong answers still
  show the full correction and now wait for an explicit Next tap.
- Every tab (Prepositions, Articles, Possessives, Pronouns) gets its own
  pinned Daily Practice card. Sessions mix questions from every lesson in
  the tab via a shuffle-bag over lesson ids.
- Daily Practice is locked until every lesson in its section has been
  passed (one full round at 70%); the locked card shows x/y progress.
- Finishing a daily session offers 'Continue: <section> Daily Practice'
  cycling to the next tab with an unlocked daily, with play again and
  back to menu as secondary options.
This commit is contained in:
2026-07-21 07:09:40 +00:00
parent 80f7e0eca2
commit c4c28f46ed
3 changed files with 159 additions and 44 deletions
+26
View File
@@ -258,6 +258,32 @@ describe('no nominative anywhere', () => {
});
});
// ── Daily practice (per-tab mixed sessions) ─────────────────
describe('daily practice', () => {
const dailies = LEVELS.filter(l => l.daily);
it('every tab has exactly one daily level', () => {
expect(dailies.map(l => l.tab).sort()).toEqual(['article', 'poss', 'prep', 'pronoun']);
});
it.each(dailies.map(l => [l.id, l.tab]))('daily %s mixes every lesson type of the %s tab', (id, tab) => {
const expected = new Set(LEVELS.filter(l => l.tab === tab && !l.daily).map(l => l.type));
const seen = new Set(runSession(id, 80).map(q => q.type));
// Mixed definite-article lessons legitimately emit chunk questions too.
for (const t of seen) expect([...expected, 'chunk'], `unexpected type ${t} in ${tab} daily`).toContain(t);
for (const t of expected) expect(seen, `${tab} daily never produced a ${t} question`).toContain(t);
});
it('daily questions always carry an answer', () => {
for (const d of dailies) {
for (const q of runSession(d.id, 100)) {
expect(q.ans).toBeTruthy();
}
}
});
});
// ── Every level generates without crashing ──────────────────
describe('all levels', () => {