chore: 官网拆分 homepage + 关于页瘦身 + 端口 5175(当前可用状态基线)

This commit is contained in:
Logan.Li committed 2026-10-14 20:31:51 +08:00
1 parent 5eb11826db
commit be151cb68f
85 files changed
+4420 -1530

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
import { act, renderHook } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { useTheme } from '../../src/hooks/useTheme'
import { useSettingsStore } from '../../src/stores/settingsStore'
import { darkColors, lightColors } from '../../src/themes/app-theme'
afterEach(() => vi.unstubAllGlobals())
it('keeps the returned theme, surfaces and borders synchronized with system changes', () => {
let matches = false
const listeners = new Set<() => void>()
vi.stubGlobal('matchMedia', () => ({
get matches() {
return matches
},
addEventListener: (_event: string, listener: () => void) => listeners.add(listener),
removeEventListener: (_event: string, listener: () => void) => listeners.delete(listener),
}))
const original = useSettingsStore.getState().settings
useSettingsStore.setState({ settings: { ...original, theme: 'system' } })
const { result, unmount } = renderHook(() => useTheme())
expect(result.current.effective).toBe('light')
expect(document.documentElement.style.getPropertyValue('--hairline')).toBe(lightColors.hairline)
act(() => {
matches = true
listeners.forEach((listener) => listener())
})
expect(result.current.effective).toBe('dark')
expect(document.documentElement.dataset.theme).toBe('dark')
expect(document.documentElement.style.getPropertyValue('--hairline')).toBe(darkColors.hairline)
act(() => useSettingsStore.setState({ settings: { ...original, theme: 'light' } }))
expect(result.current.effective).toBe('light')
unmount()
expect(listeners.size).toBe(0)
useSettingsStore.setState({ settings: original })
})
+41
View File
@@ -0,0 +1,41 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { invokeMock } = vi.hoisted(() => ({ invokeMock: vi.fn() }))
vi.mock('@tauri-apps/api/core', () => ({ invoke: invokeMock }))
vi.mock('../../src/platform/runtime', () => ({ isTauriRuntime: () => true }))
import { invokeEnvelope, invokeNative, NativeCommandError } from '../../src/platform/invoke'
describe('native error boundary', () => {
beforeEach(() => vi.clearAllMocks())
it('preserves business failures as envelopes', async () => {
const response = { success: false, error: 'bad data', errorCode: 'native.invalidBase64' }
invokeMock.mockResolvedValueOnce(response)
expect(await invokeNative('write_file')).toBe(response)
})
it('normalizes structured rejections without losing their code or cause', async () => {
const cause = { error: 'invalid encoding', errorCode: 'native.invalidBase64' }
invokeMock.mockRejectedValueOnce(cause)
const error = await invokeNative('write_file').catch((value: unknown) => value)
expect(error).toBeInstanceOf(NativeCommandError)
expect(error).toMatchObject({
command: 'write_file',
detail: 'invalid encoding',
errorCode: cause.errorCode,
originalError: cause,
})
expect((error as Error).message).not.toContain('[object Object]')
})
it('retains codes when adapting rejected calls to envelopes', async () => {
invokeMock.mockRejectedValueOnce({ error: 'join failed', errorCode: 'native.taskJoin' })
expect(await invokeEnvelope('read_file')).toMatchObject({ success: false, errorCode: 'native.taskJoin' })
})
it('preserves plain rejection messages', async () => {
invokeMock.mockRejectedValueOnce('custom native failure')
await expect(invokeNative('test')).rejects.toThrow('custom native failure')
})
})
+4
View File
@@ -29,4 +29,8 @@ describe('settings migration', () => {
const settings = normalizeAppSettings({ language: 'fr-FR' })
expect(isAppLanguage(settings.language)).toBe(true)
})
it('rejects unsupported saved themes', () => {
expect(normalizeAppSettings({ theme: 'invalid' }).theme).toBe('dark')
expect(normalizeAppSettings({ theme: 'system' }).theme).toBe('system')
})
})
+71
View File
@@ -0,0 +1,71 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
function deferred<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((done) => {
resolve = done
})
return { promise, resolve }
}
let store: typeof import('../../src/stores/settingsStore').useSettingsStore
beforeEach(async () => {
vi.resetModules()
vi.mocked(window.opsxApi.settingsGet).mockReset().mockResolvedValue({ success: true })
vi.mocked(window.opsxApi.settingsSet).mockReset().mockResolvedValue({ success: true })
store = (await import('../../src/stores/settingsStore')).useSettingsStore
})
describe('settings persistence', () => {
it('serializes writes while keeping later edits visible', async () => {
const first = deferred<{ success: boolean }>()
vi.mocked(window.opsxApi.settingsSet).mockImplementationOnce(() => first.promise)
const saveTheme = store.getState().setSetting('theme', 'light')
const saveFont = store.getState().setSetting('fontSize', 20)
expect(store.getState().settings).toMatchObject({ theme: 'light', fontSize: 20 })
await Promise.resolve()
expect(window.opsxApi.settingsSet).toHaveBeenCalledTimes(1)
first.resolve({ success: true })
await Promise.all([saveTheme, saveFont])
expect(window.opsxApi.settingsSet).toHaveBeenLastCalledWith(
'app-settings',
expect.objectContaining({ theme: 'light', fontSize: 20 })
)
})
it('rolls back a rejected envelope without persisting the failed patch in the next write', async () => {
vi.mocked(window.opsxApi.settingsSet).mockResolvedValueOnce({ success: false, error: 'disk full' })
await Promise.all([store.getState().setSetting('theme', 'light'), store.getState().setSetting('fontSize', 20)])
expect(store.getState().settings).toMatchObject({ theme: 'dark', fontSize: 20 })
expect(window.opsxApi.settingsSet).toHaveBeenLastCalledWith(
'app-settings',
expect.objectContaining({ theme: 'dark', fontSize: 20 })
)
const { useToastStore } = await import('../../src/stores/toastStore')
expect(useToastStore.getState().toasts.at(-1)).toMatchObject({
type: 'error',
message: expect.stringContaining('disk full'),
})
})
it('continues saving after a thrown IPC failure, including edits to the same field', async () => {
vi.mocked(window.opsxApi.settingsSet).mockRejectedValueOnce(new Error('IPC unavailable'))
await Promise.all([store.getState().setSetting('fontSize', 18), store.getState().setSetting('fontSize', 22)])
expect(store.getState().settings.fontSize).toBe(22)
expect(window.opsxApi.settingsSet).toHaveBeenCalledTimes(2)
})
it('merges edits made while initial settings are loading into the persisted snapshot', async () => {
const read = deferred<{ success: boolean; value: unknown }>()
vi.mocked(window.opsxApi.settingsGet).mockReturnValueOnce(read.promise)
const load = store.getState().loadSettings()
const save = store.getState().setSetting('fontSize', 22)
read.resolve({ success: true, value: { theme: 'light', agentSafetyMode: 'relaxed' } })
await Promise.all([load, save])
expect(store.getState().settings).toMatchObject({ theme: 'light', fontSize: 22 })
expect(window.opsxApi.settingsSet).toHaveBeenLastCalledWith(
'app-settings',
expect.objectContaining({ theme: 'light', fontSize: 22 })
)
})
})