hermes-agent/apps/desktop/src/i18n/context.test.tsx

232 lines
7.7 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { HermesConfigRecord } from '@/hermes'
import { type I18nConfigClient, I18nProvider, useI18n } from './context'
import type { Locale } from './types'
function LanguageProbe({ target = 'zh' }: { target?: Locale }) {
const { isLoadingConfig, isSavingLocale, locale, saveError, setLocale, t } = useI18n()
return (
<div>
<p data-testid="locale">{locale}</p>
<p data-testid="label">{t.language.label}</p>
<p data-testid="save">{t.common.save}</p>
<p data-testid="loading">{String(isLoadingConfig)}</p>
<p data-testid="saving">{String(isSavingLocale)}</p>
<p data-testid="save-error">{saveError?.message ?? ''}</p>
<button onClick={() => void setLocale(target).catch(() => undefined)} type="button">
switch
</button>
</div>
)
}
describe('I18nProvider', () => {
afterEach(() => {
cleanup()
vi.restoreAllMocks()
})
it('defaults to English without a config client', () => {
render(
<I18nProvider configClient={null}>
<LanguageProbe />
</I18nProvider>
)
expect(screen.getByTestId('locale').textContent).toBe('en')
expect(screen.getByTestId('label').textContent).toBe('Language')
})
it('normalizes an initial locale alias and switches translations', async () => {
render(
<I18nProvider configClient={null} initialLocale="zh-CN">
<LanguageProbe target="en" />
</I18nProvider>
)
expect(screen.getByTestId('locale').textContent).toBe('zh')
expect(screen.getByTestId('label').textContent).toBe('语言')
fireEvent.click(screen.getByRole('button', { name: 'switch' }))
await waitFor(() => expect(screen.getByTestId('locale').textContent).toBe('en'))
expect(screen.getByTestId('label').textContent).toBe('Language')
})
it('loads the initial locale from display.language config', async () => {
const configClient: I18nConfigClient = {
getConfig: vi.fn().mockResolvedValue({ display: { language: 'zh-Hans' } }),
saveConfig: vi.fn()
}
render(
<I18nProvider configClient={configClient}>
<LanguageProbe />
</I18nProvider>
)
await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false'))
expect(screen.getByTestId('locale').textContent).toBe('zh')
expect(screen.getByTestId('label').textContent).toBe('语言')
expect(configClient.saveConfig).not.toHaveBeenCalled()
})
it('keeps English usable when config loading fails', async () => {
const configClient: I18nConfigClient = {
getConfig: vi.fn().mockRejectedValue(new Error('config unavailable')),
saveConfig: vi.fn()
}
render(
<I18nProvider configClient={configClient} initialLocale="zh">
<LanguageProbe />
</I18nProvider>
)
await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false'))
expect(screen.getByTestId('locale').textContent).toBe('en')
expect(screen.getByTestId('label').textContent).toBe('Language')
expect(configClient.saveConfig).not.toHaveBeenCalled()
})
it('loads zh-hant from display.language config', async () => {
const configClient: I18nConfigClient = {
getConfig: vi.fn().mockResolvedValue({ display: { language: 'zh-TW' } }),
saveConfig: vi.fn()
}
render(
<I18nProvider configClient={configClient} initialLocale="zh">
<LanguageProbe />
</I18nProvider>
)
await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false'))
expect(screen.getByTestId('locale').textContent).toBe('zh-hant')
expect(screen.getByTestId('save').textContent).toBe('儲存')
expect(configClient.saveConfig).not.toHaveBeenCalled()
})
it('loads ja from display.language config', async () => {
const configClient: I18nConfigClient = {
getConfig: vi.fn().mockResolvedValue({ display: { language: 'ja-JP' } }),
saveConfig: vi.fn()
}
render(
<I18nProvider configClient={configClient}>
<LanguageProbe />
</I18nProvider>
)
await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false'))
expect(screen.getByTestId('locale').textContent).toBe('ja')
expect(screen.getByTestId('save').textContent).toBe('保存')
expect(configClient.saveConfig).not.toHaveBeenCalled()
})
it('does not overwrite unsupported configured languages', async () => {
const configClient: I18nConfigClient = {
getConfig: vi.fn().mockResolvedValue({ display: { language: 'de' } }),
saveConfig: vi.fn()
}
render(
<I18nProvider configClient={configClient} initialLocale="zh">
<LanguageProbe />
</I18nProvider>
)
await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false'))
expect(screen.getByTestId('locale').textContent).toBe('en')
expect(screen.getByTestId('label').textContent).toBe('Language')
expect(configClient.saveConfig).not.toHaveBeenCalled()
})
it('reads latest config before saving language and preserves unrelated values', async () => {
const saveConfig = vi.fn().mockResolvedValue({ ok: true })
const latestConfig: HermesConfigRecord = {
display: { language: 'en', skin: 'slate' },
terminal: { cwd: '/new' }
}
const configClient: I18nConfigClient = {
getConfig: vi
.fn()
.mockResolvedValueOnce({ display: { language: 'en', skin: 'mono' }, terminal: { cwd: '/old' } })
.mockResolvedValueOnce(latestConfig),
saveConfig
}
render(
<I18nProvider configClient={configClient}>
<LanguageProbe />
</I18nProvider>
)
await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false'))
fireEvent.click(screen.getByRole('button', { name: 'switch' }))
await waitFor(() => expect(saveConfig).toHaveBeenCalledTimes(1))
expect(saveConfig).toHaveBeenCalledWith({
display: { language: 'zh', skin: 'slate' },
terminal: { cwd: '/new' }
})
})
it('saves newly supported locales to display.language', async () => {
const saveConfig = vi.fn().mockResolvedValue({ ok: true })
const configClient: I18nConfigClient = {
getConfig: vi
.fn()
.mockResolvedValueOnce({ display: { language: 'en' } })
.mockResolvedValueOnce({ display: { language: 'en', skin: 'mono' } }),
saveConfig
}
render(
<I18nProvider configClient={configClient}>
<LanguageProbe target="ja" />
</I18nProvider>
)
await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false'))
fireEvent.click(screen.getByRole('button', { name: 'switch' }))
await waitFor(() => expect(saveConfig).toHaveBeenCalledTimes(1))
expect(saveConfig).toHaveBeenCalledWith({ display: { language: 'ja', skin: 'mono' } })
expect(screen.getByTestId('locale').textContent).toBe('ja')
})
it('rolls back the visible locale when saving fails', async () => {
const configClient: I18nConfigClient = {
getConfig: vi.fn().mockResolvedValue({ display: { language: 'en' } }),
saveConfig: vi.fn().mockRejectedValue(new Error('save failed'))
}
render(
<I18nProvider configClient={configClient}>
<LanguageProbe />
</I18nProvider>
)
await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false'))
fireEvent.click(screen.getByRole('button', { name: 'switch' }))
await waitFor(() => expect(screen.getByTestId('save-error').textContent).toBe('save failed'))
expect(screen.getByTestId('locale').textContent).toBe('en')
expect(screen.getByTestId('label').textContent).toBe('Language')
})
})