diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 9d6d34cd..6ad7ff35 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -2,6 +2,10 @@ name: check on: push: + branches: + - main + tags: + - 'v*' pull_request: permissions: {} diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 2ad43950..f9a29802 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -6,38 +6,17 @@ import { useSerialPoll } from './hooks/useSerialPoll' import { addDictionaryEntry, configureDesktopApi, - clearHistory, - copyText, - deleteHistoryItem, getAppStatus, - getHistory, getRecordingLevel, - getShortcutStatus, - getSettings, getMicrophones, getReadiness, - listGpuDevices, - listLanguages, - listModels, quitApp, removeStaleInstalls, - repairLegacyShortcut, - retryShortcut, - onSetupEvent, - setSettings, - setMicrophone, - startSetup, - stopRecording, - testInputDevice, - testMicrophoneFallback, toggleRecording, } from './tauri' +import { deferred, resetDesktopApiMocks } from './test/desktopApiHarness' import type { AppStatus, - ComponentId, - SettingsChange, - SetupEvent, - SetupPlanId, } from './generated/ipc' const previewDesktopApi = createPreviewDesktopApi() @@ -51,61 +30,10 @@ const { vi.mock('./tauri', async (importOriginal) => { const actual = await importOriginal() - return { - ...actual, - addDictionaryEntry: vi.fn((spoken: string, written: string) => - actual.addDictionaryEntry(spoken, written)), - clearHistory: vi.fn(() => Promise.resolve(0)), - copyText: vi.fn((text: string) => actual.copyText(text)), - deleteHistoryItem: vi.fn(() => Promise.resolve(false)), - getAppStatus: vi.fn(() => actual.getAppStatus()), - getHistory: vi.fn(() => actual.getHistory()), - getRecordingLevel: vi.fn(() => actual.getRecordingLevel()), - getShortcutStatus: vi.fn(() => actual.getShortcutStatus()), - getSettings: vi.fn(() => actual.getSettings()), - getMicrophones: vi.fn(() => actual.getMicrophones()), - getReadiness: vi.fn(() => actual.getReadiness()), - listGpuDevices: vi.fn((refresh?: boolean) => actual.listGpuDevices(refresh)), - listLanguages: vi.fn(() => actual.listLanguages()), - listModels: vi.fn(() => actual.listModels()), - quitApp: vi.fn(() => actual.quitApp()), - setSettings: vi.fn((settings: SettingsChange) => actual.setSettings(settings)), - setMicrophone: vi.fn((id: string | null) => actual.setMicrophone(id)), - startSetup: vi.fn((plan: SetupPlanId, managedCopy?: boolean) => actual.startSetup(plan, managedCopy)), - removeStaleInstalls: vi.fn(() => actual.removeStaleInstalls()), - repairLegacyShortcut: vi.fn(() => actual.repairLegacyShortcut()), - repairManaged: vi.fn((component: ComponentId) => actual.repairManaged(component)), - onSetupEvent: vi.fn((handler: (event: SetupEvent) => void) => actual.onSetupEvent(handler)), - retryShortcut: vi.fn(() => actual.retryShortcut()), - stopRecording: vi.fn((activation: string) => actual.stopRecording(activation)), - testInputDevice: vi.fn((id: string | null) => actual.testInputDevice(id)), - testMicrophoneFallback: vi.fn(() => actual.testMicrophoneFallback()), - toggleRecording: vi.fn(() => actual.toggleRecording()), - } + const { createDesktopApiMocks } = await import('./test/desktopApiHarness') + return { ...actual, ...createDesktopApiMocks(actual) } }) -function deferred() { - const state: { - resolve: ((value: T | PromiseLike) => void) | null - reject: ((reason?: unknown) => void) | null - } = { resolve: null, reject: null } - const promise = new Promise((resolvePromise, rejectPromise) => { - state.resolve = resolvePromise - state.reject = rejectPromise - }) - return { - promise, - resolve(value: T | PromiseLike) { - if (!state.resolve) throw new Error('deferred promise is not initialized') - state.resolve(value) - }, - reject(reason?: unknown) { - if (!state.reject) throw new Error('deferred promise is not initialized') - state.reject(reason) - }, - } -} - function SerialPollHarness({ request, onResult, @@ -125,57 +53,8 @@ describe('Echo desktop shell', () => { localStorage.removeItem('echo-shortcut-verified-at') localStorage.removeItem('echo-shortcut-verified-identity') const actual = await vi.importActual('./tauri') - vi.mocked(addDictionaryEntry).mockReset() - vi.mocked(addDictionaryEntry).mockImplementation((spoken, written) => - actual.addDictionaryEntry(spoken, written)) - vi.mocked(clearHistory).mockReset() - vi.mocked(clearHistory).mockResolvedValue(3) - vi.mocked(copyText).mockReset() - vi.mocked(copyText).mockImplementation((text) => actual.copyText(text)) - vi.mocked(deleteHistoryItem).mockReset() - vi.mocked(deleteHistoryItem).mockResolvedValue(true) - vi.mocked(getAppStatus).mockReset() - vi.mocked(getAppStatus).mockImplementation(() => actual.getAppStatus()) - vi.mocked(getHistory).mockReset() - vi.mocked(getHistory).mockImplementation(() => actual.getHistory()) - vi.mocked(getRecordingLevel).mockReset() - vi.mocked(getRecordingLevel).mockImplementation(() => actual.getRecordingLevel()) - vi.mocked(getShortcutStatus).mockReset() - vi.mocked(getShortcutStatus).mockImplementation(() => actual.getShortcutStatus()) - vi.mocked(setSettings).mockReset() - vi.mocked(setSettings).mockImplementation((settings) => actual.setSettings(settings)) - vi.mocked(repairLegacyShortcut).mockReset() - vi.mocked(repairLegacyShortcut).mockImplementation(() => actual.repairLegacyShortcut()) - vi.mocked(retryShortcut).mockReset() - vi.mocked(retryShortcut).mockImplementation(() => actual.retryShortcut()) - vi.mocked(stopRecording).mockReset() - vi.mocked(stopRecording).mockImplementation((activation) => actual.stopRecording(activation)) - vi.mocked(toggleRecording).mockReset() - vi.mocked(toggleRecording).mockImplementation(() => actual.toggleRecording()) - vi.mocked(getSettings).mockReset() - vi.mocked(getSettings).mockImplementation(() => actual.getSettings()) - vi.mocked(getMicrophones).mockReset() - vi.mocked(getMicrophones).mockImplementation(() => actual.getMicrophones()) - vi.mocked(getReadiness).mockReset() - vi.mocked(getReadiness).mockImplementation(() => actual.getReadiness()) - vi.mocked(listGpuDevices).mockReset() - vi.mocked(listGpuDevices).mockImplementation((refresh) => actual.listGpuDevices(refresh)) - vi.mocked(listLanguages).mockReset() - vi.mocked(listLanguages).mockImplementation(() => actual.listLanguages()) - vi.mocked(setMicrophone).mockReset() - vi.mocked(setMicrophone).mockImplementation((id) => actual.setMicrophone(id)) - vi.mocked(startSetup).mockReset() - vi.mocked(startSetup).mockImplementation((plan, managedCopy) => actual.startSetup(plan, managedCopy)) - vi.mocked(listModels).mockReset() - vi.mocked(listModels).mockImplementation(() => actual.listModels()) - vi.mocked(quitApp).mockReset() - vi.mocked(quitApp).mockImplementation(() => actual.quitApp()) - vi.mocked(onSetupEvent).mockReset() - vi.mocked(onSetupEvent).mockImplementation((handler) => actual.onSetupEvent(handler)) - vi.mocked(testInputDevice).mockReset() - vi.mocked(testInputDevice).mockImplementation((id) => actual.testInputDevice(id)) - vi.mocked(testMicrophoneFallback).mockReset() - vi.mocked(testMicrophoneFallback).mockImplementation(() => actual.testMicrophoneFallback()) + const mocks = await import('./tauri') + resetDesktopApiMocks(mocks, actual) }) it('waits for a status request to settle before polling again', async () => { diff --git a/frontend/src/app/AppSettings.test.tsx b/frontend/src/app/AppSettings.test.tsx index 189e15de..0c1b2f76 100644 --- a/frontend/src/app/AppSettings.test.tsx +++ b/frontend/src/app/AppSettings.test.tsx @@ -3,41 +3,21 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import App from '../App' import { createPreviewDesktopApi } from '../api/previewDesktopApi' import { - addDictionaryEntry, configureDesktopApi, - clearHistory, - copyText, - deleteHistoryItem, - getAppStatus, - getHistory, - getRecordingLevel, - getShortcutStatus, getSettings, getMicrophones, getReadiness, listGpuDevices, - listLanguages, - listModels, - quitApp, - repairLegacyShortcut, - retryShortcut, repairManaged, onSetupEvent, setSettings, - setMicrophone, - startSetup, - stopRecording, testInputDevice, - testMicrophoneFallback, } from '../tauri' +import { deferred, resetDesktopApiMocks } from '../test/desktopApiHarness' import type { - ComponentId, ComponentStatus, LanguageOptions, ResolvedSpeechEngine, - SettingsChange, - SetupEvent, - SetupPlanId, } from '../generated/ipc' const previewDesktopApi = createPreviewDesktopApi() @@ -66,60 +46,10 @@ function requireFixture(value: T | null | undefined, description: string): T vi.mock('../tauri', async (importOriginal) => { const actual = await importOriginal() - return { - ...actual, - addDictionaryEntry: vi.fn((spoken: string, written: string) => - actual.addDictionaryEntry(spoken, written)), - clearHistory: vi.fn(() => Promise.resolve(0)), - copyText: vi.fn((text: string) => actual.copyText(text)), - deleteHistoryItem: vi.fn(() => Promise.resolve(false)), - getAppStatus: vi.fn(() => actual.getAppStatus()), - getHistory: vi.fn(() => actual.getHistory()), - getRecordingLevel: vi.fn(() => actual.getRecordingLevel()), - getShortcutStatus: vi.fn(() => actual.getShortcutStatus()), - getSettings: vi.fn(() => actual.getSettings()), - getMicrophones: vi.fn(() => actual.getMicrophones()), - getReadiness: vi.fn(() => actual.getReadiness()), - listGpuDevices: vi.fn((refresh?: boolean) => actual.listGpuDevices(refresh)), - listLanguages: vi.fn(() => actual.listLanguages()), - listModels: vi.fn(() => actual.listModels()), - quitApp: vi.fn(() => actual.quitApp()), - setSettings: vi.fn((settings: SettingsChange) => actual.setSettings(settings)), - setMicrophone: vi.fn((id: string | null) => actual.setMicrophone(id)), - startSetup: vi.fn((plan: SetupPlanId, managedCopy?: boolean) => actual.startSetup(plan, managedCopy)), - removeStaleInstalls: vi.fn(() => actual.removeStaleInstalls()), - repairLegacyShortcut: vi.fn(() => actual.repairLegacyShortcut()), - repairManaged: vi.fn((component: ComponentId) => actual.repairManaged(component)), - onSetupEvent: vi.fn((handler: (event: SetupEvent) => void) => actual.onSetupEvent(handler)), - retryShortcut: vi.fn(() => actual.retryShortcut()), - stopRecording: vi.fn((activation: string) => actual.stopRecording(activation)), - testInputDevice: vi.fn((id: string | null) => actual.testInputDevice(id)), - testMicrophoneFallback: vi.fn(() => actual.testMicrophoneFallback()), - } + const { createDesktopApiMocks } = await import('../test/desktopApiHarness') + return { ...actual, ...createDesktopApiMocks(actual) } }) -function deferred() { - const state: { - resolve: ((value: T | PromiseLike) => void) | null - reject: ((reason?: unknown) => void) | null - } = { resolve: null, reject: null } - const promise = new Promise((resolvePromise, rejectPromise) => { - state.resolve = resolvePromise - state.reject = rejectPromise - }) - return { - promise, - resolve(value: T | PromiseLike) { - if (!state.resolve) throw new Error('deferred promise is not initialized') - state.resolve(value) - }, - reject(reason?: unknown) { - if (!state.reject) throw new Error('deferred promise is not initialized') - state.reject(reason) - }, - } -} - const nextRunEngineCases: Record { localStorage.removeItem('echo-shortcut-verified-at') localStorage.removeItem('echo-shortcut-verified-identity') const actual = await vi.importActual('../tauri') - vi.mocked(addDictionaryEntry).mockReset() - vi.mocked(addDictionaryEntry).mockImplementation((spoken, written) => - actual.addDictionaryEntry(spoken, written)) - vi.mocked(clearHistory).mockReset() - vi.mocked(clearHistory).mockResolvedValue(3) - vi.mocked(copyText).mockReset() - vi.mocked(copyText).mockImplementation((text) => actual.copyText(text)) - vi.mocked(deleteHistoryItem).mockReset() - vi.mocked(deleteHistoryItem).mockResolvedValue(true) - vi.mocked(getAppStatus).mockReset() - vi.mocked(getAppStatus).mockImplementation(() => actual.getAppStatus()) - vi.mocked(getHistory).mockReset() - vi.mocked(getHistory).mockImplementation(() => actual.getHistory()) - vi.mocked(getRecordingLevel).mockReset() - vi.mocked(getRecordingLevel).mockImplementation(() => actual.getRecordingLevel()) - vi.mocked(getShortcutStatus).mockReset() - vi.mocked(getShortcutStatus).mockImplementation(() => actual.getShortcutStatus()) - vi.mocked(setSettings).mockReset() - vi.mocked(setSettings).mockImplementation((settings) => actual.setSettings(settings)) - vi.mocked(repairLegacyShortcut).mockReset() - vi.mocked(repairLegacyShortcut).mockImplementation(() => actual.repairLegacyShortcut()) - vi.mocked(retryShortcut).mockReset() - vi.mocked(retryShortcut).mockImplementation(() => actual.retryShortcut()) - vi.mocked(stopRecording).mockReset() - vi.mocked(stopRecording).mockImplementation((activation) => actual.stopRecording(activation)) - vi.mocked(getSettings).mockReset() - vi.mocked(getSettings).mockImplementation(() => actual.getSettings()) - vi.mocked(getMicrophones).mockReset() - vi.mocked(getMicrophones).mockImplementation(() => actual.getMicrophones()) - vi.mocked(getReadiness).mockReset() - vi.mocked(getReadiness).mockImplementation(() => actual.getReadiness()) - vi.mocked(listGpuDevices).mockReset() - vi.mocked(listGpuDevices).mockImplementation((refresh) => actual.listGpuDevices(refresh)) - vi.mocked(listLanguages).mockReset() - vi.mocked(listLanguages).mockImplementation(() => actual.listLanguages()) - vi.mocked(setMicrophone).mockReset() - vi.mocked(setMicrophone).mockImplementation((id) => actual.setMicrophone(id)) - vi.mocked(startSetup).mockReset() - vi.mocked(startSetup).mockImplementation((plan, managedCopy) => actual.startSetup(plan, managedCopy)) - vi.mocked(listModels).mockReset() - vi.mocked(listModels).mockImplementation(() => actual.listModels()) - vi.mocked(quitApp).mockReset() - vi.mocked(quitApp).mockImplementation(() => actual.quitApp()) - vi.mocked(onSetupEvent).mockReset() - vi.mocked(onSetupEvent).mockImplementation((handler) => actual.onSetupEvent(handler)) - vi.mocked(testInputDevice).mockReset() - vi.mocked(testInputDevice).mockImplementation((id) => actual.testInputDevice(id)) - vi.mocked(testMicrophoneFallback).mockReset() - vi.mocked(testMicrophoneFallback).mockImplementation(() => actual.testMicrophoneFallback()) + const mocks = await import('../tauri') + resetDesktopApiMocks(mocks, actual) }) it('unlistens when the Settings subscription setup resolves after navigation', async () => { diff --git a/frontend/src/app/AppSetup.test.tsx b/frontend/src/app/AppSetup.test.tsx index 6a1fe649..3c01632c 100644 --- a/frontend/src/app/AppSetup.test.tsx +++ b/frontend/src/app/AppSetup.test.tsx @@ -4,37 +4,18 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import App from '../App' import { createPreviewDesktopApi } from '../api/previewDesktopApi' import { - addDictionaryEntry, configureDesktopApi, - clearHistory, - copyText, - deleteHistoryItem, - getAppStatus, - getHistory, - getRecordingLevel, - getShortcutStatus, getSettings, getMicrophones, getReadiness, - listGpuDevices, - listLanguages, - listModels, - quitApp, - repairLegacyShortcut, - retryShortcut, onSetupEvent, - setSettings, setMicrophone, startSetup, - stopRecording, testInputDevice, - testMicrophoneFallback, } from '../tauri' +import { deferred, resetDesktopApiMocks } from '../test/desktopApiHarness' import type { - ComponentId, - SettingsChange, SetupEvent, - SetupPlanId, } from '../generated/ipc' const previewDesktopApi = createPreviewDesktopApi() @@ -50,60 +31,10 @@ function requireFixture(value: T | null | undefined, description: string): T vi.mock('../tauri', async (importOriginal) => { const actual = await importOriginal() - return { - ...actual, - addDictionaryEntry: vi.fn((spoken: string, written: string) => - actual.addDictionaryEntry(spoken, written)), - clearHistory: vi.fn(() => Promise.resolve(0)), - copyText: vi.fn((text: string) => actual.copyText(text)), - deleteHistoryItem: vi.fn(() => Promise.resolve(false)), - getAppStatus: vi.fn(() => actual.getAppStatus()), - getHistory: vi.fn(() => actual.getHistory()), - getRecordingLevel: vi.fn(() => actual.getRecordingLevel()), - getShortcutStatus: vi.fn(() => actual.getShortcutStatus()), - getSettings: vi.fn(() => actual.getSettings()), - getMicrophones: vi.fn(() => actual.getMicrophones()), - getReadiness: vi.fn(() => actual.getReadiness()), - listGpuDevices: vi.fn((refresh?: boolean) => actual.listGpuDevices(refresh)), - listLanguages: vi.fn(() => actual.listLanguages()), - listModels: vi.fn(() => actual.listModels()), - quitApp: vi.fn(() => actual.quitApp()), - setSettings: vi.fn((settings: SettingsChange) => actual.setSettings(settings)), - setMicrophone: vi.fn((id: string | null) => actual.setMicrophone(id)), - startSetup: vi.fn((plan: SetupPlanId, managedCopy?: boolean) => actual.startSetup(plan, managedCopy)), - removeStaleInstalls: vi.fn(() => actual.removeStaleInstalls()), - repairLegacyShortcut: vi.fn(() => actual.repairLegacyShortcut()), - repairManaged: vi.fn((component: ComponentId) => actual.repairManaged(component)), - onSetupEvent: vi.fn((handler: (event: SetupEvent) => void) => actual.onSetupEvent(handler)), - retryShortcut: vi.fn(() => actual.retryShortcut()), - stopRecording: vi.fn((activation: string) => actual.stopRecording(activation)), - testInputDevice: vi.fn((id: string | null) => actual.testInputDevice(id)), - testMicrophoneFallback: vi.fn(() => actual.testMicrophoneFallback()), - } + const { createDesktopApiMocks } = await import('../test/desktopApiHarness') + return { ...actual, ...createDesktopApiMocks(actual) } }) -function deferred() { - const state: { - resolve: ((value: T | PromiseLike) => void) | null - reject: ((reason?: unknown) => void) | null - } = { resolve: null, reject: null } - const promise = new Promise((resolvePromise, rejectPromise) => { - state.resolve = resolvePromise - state.reject = rejectPromise - }) - return { - promise, - resolve(value: T | PromiseLike) { - if (!state.resolve) throw new Error('deferred promise is not initialized') - state.resolve(value) - }, - reject(reason?: unknown) { - if (!state.reject) throw new Error('deferred promise is not initialized') - state.reject(reason) - }, - } -} - describe('Echo desktop shell', () => { beforeEach(async () => { vi.restoreAllMocks() @@ -112,55 +43,8 @@ describe('Echo desktop shell', () => { localStorage.removeItem('echo-shortcut-verified-at') localStorage.removeItem('echo-shortcut-verified-identity') const actual = await vi.importActual('../tauri') - vi.mocked(addDictionaryEntry).mockReset() - vi.mocked(addDictionaryEntry).mockImplementation((spoken, written) => - actual.addDictionaryEntry(spoken, written)) - vi.mocked(clearHistory).mockReset() - vi.mocked(clearHistory).mockResolvedValue(3) - vi.mocked(copyText).mockReset() - vi.mocked(copyText).mockImplementation((text) => actual.copyText(text)) - vi.mocked(deleteHistoryItem).mockReset() - vi.mocked(deleteHistoryItem).mockResolvedValue(true) - vi.mocked(getAppStatus).mockReset() - vi.mocked(getAppStatus).mockImplementation(() => actual.getAppStatus()) - vi.mocked(getHistory).mockReset() - vi.mocked(getHistory).mockImplementation(() => actual.getHistory()) - vi.mocked(getRecordingLevel).mockReset() - vi.mocked(getRecordingLevel).mockImplementation(() => actual.getRecordingLevel()) - vi.mocked(getShortcutStatus).mockReset() - vi.mocked(getShortcutStatus).mockImplementation(() => actual.getShortcutStatus()) - vi.mocked(setSettings).mockReset() - vi.mocked(setSettings).mockImplementation((settings) => actual.setSettings(settings)) - vi.mocked(repairLegacyShortcut).mockReset() - vi.mocked(repairLegacyShortcut).mockImplementation(() => actual.repairLegacyShortcut()) - vi.mocked(retryShortcut).mockReset() - vi.mocked(retryShortcut).mockImplementation(() => actual.retryShortcut()) - vi.mocked(stopRecording).mockReset() - vi.mocked(stopRecording).mockImplementation((activation) => actual.stopRecording(activation)) - vi.mocked(getSettings).mockReset() - vi.mocked(getSettings).mockImplementation(() => actual.getSettings()) - vi.mocked(getMicrophones).mockReset() - vi.mocked(getMicrophones).mockImplementation(() => actual.getMicrophones()) - vi.mocked(getReadiness).mockReset() - vi.mocked(getReadiness).mockImplementation(() => actual.getReadiness()) - vi.mocked(listGpuDevices).mockReset() - vi.mocked(listGpuDevices).mockImplementation((refresh) => actual.listGpuDevices(refresh)) - vi.mocked(listLanguages).mockReset() - vi.mocked(listLanguages).mockImplementation(() => actual.listLanguages()) - vi.mocked(setMicrophone).mockReset() - vi.mocked(setMicrophone).mockImplementation((id) => actual.setMicrophone(id)) - vi.mocked(startSetup).mockReset() - vi.mocked(startSetup).mockImplementation((plan, managedCopy) => actual.startSetup(plan, managedCopy)) - vi.mocked(listModels).mockReset() - vi.mocked(listModels).mockImplementation(() => actual.listModels()) - vi.mocked(quitApp).mockReset() - vi.mocked(quitApp).mockImplementation(() => actual.quitApp()) - vi.mocked(onSetupEvent).mockReset() - vi.mocked(onSetupEvent).mockImplementation((handler) => actual.onSetupEvent(handler)) - vi.mocked(testInputDevice).mockReset() - vi.mocked(testInputDevice).mockImplementation((id) => actual.testInputDevice(id)) - vi.mocked(testMicrophoneFallback).mockReset() - vi.mocked(testMicrophoneFallback).mockImplementation(() => actual.testMicrophoneFallback()) + const mocks = await import('../tauri') + resetDesktopApiMocks(mocks, actual) }) it('unlistens when SetupChecklist subscription setup resolves after unmount', async () => { diff --git a/frontend/src/app/AppShortcut.test.tsx b/frontend/src/app/AppShortcut.test.tsx index e9c6b910..94a74849 100644 --- a/frontend/src/app/AppShortcut.test.tsx +++ b/frontend/src/app/AppShortcut.test.tsx @@ -4,38 +4,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import App from '../App' import { createPreviewDesktopApi } from '../api/previewDesktopApi' import { - addDictionaryEntry, configureDesktopApi, - clearHistory, - copyText, - deleteHistoryItem, getAppStatus, - getHistory, - getRecordingLevel, getShortcutStatus, getSettings, getMicrophones, getReadiness, - listGpuDevices, - listLanguages, listModels, - quitApp, repairLegacyShortcut, retryShortcut, - onSetupEvent, - setSettings, - setMicrophone, - startSetup, stopRecording, - testInputDevice, - testMicrophoneFallback, } from '../tauri' +import { deferred, resetDesktopApiMocks } from '../test/desktopApiHarness' import type { AppStatus, - ComponentId, - SettingsChange, - SetupEvent, - SetupPlanId, ShortcutStatus, } from '../generated/ipc' @@ -49,60 +31,10 @@ const { vi.mock('../tauri', async (importOriginal) => { const actual = await importOriginal() - return { - ...actual, - addDictionaryEntry: vi.fn((spoken: string, written: string) => - actual.addDictionaryEntry(spoken, written)), - clearHistory: vi.fn(() => Promise.resolve(0)), - copyText: vi.fn((text: string) => actual.copyText(text)), - deleteHistoryItem: vi.fn(() => Promise.resolve(false)), - getAppStatus: vi.fn(() => actual.getAppStatus()), - getHistory: vi.fn(() => actual.getHistory()), - getRecordingLevel: vi.fn(() => actual.getRecordingLevel()), - getShortcutStatus: vi.fn(() => actual.getShortcutStatus()), - getSettings: vi.fn(() => actual.getSettings()), - getMicrophones: vi.fn(() => actual.getMicrophones()), - getReadiness: vi.fn(() => actual.getReadiness()), - listGpuDevices: vi.fn((refresh?: boolean) => actual.listGpuDevices(refresh)), - listLanguages: vi.fn(() => actual.listLanguages()), - listModels: vi.fn(() => actual.listModels()), - quitApp: vi.fn(() => actual.quitApp()), - setSettings: vi.fn((settings: SettingsChange) => actual.setSettings(settings)), - setMicrophone: vi.fn((id: string | null) => actual.setMicrophone(id)), - startSetup: vi.fn((plan: SetupPlanId, managedCopy?: boolean) => actual.startSetup(plan, managedCopy)), - removeStaleInstalls: vi.fn(() => actual.removeStaleInstalls()), - repairLegacyShortcut: vi.fn(() => actual.repairLegacyShortcut()), - repairManaged: vi.fn((component: ComponentId) => actual.repairManaged(component)), - onSetupEvent: vi.fn((handler: (event: SetupEvent) => void) => actual.onSetupEvent(handler)), - retryShortcut: vi.fn(() => actual.retryShortcut()), - stopRecording: vi.fn((activation: string) => actual.stopRecording(activation)), - testInputDevice: vi.fn((id: string | null) => actual.testInputDevice(id)), - testMicrophoneFallback: vi.fn(() => actual.testMicrophoneFallback()), - } + const { createDesktopApiMocks } = await import('../test/desktopApiHarness') + return { ...actual, ...createDesktopApiMocks(actual) } }) -function deferred() { - const state: { - resolve: ((value: T | PromiseLike) => void) | null - reject: ((reason?: unknown) => void) | null - } = { resolve: null, reject: null } - const promise = new Promise((resolvePromise, rejectPromise) => { - state.resolve = resolvePromise - state.reject = rejectPromise - }) - return { - promise, - resolve(value: T | PromiseLike) { - if (!state.resolve) throw new Error('deferred promise is not initialized') - state.resolve(value) - }, - reject(reason?: unknown) { - if (!state.reject) throw new Error('deferred promise is not initialized') - state.reject(reason) - }, - } -} - function activeShortcut( activation: string | null = null, effective = 'Super+Alt+Space', @@ -135,55 +67,8 @@ describe('Echo desktop shell', () => { localStorage.removeItem('echo-shortcut-verified-at') localStorage.removeItem('echo-shortcut-verified-identity') const actual = await vi.importActual('../tauri') - vi.mocked(addDictionaryEntry).mockReset() - vi.mocked(addDictionaryEntry).mockImplementation((spoken, written) => - actual.addDictionaryEntry(spoken, written)) - vi.mocked(clearHistory).mockReset() - vi.mocked(clearHistory).mockResolvedValue(3) - vi.mocked(copyText).mockReset() - vi.mocked(copyText).mockImplementation((text) => actual.copyText(text)) - vi.mocked(deleteHistoryItem).mockReset() - vi.mocked(deleteHistoryItem).mockResolvedValue(true) - vi.mocked(getAppStatus).mockReset() - vi.mocked(getAppStatus).mockImplementation(() => actual.getAppStatus()) - vi.mocked(getHistory).mockReset() - vi.mocked(getHistory).mockImplementation(() => actual.getHistory()) - vi.mocked(getRecordingLevel).mockReset() - vi.mocked(getRecordingLevel).mockImplementation(() => actual.getRecordingLevel()) - vi.mocked(getShortcutStatus).mockReset() - vi.mocked(getShortcutStatus).mockImplementation(() => actual.getShortcutStatus()) - vi.mocked(setSettings).mockReset() - vi.mocked(setSettings).mockImplementation((settings) => actual.setSettings(settings)) - vi.mocked(repairLegacyShortcut).mockReset() - vi.mocked(repairLegacyShortcut).mockImplementation(() => actual.repairLegacyShortcut()) - vi.mocked(retryShortcut).mockReset() - vi.mocked(retryShortcut).mockImplementation(() => actual.retryShortcut()) - vi.mocked(stopRecording).mockReset() - vi.mocked(stopRecording).mockImplementation((activation) => actual.stopRecording(activation)) - vi.mocked(getSettings).mockReset() - vi.mocked(getSettings).mockImplementation(() => actual.getSettings()) - vi.mocked(getMicrophones).mockReset() - vi.mocked(getMicrophones).mockImplementation(() => actual.getMicrophones()) - vi.mocked(getReadiness).mockReset() - vi.mocked(getReadiness).mockImplementation(() => actual.getReadiness()) - vi.mocked(listGpuDevices).mockReset() - vi.mocked(listGpuDevices).mockImplementation((refresh) => actual.listGpuDevices(refresh)) - vi.mocked(listLanguages).mockReset() - vi.mocked(listLanguages).mockImplementation(() => actual.listLanguages()) - vi.mocked(setMicrophone).mockReset() - vi.mocked(setMicrophone).mockImplementation((id) => actual.setMicrophone(id)) - vi.mocked(startSetup).mockReset() - vi.mocked(startSetup).mockImplementation((plan, managedCopy) => actual.startSetup(plan, managedCopy)) - vi.mocked(listModels).mockReset() - vi.mocked(listModels).mockImplementation(() => actual.listModels()) - vi.mocked(quitApp).mockReset() - vi.mocked(quitApp).mockImplementation(() => actual.quitApp()) - vi.mocked(onSetupEvent).mockReset() - vi.mocked(onSetupEvent).mockImplementation((handler) => actual.onSetupEvent(handler)) - vi.mocked(testInputDevice).mockReset() - vi.mocked(testInputDevice).mockImplementation((id) => actual.testInputDevice(id)) - vi.mocked(testMicrophoneFallback).mockReset() - vi.mocked(testMicrophoneFallback).mockImplementation(() => actual.testMicrophoneFallback()) + const mocks = await import('../tauri') + resetDesktopApiMocks(mocks, actual) }) it('shows one fixed toggle shortcut with no customization controls', async () => { diff --git a/frontend/src/test/desktopApiHarness.ts b/frontend/src/test/desktopApiHarness.ts new file mode 100644 index 00000000..63915e4a --- /dev/null +++ b/frontend/src/test/desktopApiHarness.ts @@ -0,0 +1,104 @@ +import { vi } from 'vitest' +import type { DesktopApi } from '../api/DesktopApi' + +export function createDesktopApiMocks(actual: DesktopApi): DesktopApi { + return { + getAppStatus: vi.fn(actual.getAppStatus), + getShortcutStatus: vi.fn(actual.getShortcutStatus), + retryShortcut: vi.fn(actual.retryShortcut), + repairLegacyShortcut: vi.fn(actual.repairLegacyShortcut), + getHistory: vi.fn(actual.getHistory), + deleteHistoryItem: vi.fn(actual.deleteHistoryItem), + clearHistory: vi.fn(actual.clearHistory), + getDictionary: vi.fn(actual.getDictionary), + addDictionaryEntry: vi.fn(actual.addDictionaryEntry), + addDictionaryEntriesBatch: vi.fn(actual.addDictionaryEntriesBatch), + removeDictionaryEntry: vi.fn(actual.removeDictionaryEntry), + startDictionaryTrainingSample: vi.fn(actual.startDictionaryTrainingSample), + finishDictionaryTrainingSample: vi.fn(actual.finishDictionaryTrainingSample), + cancelDictionaryTrainingSample: vi.fn(actual.cancelDictionaryTrainingSample), + toggleRecording: vi.fn(actual.toggleRecording), + stopRecording: vi.fn(actual.stopRecording), + getRecordingLevel: vi.fn(actual.getRecordingLevel), + copyText: vi.fn(actual.copyText), + quitApp: vi.fn(actual.quitApp), + removeStaleInstalls: vi.fn(actual.removeStaleInstalls), + getSettings: vi.fn(actual.getSettings), + listModels: vi.fn(actual.listModels), + listLanguages: vi.fn(actual.listLanguages), + setSettings: vi.fn(actual.setSettings), + listGpuDevices: vi.fn(actual.listGpuDevices), + getMicrophones: vi.fn(actual.getMicrophones), + setMicrophone: vi.fn(actual.setMicrophone), + testInputDevice: vi.fn(actual.testInputDevice), + testMicrophoneFallback: vi.fn(actual.testMicrophoneFallback), + getReadiness: vi.fn(actual.getReadiness), + startSetup: vi.fn(actual.startSetup), + repairManaged: vi.fn(actual.repairManaged), + verifyManaged: vi.fn(actual.verifyManaged), + removeManaged: vi.fn(actual.removeManaged), + cancelSetup: vi.fn(actual.cancelSetup), + onSetupEvent: vi.fn(actual.onSetupEvent), + onSettingsEvent: vi.fn(actual.onSettingsEvent), + } +} + +export function resetDesktopApiMocks(mocks: DesktopApi, actual: DesktopApi): void { + vi.mocked(mocks.getAppStatus).mockReset().mockImplementation(actual.getAppStatus) + vi.mocked(mocks.getShortcutStatus).mockReset().mockImplementation(actual.getShortcutStatus) + vi.mocked(mocks.retryShortcut).mockReset().mockImplementation(actual.retryShortcut) + vi.mocked(mocks.repairLegacyShortcut).mockReset().mockImplementation(actual.repairLegacyShortcut) + vi.mocked(mocks.getHistory).mockReset().mockImplementation(actual.getHistory) + vi.mocked(mocks.deleteHistoryItem).mockReset().mockResolvedValue(true) + vi.mocked(mocks.clearHistory).mockReset().mockResolvedValue(3) + vi.mocked(mocks.getDictionary).mockReset().mockImplementation(actual.getDictionary) + vi.mocked(mocks.addDictionaryEntry).mockReset().mockImplementation(actual.addDictionaryEntry) + vi.mocked(mocks.addDictionaryEntriesBatch).mockReset().mockImplementation(actual.addDictionaryEntriesBatch) + vi.mocked(mocks.removeDictionaryEntry).mockReset().mockImplementation(actual.removeDictionaryEntry) + vi.mocked(mocks.startDictionaryTrainingSample).mockReset().mockImplementation(actual.startDictionaryTrainingSample) + vi.mocked(mocks.finishDictionaryTrainingSample).mockReset().mockImplementation(actual.finishDictionaryTrainingSample) + vi.mocked(mocks.cancelDictionaryTrainingSample).mockReset().mockImplementation(actual.cancelDictionaryTrainingSample) + vi.mocked(mocks.toggleRecording).mockReset().mockImplementation(actual.toggleRecording) + vi.mocked(mocks.stopRecording).mockReset().mockImplementation(actual.stopRecording) + vi.mocked(mocks.getRecordingLevel).mockReset().mockImplementation(actual.getRecordingLevel) + vi.mocked(mocks.copyText).mockReset().mockImplementation(actual.copyText) + vi.mocked(mocks.quitApp).mockReset().mockImplementation(actual.quitApp) + vi.mocked(mocks.removeStaleInstalls).mockReset().mockImplementation(actual.removeStaleInstalls) + vi.mocked(mocks.getSettings).mockReset().mockImplementation(actual.getSettings) + vi.mocked(mocks.listModels).mockReset().mockImplementation(actual.listModels) + vi.mocked(mocks.listLanguages).mockReset().mockImplementation(actual.listLanguages) + vi.mocked(mocks.setSettings).mockReset().mockImplementation(actual.setSettings) + vi.mocked(mocks.listGpuDevices).mockReset().mockImplementation(actual.listGpuDevices) + vi.mocked(mocks.getMicrophones).mockReset().mockImplementation(actual.getMicrophones) + vi.mocked(mocks.setMicrophone).mockReset().mockImplementation(actual.setMicrophone) + vi.mocked(mocks.testInputDevice).mockReset().mockImplementation(actual.testInputDevice) + vi.mocked(mocks.testMicrophoneFallback).mockReset().mockImplementation(actual.testMicrophoneFallback) + vi.mocked(mocks.getReadiness).mockReset().mockImplementation(actual.getReadiness) + vi.mocked(mocks.startSetup).mockReset().mockImplementation(actual.startSetup) + vi.mocked(mocks.repairManaged).mockReset().mockImplementation(actual.repairManaged) + vi.mocked(mocks.verifyManaged).mockReset().mockImplementation(actual.verifyManaged) + vi.mocked(mocks.removeManaged).mockReset().mockImplementation(actual.removeManaged) + vi.mocked(mocks.cancelSetup).mockReset().mockImplementation(actual.cancelSetup) + vi.mocked(mocks.onSetupEvent).mockReset().mockImplementation(actual.onSetupEvent) + vi.mocked(mocks.onSettingsEvent).mockReset().mockImplementation(actual.onSettingsEvent) +} + +export function deferred() { + let resolvePromise: ((value: T | PromiseLike) => void) | undefined + let rejectPromise: ((reason?: unknown) => void) | undefined + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve + rejectPromise = reject + }) + return { + promise, + resolve(value: T | PromiseLike) { + if (!resolvePromise) throw new Error('deferred promise is not initialized') + resolvePromise(value) + }, + reject(reason?: unknown) { + if (!rejectPromise) throw new Error('deferred promise is not initialized') + rejectPromise(reason) + }, + } +}