Files
Zauberwald/src/game/typing.test.ts
T
gurixandClaude Opus 4.6 5fe66cb315 feat(01-04): implement typing exercise engine with TDD
- generateLetters: random 8-12 letters from level key set
- handleKeyPress: correct/wrong input with adaptive tempo
- Adaptive tempo: 3s start, -200ms after 2x correct, +500ms on error
- Bounds: min 1500ms, max 4000ms
- 18 passing tests covering all behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:10:53 +02:00

175 lines
5.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
generateLetters,
createExerciseState,
handleKeyPress,
getCurrentLetter,
isExerciseComplete,
} from "./typing";
import { getKeysUpToLevel } from "./levels";
describe("generateLetters", () => {
it("returns 8-12 letters for level 1", () => {
for (let i = 0; i < 20; i++) {
const letters = generateLetters(1);
expect(letters.length).toBeGreaterThanOrEqual(8);
expect(letters.length).toBeLessThanOrEqual(12);
}
});
it("only uses keys from level 1 key set (f, j, space)", () => {
const validKeys = ["f", "j", " "];
for (let i = 0; i < 20; i++) {
const letters = generateLetters(1);
for (const letter of letters) {
expect(validKeys).toContain(letter);
}
}
});
it("only uses keys from levels 1-3 key set for level 3", () => {
const validKeys = getKeysUpToLevel(3);
for (let i = 0; i < 20; i++) {
const letters = generateLetters(3);
for (const letter of letters) {
expect(validKeys).toContain(letter);
}
}
});
});
describe("createExerciseState", () => {
it("sets initial interval to 3000ms", () => {
const state = createExerciseState(1);
expect(state.intervalMs).toBe(3000);
});
it("starts at letter index 0 with 0 correct, 0 errors, 0 streak", () => {
const state = createExerciseState(1);
expect(state.currentLetterIndex).toBe(0);
expect(state.totalCorrect).toBe(0);
expect(state.totalErrors).toBe(0);
expect(state.correctStreak).toBe(0);
});
});
describe("handleKeyPress", () => {
it("increments currentLetterIndex and totalCorrect on correct key", () => {
const state = createExerciseState(1);
const expected = state.letters[0]!;
const result = handleKeyPress(state, expected);
expect(result.correct).toBe(true);
expect(state.currentLetterIndex).toBe(1);
expect(state.totalCorrect).toBe(1);
});
it("does NOT increment currentLetterIndex on wrong key, increments totalErrors", () => {
const state = createExerciseState(1);
// Pick a key that is definitely wrong
const expected = state.letters[0]!;
const wrongKey = expected === "f" ? "j" : "f";
const result = handleKeyPress(state, wrongKey);
expect(result.correct).toBe(false);
expect(state.currentLetterIndex).toBe(0);
expect(state.totalErrors).toBe(1);
});
it("increments correctStreak on correct key", () => {
const state = createExerciseState(1);
const expected = state.letters[0]!;
handleKeyPress(state, expected);
expect(state.correctStreak).toBe(1);
});
it("resets correctStreak to 0 on error", () => {
const state = createExerciseState(1);
// Get one correct to build streak
handleKeyPress(state, state.letters[0]!);
expect(state.correctStreak).toBe(1);
// Now wrong key
const expected = state.letters[1]!;
const wrongKey = expected === "f" ? "j" : "f";
handleKeyPress(state, wrongKey);
expect(state.correctStreak).toBe(0);
});
it("decreases interval by 200ms after 2 correct in sequence", () => {
const state = createExerciseState(1);
expect(state.intervalMs).toBe(3000);
handleKeyPress(state, state.letters[0]!);
handleKeyPress(state, state.letters[1]!);
expect(state.intervalMs).toBe(2800);
});
it("increases interval by 500ms after 1 error", () => {
const state = createExerciseState(1);
// Force an error
const expected = state.letters[0]!;
const wrongKey = expected === "f" ? "j" : "f";
handleKeyPress(state, wrongKey);
expect(state.intervalMs).toBe(3500);
});
it("interval never goes below 1500ms", () => {
const state = createExerciseState(1);
// Force interval down to minimum
state.intervalMs = 1600;
state.correctStreak = 1;
handleKeyPress(state, state.letters[0]!);
// After 2 correct streak, -200ms: 1600 -> 1500 (clamped)
expect(state.intervalMs).toBe(1500);
// Try again: force streak
state.correctStreak = 1;
handleKeyPress(state, state.letters[1]!);
// Should stay at 1500
expect(state.intervalMs).toBe(1500);
});
it("interval never goes above 4000ms", () => {
const state = createExerciseState(1);
state.intervalMs = 3800;
const expected = state.letters[0]!;
const wrongKey = expected === "f" ? "j" : "f";
handleKeyPress(state, wrongKey);
// 3800 + 500 = 4300, clamped to 4000
expect(state.intervalMs).toBe(4000);
});
it("exercise is complete when currentLetterIndex equals letters.length", () => {
const state = createExerciseState(1);
// Type all letters correctly
let lastResult = { correct: false, exerciseComplete: false, newInterval: 0 };
for (let i = 0; i < state.letters.length; i++) {
lastResult = handleKeyPress(state, state.letters[i]!);
}
expect(lastResult.exerciseComplete).toBe(true);
expect(isExerciseComplete(state)).toBe(true);
});
});
describe("getCurrentLetter", () => {
it("returns the current letter", () => {
const state = createExerciseState(1);
expect(getCurrentLetter(state)).toBe(state.letters[0]);
});
it("returns null when exercise is complete", () => {
const state = createExerciseState(1);
state.currentLetterIndex = state.letters.length;
expect(getCurrentLetter(state)).toBeNull();
});
});
describe("isExerciseComplete", () => {
it("returns false when exercise is in progress", () => {
const state = createExerciseState(1);
expect(isExerciseComplete(state)).toBe(false);
});
it("returns true when all letters are typed", () => {
const state = createExerciseState(1);
state.currentLetterIndex = state.letters.length;
expect(isExerciseComplete(state)).toBe(true);
});
});