515 tests across 38 files. Branch coverage 88%, function coverage 83% on testable logic (stores, composables, api, utils, services, router). New test files: websocket, useLoginSounds, useMobileBackButton, useControllerNav, routes. Extended: rpc-client (99.5%), container store (100%). Fixed: useNavSounds AudioContext mock, type errors across tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
212 lines
5.3 KiB
TypeScript
212 lines
5.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
// Mock Audio globally
|
|
class MockAudio {
|
|
src = ''
|
|
volume = 1
|
|
loop = false
|
|
currentTime = 0
|
|
play = vi.fn().mockResolvedValue(undefined)
|
|
pause = vi.fn()
|
|
addEventListener = vi.fn()
|
|
}
|
|
|
|
vi.stubGlobal('Audio', MockAudio)
|
|
|
|
// Mock fetch for playLoopStart
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
|
}))
|
|
|
|
// Mock AudioContext
|
|
const mockBufferSource = {
|
|
buffer: null as AudioBuffer | null,
|
|
connect: vi.fn(),
|
|
start: vi.fn(),
|
|
stop: vi.fn(),
|
|
}
|
|
|
|
const mockMediaElementSource = {
|
|
connect: vi.fn(),
|
|
}
|
|
|
|
const mockGainNode = {
|
|
gain: {
|
|
value: 1,
|
|
setValueAtTime: vi.fn(),
|
|
linearRampToValueAtTime: vi.fn(),
|
|
exponentialRampToValueAtTime: vi.fn(),
|
|
},
|
|
connect: vi.fn(),
|
|
}
|
|
|
|
const mockAudioContext = {
|
|
state: 'running' as AudioContextState,
|
|
currentTime: 0,
|
|
destination: {},
|
|
resume: vi.fn().mockResolvedValue(undefined),
|
|
createOscillator: vi.fn().mockReturnValue({
|
|
type: 'sine',
|
|
frequency: { value: 440, setValueAtTime: vi.fn() },
|
|
connect: vi.fn(),
|
|
start: vi.fn(),
|
|
stop: vi.fn(),
|
|
}),
|
|
createGain: vi.fn().mockReturnValue({ ...mockGainNode, gain: { ...mockGainNode.gain } }),
|
|
createBufferSource: vi.fn().mockReturnValue({ ...mockBufferSource }),
|
|
createMediaElementSource: vi.fn().mockReturnValue({ ...mockMediaElementSource }),
|
|
decodeAudioData: vi.fn().mockResolvedValue({} as AudioBuffer),
|
|
}
|
|
|
|
vi.stubGlobal('AudioContext', vi.fn().mockImplementation(() => ({ ...mockAudioContext })))
|
|
|
|
import {
|
|
playPop,
|
|
playLoginSuccessWhoosh,
|
|
playTypingSound,
|
|
playIntroTyping,
|
|
stopIntroTyping,
|
|
playWelcomeNoderunnerSpeech,
|
|
playTypingTick,
|
|
resumeAudioContext,
|
|
startSynthwave,
|
|
stopSynthwave,
|
|
playLoopStart,
|
|
playKeyboardTypingSound,
|
|
playDashboardLoadOomph,
|
|
} from '../useLoginSounds'
|
|
|
|
describe('useLoginSounds', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('playPop', () => {
|
|
it('creates Audio with pop.mp3 and plays it', () => {
|
|
playPop()
|
|
// Audio constructor was called (via MockAudio)
|
|
expect(MockAudio.prototype.constructor).toBeDefined()
|
|
})
|
|
|
|
it('does not throw', () => {
|
|
expect(() => playPop()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('playLoginSuccessWhoosh', () => {
|
|
it('does not throw', () => {
|
|
expect(() => playLoginSuccessWhoosh()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('playTypingSound', () => {
|
|
it('does not throw', () => {
|
|
expect(() => playTypingSound()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('playIntroTyping', () => {
|
|
it('does not throw', () => {
|
|
expect(() => playIntroTyping()).not.toThrow()
|
|
})
|
|
|
|
it('creates a looping audio element', () => {
|
|
playIntroTyping()
|
|
// Does not throw, creates audio
|
|
})
|
|
})
|
|
|
|
describe('stopIntroTyping', () => {
|
|
it('does not throw when no audio playing', () => {
|
|
expect(() => stopIntroTyping()).not.toThrow()
|
|
})
|
|
|
|
it('stops audio that was started', () => {
|
|
playIntroTyping()
|
|
expect(() => stopIntroTyping()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('playWelcomeNoderunnerSpeech', () => {
|
|
it('does not throw', () => {
|
|
expect(() => playWelcomeNoderunnerSpeech()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('playTypingTick', () => {
|
|
it('does not throw', () => {
|
|
expect(() => playTypingTick()).not.toThrow()
|
|
})
|
|
|
|
it('can be called multiple times (pool rotation)', () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
expect(() => playTypingTick()).not.toThrow()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('resumeAudioContext', () => {
|
|
it('does not throw', () => {
|
|
expect(() => resumeAudioContext()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('startSynthwave', () => {
|
|
it('does not throw when no audio context', () => {
|
|
// Without calling resumeAudioContext first, context might be null
|
|
expect(() => startSynthwave()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('stopSynthwave', () => {
|
|
it('does not throw when nothing is playing', () => {
|
|
expect(() => stopSynthwave()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('playLoopStart', () => {
|
|
it('does not throw when no audio context', () => {
|
|
expect(() => playLoopStart()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('playKeyboardTypingSound', () => {
|
|
it('does not throw when no audio context', () => {
|
|
expect(() => playKeyboardTypingSound()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('playDashboardLoadOomph', () => {
|
|
it('does not throw when no audio context', () => {
|
|
expect(() => playDashboardLoadOomph()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('audio context lifecycle', () => {
|
|
it('resumeAudioContext then startSynthwave does not throw', () => {
|
|
resumeAudioContext()
|
|
expect(() => startSynthwave()).not.toThrow()
|
|
})
|
|
|
|
it('resumeAudioContext then stopSynthwave does not throw', () => {
|
|
resumeAudioContext()
|
|
expect(() => stopSynthwave()).not.toThrow()
|
|
})
|
|
|
|
it('resumeAudioContext then playKeyboardTypingSound does not throw', () => {
|
|
resumeAudioContext()
|
|
expect(() => playKeyboardTypingSound()).not.toThrow()
|
|
})
|
|
|
|
it('resumeAudioContext then playDashboardLoadOomph does not throw', () => {
|
|
resumeAudioContext()
|
|
expect(() => playDashboardLoadOomph()).not.toThrow()
|
|
})
|
|
|
|
it('resumeAudioContext then playLoopStart does not throw', () => {
|
|
resumeAudioContext()
|
|
expect(() => playLoopStart()).not.toThrow()
|
|
})
|
|
})
|
|
})
|