import { describe, it, expect } from 'vitest' import { MORSE, NOVICE_LETTERS, OPERATOR_WORDS, wpmToTiming, DEFAULT_WPM, DEFAULT_TIMING, validatePhrase, pickChallenge, } from './morse' describe('MORSE map', () => { it('has all 26 letters', () => { const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') letters.forEach(l => expect(MORSE[l]).toBeDefined()) }) it('A is .-', () => expect(MORSE['A']).toBe('.-')) it('S is ...', () => expect(MORSE['S']).toBe('...')) it('O is ---', () => expect(MORSE['O']).toBe('---')) }) describe('NOVICE_LETTERS', () => { it('has 12 letters', () => expect(NOVICE_LETTERS).toHaveLength(12)) it('includes A E T I N S H R D L U O', () => { ;['A','E','T','I','N','S','H','R','D','L','U','O'].forEach(l => expect(NOVICE_LETTERS).toContain(l) ) }) }) describe('OPERATOR_WORDS', () => { it('has at least 20 words', () => expect(OPERATOR_WORDS.length).toBeGreaterThanOrEqual(20)) it('all words use only Novice letters', () => { const novice = new Set(['A','E','T','I','N','S','H','R','D','L','U','O']) OPERATOR_WORDS.forEach(word => { word.split('').forEach(l => expect(novice.has(l)).toBe(true)) }) }) }) describe('DEFAULT_WPM and DEFAULT_TIMING', () => { it('DEFAULT_WPM is 10', () => expect(DEFAULT_WPM).toBe(10)) it('DEFAULT_TIMING equals wpmToTiming(10)', () => { expect(DEFAULT_TIMING).toEqual(wpmToTiming(10)) }) }) describe('wpmToTiming', () => { it('10 WPM gives dot=120ms', () => { expect(wpmToTiming(10).dot).toBe(120) }) it('dash is 3x dot', () => { const t = wpmToTiming(10) expect(t.dash).toBe(t.dot * 3) }) it('20 WPM gives dot=60ms', () => { expect(wpmToTiming(20).dot).toBe(60) }) it('elementGap equals dot', () => { const t = wpmToTiming(10) expect(t.elementGap).toBe(t.dot) }) it('charGap is 3x dot', () => { const t = wpmToTiming(10) expect(t.charGap).toBe(t.dot * 3) }) it('wordGap is 7x dot', () => { const t = wpmToTiming(10) expect(t.wordGap).toBe(t.dot * 7) }) }) describe('validatePhrase', () => { it('accepts valid phrase for E (.)', () => { expect(validatePhrase('go', 'E')).toEqual({ valid: true }) }) it('accepts valid phrase for A (.-)', () => { expect(validatePhrase('go WALKING', 'A')).toEqual({ valid: true }) }) it('accepts valid phrase for S (...)', () => { expect(validatePhrase('go go go', 'S')).toEqual({ valid: true }) }) it('rejects wrong word count', () => { const r = validatePhrase('go go', 'S') expect(r.valid).toBe(false) expect(r.message).toContain('3') }) it('rejects long word for dot position', () => { const r = validatePhrase('WALKING go go', 'S') expect(r.valid).toBe(false) }) it('rejects short word for dash position', () => { const r = validatePhrase('go go', 'A') expect(r.valid).toBe(false) }) it('rejects unknown letter', () => { expect(validatePhrase('test', '1')).toEqual({ valid: false, message: 'Unknown letter' }) }) it('handles empty phrase', () => { expect(validatePhrase('', 'E')).toEqual({ valid: false, message: expect.stringContaining('1') }) }) }) describe('pickChallenge', () => { it('always returns item from pool', () => { const pool = ['A', 'E', 'T', 'I'] const result = pickChallenge(pool, {}, []) expect(pool).toContain(result) }) it('avoids last 3 items when pool is large enough', () => { const pool = ['A', 'E', 'T', 'I', 'N', 'S'] const history = ['A', 'E', 'T'] for (let i = 0; i < 20; i++) { const result = pickChallenge(pool, {}, history) expect(['A','E','T']).not.toContain(result) } }) it('prefers items with lower accuracy', () => { const pool = ['A', 'B'] const stats = { A: { correct: 9, attempts: 10 }, B: { correct: 1, attempts: 10 }, } let bCount = 0 for (let i = 0; i < 100; i++) { if (pickChallenge(pool, stats, []) === 'B') bCount++ } expect(bCount).toBeGreaterThan(60) }) })