diff --git a/src/hooks/useSettings.test.ts b/src/hooks/useSettings.test.ts index 3bd65ebb..045978d9 100644 --- a/src/hooks/useSettings.test.ts +++ b/src/hooks/useSettings.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { renderHook, act, waitFor } from '@testing-library/react' +import { invoke } from '@tauri-apps/api/core' import type { Settings } from '../types' import { useSettings } from './useSettings' @@ -46,6 +47,8 @@ const mockInvokeFn = vi.fn((cmd: string, args?: Record): Promis return Promise.resolve(null) }) +const nativeInvoke = vi.mocked(invoke) + vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(), })) @@ -65,10 +68,29 @@ async function renderLoadedSettings(): Promise { return result.current.settings } +function changedSettings(): Settings { + return { + auto_pull_interval_minutes: null, + autogit_enabled: false, + autogit_idle_threshold_seconds: 120, + autogit_inactive_threshold_seconds: 45, + auto_advance_inbox_after_organize: false, + telemetry_consent: null, + crash_reporting_enabled: null, + analytics_enabled: null, + anonymous_id: null, + release_channel: null, + theme_mode: null, + ui_language: 'zh-Hans', + default_ai_agent: null, + } +} + describe('useSettings', () => { beforeEach(() => { vi.clearAllMocks() mockSettingsStore = { ...defaultSettings } + nativeInvoke.mockResolvedValue(undefined) }) it('returns empty settings initially', () => { @@ -92,6 +114,15 @@ describe('useSettings', () => { expect(mockInvokeFn).toHaveBeenCalledWith('get_settings', {}) }) + it('loads settings from native invoke when Tauri globals are not detectable', async () => { + nativeInvoke.mockResolvedValueOnce({ ...savedSettings, ui_language: 'zh-Hans' }) + + const settings = await renderLoadedSettings() + + expect(settings.ui_language).toBe('zh-Hans') + expect(mockInvokeFn).not.toHaveBeenCalledWith('get_settings', {}) + }) + it('normalizes a legacy beta release channel back to stable on load', async () => { mockSettingsStore = { ...savedSettings, @@ -119,21 +150,7 @@ describe('useSettings', () => { expect(result.current.loaded).toBe(true) }) - const newSettings: Settings = { - auto_pull_interval_minutes: null, - autogit_enabled: false, - autogit_idle_threshold_seconds: 120, - autogit_inactive_threshold_seconds: 45, - auto_advance_inbox_after_organize: false, - telemetry_consent: null, - crash_reporting_enabled: null, - analytics_enabled: null, - anonymous_id: null, - release_channel: null, - theme_mode: null, - ui_language: 'zh-Hans', - default_ai_agent: null, - } + const newSettings = changedSettings() await act(async () => { await result.current.saveSettings(newSettings) @@ -143,6 +160,27 @@ describe('useSettings', () => { expect(result.current.settings).toEqual(newSettings) }) + it('saves settings through native invoke when Tauri globals are not detectable', async () => { + const { result } = renderHook(() => useSettings()) + + await waitFor(() => { + expect(result.current.loaded).toBe(true) + }) + + const newSettings = changedSettings() + + vi.clearAllMocks() + nativeInvoke.mockResolvedValueOnce(null) + + await act(async () => { + await result.current.saveSettings(newSettings) + }) + + expect(nativeInvoke).toHaveBeenCalledWith('save_settings', { settings: newSettings }) + expect(mockInvokeFn).not.toHaveBeenCalled() + expect(result.current.settings).toEqual(newSettings) + }) + it('handles load error gracefully', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) mockInvokeFn.mockImplementationOnce(() => Promise.reject(new Error('no config'))) diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 443ec5a2..3f2ca2e6 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -7,8 +7,22 @@ import { normalizeReleaseChannel, serializeReleaseChannel } from '../lib/release import { normalizeThemeMode } from '../lib/themeMode' import type { Settings } from '../types' -function tauriCall(command: string, tauriArgs: Record, mockArgs?: Record): Promise { - return isTauri() ? invoke(command, tauriArgs) : mockInvoke(command, mockArgs ?? tauriArgs) +async function invokeNativeIfAvailable(command: string, tauriArgs: Record): Promise { + try { + return await invoke(command, tauriArgs) + } catch (err) { + if (isTauri()) throw err + return undefined + } +} + +async function tauriCall(command: string, tauriArgs: Record, mockArgs?: Record): Promise { + if (isTauri()) return invoke(command, tauriArgs) + + const nativeResult = await invokeNativeIfAvailable(command, tauriArgs) + if (nativeResult !== undefined) return nativeResult + + return mockInvoke(command, mockArgs ?? tauriArgs) } const EMPTY_SETTINGS: Settings = {