fix(voice): let a click on the RecordingTip actually cancel the recording
This commit is contained in:
parent
83cf9133ac
commit
c5b3bdedb6
2 changed files with 94 additions and 1 deletions
|
|
@ -14,6 +14,11 @@ function safeSendToRenderer(channel: string, data: unknown): void {
|
|||
}
|
||||
}
|
||||
|
||||
/** 두 IPC 전송 경로(invoke/send)가 공유하는 단일 취소 동작 */
|
||||
function cancelVoiceSession(): void {
|
||||
getVoiceModeService().cancelSession()
|
||||
}
|
||||
|
||||
export function registerVoiceHandlers(): void {
|
||||
// 음성 세션 에러/경고를 메인 윈도우로 브로드캐스트 —
|
||||
// 모델 미설치/엔진 실패 등의 경고가 recording tip(3초) 외에도
|
||||
|
|
@ -55,11 +60,18 @@ export function registerVoiceHandlers(): void {
|
|||
}
|
||||
})
|
||||
|
||||
// 취소는 두 경로로 들어온다 — 메인 윈도우는 invoke(handle), RecordingTip 팝업은
|
||||
// popupAPI.send(= ipcRenderer.send, 단방향)라서 ipcMain.on이 따로 있어야 한다.
|
||||
// handle()은 send() 메시지를 절대 받지 않는다.
|
||||
ipcMain.handle(IPC_CHANNELS.VOICE.CANCEL_RECORDING, async (_event, _params: CancelRecordingParams) => {
|
||||
getVoiceModeService().cancelSession()
|
||||
cancelVoiceSession()
|
||||
return ipcSuccess(undefined)
|
||||
})
|
||||
|
||||
ipcMain.on(IPC_CHANNELS.VOICE.CANCEL_RECORDING, () => {
|
||||
cancelVoiceSession()
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.VOICE.GET_STATE, async () => {
|
||||
return ipcSuccess(getVoiceModeService().getState())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||
|
||||
// ipcMain.handle(invoke 전용)과 ipcMain.on(send 전용)을 분리해 기록한다.
|
||||
// Electron에서 ipcRenderer.send 메시지는 handle 등록에 절대 도달하지 않으므로
|
||||
// 이 둘을 섞어 쓰는 하네스로는 RecordingTip 클릭 취소 회귀를 잡을 수 없다.
|
||||
const invokeHandlers = vi.hoisted(
|
||||
() => new Map<string, (event: unknown, ...args: unknown[]) => Promise<unknown>>()
|
||||
)
|
||||
const sendListeners = vi.hoisted(
|
||||
() => new Map<string, Array<(event: unknown, ...args: unknown[]) => void>>()
|
||||
)
|
||||
const voice = vi.hoisted(() => ({
|
||||
cancelSession: vi.fn(),
|
||||
on: vi.fn(),
|
||||
startSession: vi.fn(async () => undefined),
|
||||
stopSession: vi.fn(async () => undefined),
|
||||
getState: vi.fn(() => ({})),
|
||||
currentSession: null
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: vi.fn((channel: string, handler: (event: unknown, ...args: unknown[]) => Promise<unknown>) => {
|
||||
invokeHandlers.set(channel, handler)
|
||||
}),
|
||||
on: vi.fn((channel: string, listener: (event: unknown, ...args: unknown[]) => void) => {
|
||||
const list = sendListeners.get(channel) ?? []
|
||||
list.push(listener)
|
||||
sendListeners.set(channel, list)
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/main/services/VoiceModeService', () => ({
|
||||
getVoiceModeService: () => voice
|
||||
}))
|
||||
vi.mock('../../../src/main/windows/WindowManager', () => ({
|
||||
getMainWindow: () => null
|
||||
}))
|
||||
|
||||
/** ipcRenderer.send 의미론 — ipcMain.on 리스너에만 전달된다 */
|
||||
function simulateRendererSend(channel: string, ...args: unknown[]): number {
|
||||
const listeners = sendListeners.get(channel) ?? []
|
||||
for (const listener of listeners) listener({}, ...args)
|
||||
return listeners.length
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
invokeHandlers.clear()
|
||||
sendListeners.clear()
|
||||
const mod = await import('../../../src/main/ipc/voice-handlers')
|
||||
mod.registerVoiceHandlers()
|
||||
})
|
||||
|
||||
describe('VOICE.CANCEL_RECORDING — RecordingTip 팝업(send) 경로', () => {
|
||||
it('popupAPI.send로 보낸 취소가 세션을 취소한다', () => {
|
||||
const delivered = simulateRendererSend(IPC_CHANNELS.VOICE.CANCEL_RECORDING, {})
|
||||
|
||||
expect(delivered).toBe(1)
|
||||
expect(voice.cancelSession).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('RecordingTip 스크립트가 보내는 채널 문자열과 IPC 상수가 일치한다', () => {
|
||||
expect(IPC_CHANNELS.VOICE.CANCEL_RECORDING).toBe('voice:cancelRecording')
|
||||
})
|
||||
})
|
||||
|
||||
describe('VOICE.CANCEL_RECORDING — 메인 윈도우(invoke) 경로', () => {
|
||||
it('invoke 경로도 여전히 세션을 취소하고 성공을 반환한다', async () => {
|
||||
const handler = invokeHandlers.get(IPC_CHANNELS.VOICE.CANCEL_RECORDING)
|
||||
if (!handler) throw new Error('cancel invoke handler was not registered')
|
||||
|
||||
const result = await handler({}, {})
|
||||
|
||||
expect(voice.cancelSession).toHaveBeenCalledTimes(1)
|
||||
expect(result).toMatchObject({ success: true })
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue