d3ro-voice/apps/desktop/tests/main/services/SoundEffectService-redteam-r1-15.test.ts

177 lines
5.3 KiB
TypeScript

// tests/main/services/SoundEffectService-redteam-r1-15.test.ts
// 회귀: Settings에서 효과음을 끄면(config.set → configSet) 재시작 없이 즉시 반영되어야 한다.
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import fs from 'fs'
import os from 'os'
import path from 'path'
const SOUND_DIR = path.join(os.tmpdir(), 'd3ro-vitest-sounds-r1-15')
const { execMock, execFileMock } = vi.hoisted(() => ({
execMock: vi.fn(),
execFileMock: vi.fn()
}))
vi.mock('child_process', async (importOriginal) => {
const actual = await importOriginal<typeof import('child_process')>()
return { ...actual, exec: execMock, execFile: execFileMock }
})
vi.mock('../../../src/main/utils/paths', () => ({
getSoundPath: (filename: string) => path.join(SOUND_DIR, filename)
}))
import {
configSet,
initInMemoryConfig,
resetInMemoryConfig
} from '../../../src/main/services/ConfigService'
import {
SoundEffectService,
getSoundEffectService,
type SoundEffectDeps
} from '../../../src/main/services/SoundEffectService'
function playCount(): number {
return execMock.mock.calls.length + execFileMock.mock.calls.length
}
beforeAll(() => {
fs.mkdirSync(SOUND_DIR, { recursive: true })
for (const name of ['recording-start.wav', 'recording-stop.wav', 'error.wav']) {
fs.writeFileSync(path.join(SOUND_DIR, name), Buffer.from('RIFF0000WAVE'))
}
})
describe('SoundEffectService — soundEnabled 변경 즉시 반영 (singleton + ConfigService)', () => {
beforeEach(() => {
execMock.mockReset()
execFileMock.mockReset()
initInMemoryConfig({ soundEnabled: true })
})
afterEach(() => {
getSoundEffectService().dispose()
resetInMemoryConfig()
})
it('Settings가 configSet으로 soundEnabled=false를 쓰면 다음 play()는 재생하지 않는다', () => {
const service = getSoundEffectService()
service.initialize()
expect(service.isEnabled()).toBe(true)
// SettingsModal → config:set 핸들러 → configSet 경로 (setEnabled를 거치지 않음)
configSet('soundEnabled', false)
expect(service.isEnabled()).toBe(false)
service.play('recording-start')
service.play('error')
expect(playCount()).toBe(0)
// 다시 켜면 곧바로 재생된다 (동기화로 들어온 값도 같은 경로)
configSet('soundEnabled', true)
expect(service.isEnabled()).toBe(true)
service.play('recording-stop')
expect(playCount()).toBe(1)
})
it('dispose 후에는 config 변경을 더 이상 구독하지 않는다', () => {
const service = getSoundEffectService()
service.initialize()
service.dispose()
configSet('soundEnabled', false)
expect(service.isEnabled()).toBe(true)
})
})
describe('SoundEffectService — 포트 주입 단위 테스트', () => {
function createDeps(initial: boolean): {
deps: SoundEffectDeps
emit: (enabled: boolean) => void
listenerCount: () => number
played: string[]
written: boolean[]
} {
const listeners = new Set<(enabled: boolean) => void>()
const played: string[] = []
const written: boolean[] = []
let value = initial
const deps: SoundEffectDeps = {
settings: {
read: () => value,
write: (enabled) => {
value = enabled
written.push(enabled)
listeners.forEach((l) => l(enabled))
},
subscribe: (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
}
},
output: {
resolvePath: (filename) => `/sounds/${filename}`,
load: (filePath) => (filePath.endsWith('error.wav') ? null : Buffer.from('wav')),
play: (filePath) => played.push(filePath)
}
}
return {
deps,
emit: (enabled) => {
value = enabled
listeners.forEach((l) => l(enabled))
},
listenerCount: () => listeners.size,
played,
written
}
}
it('초기값을 설정 포트에서 읽고, 외부 변경을 따라간다', () => {
const h = createDeps(false)
const service = new SoundEffectService(h.deps)
service.initialize()
service.play('recording-start')
expect(h.played).toEqual([])
h.emit(true)
service.play('recording-start')
service.play('chime')
expect(h.played).toEqual(['/sounds/recording-start.wav', '/sounds/recording-stop.wav'])
})
it('로드되지 않은 효과음은 건너뛴다', () => {
const h = createDeps(true)
const service = new SoundEffectService(h.deps)
service.initialize()
service.play('error')
service.play('cancel')
expect(h.played).toEqual([])
})
it('setEnabled는 설정 포트에 기록하고 상태를 갱신한다', () => {
const h = createDeps(true)
const service = new SoundEffectService(h.deps)
service.initialize()
service.setEnabled(false)
expect(h.written).toEqual([false])
expect(service.isEnabled()).toBe(false)
service.play('recording-start')
expect(h.played).toEqual([])
})
it('initialize를 두 번 호출해도 구독은 하나만 유지되고 dispose가 해제한다', () => {
const h = createDeps(true)
const service = new SoundEffectService(h.deps)
service.initialize()
service.initialize()
expect(h.listenerCount()).toBe(1)
service.dispose()
expect(h.listenerCount()).toBe(0)
})
})