fix: persist native language settings

This commit is contained in:
lucaronin
2026-04-26 09:17:28 +02:00
parent 5336236e79
commit 60238fbe78
2 changed files with 69 additions and 17 deletions

View File

@@ -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<string, unknown>): 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<Settings> {
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')))

View File

@@ -7,8 +7,22 @@ import { normalizeReleaseChannel, serializeReleaseChannel } from '../lib/release
import { normalizeThemeMode } from '../lib/themeMode'
import type { Settings } from '../types'
function tauriCall<T>(command: string, tauriArgs: Record<string, unknown>, mockArgs?: Record<string, unknown>): Promise<T> {
return isTauri() ? invoke<T>(command, tauriArgs) : mockInvoke<T>(command, mockArgs ?? tauriArgs)
async function invokeNativeIfAvailable<T>(command: string, tauriArgs: Record<string, unknown>): Promise<T | undefined> {
try {
return await invoke<T>(command, tauriArgs)
} catch (err) {
if (isTauri()) throw err
return undefined
}
}
async function tauriCall<T>(command: string, tauriArgs: Record<string, unknown>, mockArgs?: Record<string, unknown>): Promise<T> {
if (isTauri()) return invoke<T>(command, tauriArgs)
const nativeResult = await invokeNativeIfAvailable<T>(command, tauriArgs)
if (nativeResult !== undefined) return nativeResult
return mockInvoke<T>(command, mockArgs ?? tauriArgs)
}
const EMPTY_SETTINGS: Settings = {