Add About section to settings page with version info and tests (#119)

* feat(web): add About section to settings page

- Add website link to hapi.run
- Display app version from CLI package
- Display protocol version from shared module
- Add Vitest testing setup with settings page tests

🤖 Generated with Claude Code

* test(web): add tests for website link and i18n key usage

Address residual risks mentioned in PR review:
- Test website link URL and security attributes (target, rel)
- Verify correct i18n keys are used for About section via spy

Simplify test setup by using real I18nProvider and en locale.

🤖 Generated with Claude Code
This commit is contained in:
Trustin Lee
2026-01-30 09:48:49 +08:00
committed by GitHub
parent c5190b4079
commit 7cad11ca27
11 changed files with 276 additions and 9 deletions
+4
View File
@@ -239,6 +239,10 @@ export default {
'settings.voice.title': 'Voice Assistant',
'settings.voice.language': 'Voice Language',
'settings.voice.autoDetect': 'Auto-detect',
'settings.about.title': 'About',
'settings.about.website': 'Website',
'settings.about.appVersion': 'App Version',
'settings.about.protocolVersion': 'Protocol Version',
// Misc
'misc.noMachines': 'No machines available',
+4
View File
@@ -241,6 +241,10 @@ export default {
'settings.voice.title': '语音助手',
'settings.voice.language': '语音语言',
'settings.voice.autoDetect': '自动检测',
'settings.about.title': '关于',
'settings.about.website': '官方网站',
'settings.about.appVersion': '应用版本',
'settings.about.protocolVersion': '协议版本',
// Misc
'misc.noMachines': '无可用机器',
+101
View File
@@ -0,0 +1,101 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { I18nContext, I18nProvider } from '@/lib/i18n-context'
import { en } from '@/lib/locales'
import { PROTOCOL_VERSION } from '@hapi/protocol'
import SettingsPage from './index'
// Mock the router hooks
vi.mock('@tanstack/react-router', () => ({
useNavigate: () => vi.fn(),
useRouter: () => ({ history: { back: vi.fn() } }),
useLocation: () => '/settings',
}))
// Mock useFontScale hook
vi.mock('@/hooks/useFontScale', () => ({
useFontScale: () => ({ fontScale: 1, setFontScale: vi.fn() }),
getFontScaleOptions: () => [
{ value: 0.875, label: '87.5%' },
{ value: 1, label: '100%' },
{ value: 1.125, label: '112.5%' },
],
}))
// Mock languages
vi.mock('@/lib/languages', () => ({
getElevenLabsSupportedLanguages: () => [
{ code: null, name: 'Auto-detect' },
{ code: 'en', name: 'English' },
],
getLanguageDisplayName: (lang: { code: string | null; name: string }) => lang.name,
}))
function renderWithProviders(ui: React.ReactElement) {
return render(
<I18nProvider>
{ui}
</I18nProvider>
)
}
function renderWithSpyT(ui: React.ReactElement) {
const translations = en as Record<string, string>
const spyT = vi.fn((key: string) => translations[key] ?? key)
render(
<I18nContext.Provider value={{ t: spyT, locale: 'en', setLocale: vi.fn() }}>
{ui}
</I18nContext.Provider>
)
return spyT
}
describe('SettingsPage', () => {
beforeEach(() => {
vi.clearAllMocks()
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(() => 'en'),
setItem: vi.fn(),
removeItem: vi.fn(),
}
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
})
it('renders the About section', () => {
renderWithProviders(<SettingsPage />)
expect(screen.getByText('About')).toBeInTheDocument()
})
it('displays the App Version with correct value', () => {
renderWithProviders(<SettingsPage />)
expect(screen.getAllByText('App Version').length).toBeGreaterThanOrEqual(1)
expect(screen.getAllByText(__APP_VERSION__).length).toBeGreaterThanOrEqual(1)
})
it('displays the Protocol Version with correct value', () => {
renderWithProviders(<SettingsPage />)
expect(screen.getAllByText('Protocol Version').length).toBeGreaterThanOrEqual(1)
expect(screen.getAllByText(String(PROTOCOL_VERSION)).length).toBeGreaterThanOrEqual(1)
})
it('displays the website link with correct URL and security attributes', () => {
renderWithProviders(<SettingsPage />)
expect(screen.getAllByText('Website').length).toBeGreaterThanOrEqual(1)
const links = screen.getAllByRole('link', { name: 'hapi.run' })
expect(links.length).toBeGreaterThanOrEqual(1)
const link = links[0]
expect(link).toHaveAttribute('href', 'https://hapi.run')
expect(link).toHaveAttribute('target', '_blank')
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
})
it('uses correct i18n keys for About section', () => {
const spyT = renderWithSpyT(<SettingsPage />)
const calledKeys = spyT.mock.calls.map((call) => call[0])
expect(calledKeys).toContain('settings.about.title')
expect(calledKeys).toContain('settings.about.website')
expect(calledKeys).toContain('settings.about.appVersion')
expect(calledKeys).toContain('settings.about.protocolVersion')
})
})
+27
View File
@@ -3,6 +3,7 @@ import { useTranslation, type Locale } from '@/lib/use-translation'
import { useAppGoBack } from '@/hooks/useAppGoBack'
import { getElevenLabsSupportedLanguages, getLanguageDisplayName, type Language } from '@/lib/languages'
import { getFontScaleOptions, useFontScale, type FontScale } from '@/hooks/useFontScale'
import { PROTOCOL_VERSION } from '@hapi/protocol'
const locales: { value: Locale; nativeLabel: string }[] = [
{ value: 'en', nativeLabel: 'English' },
@@ -335,6 +336,32 @@ export default function SettingsPage() {
)}
</div>
</div>
{/* About section */}
<div className="border-b border-[var(--app-divider)]">
<div className="px-3 py-2 text-xs font-semibold text-[var(--app-hint)] uppercase tracking-wide">
{t('settings.about.title')}
</div>
<div className="flex w-full items-center justify-between px-3 py-3">
<span className="text-[var(--app-fg)]">{t('settings.about.website')}</span>
<a
href="https://hapi.run"
target="_blank"
rel="noopener noreferrer"
className="text-[var(--app-link)] hover:underline"
>
hapi.run
</a>
</div>
<div className="flex w-full items-center justify-between px-3 py-3">
<span className="text-[var(--app-fg)]">{t('settings.about.appVersion')}</span>
<span className="text-[var(--app-hint)]">{__APP_VERSION__}</span>
</div>
<div className="flex w-full items-center justify-between px-3 py-3">
<span className="text-[var(--app-fg)]">{t('settings.about.protocolVersion')}</span>
<span className="text-[var(--app-hint)]">{PROTOCOL_VERSION}</span>
</div>
</div>
</div>
</div>
</div>
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest'
+1
View File
@@ -0,0 +1 @@
declare const __APP_VERSION__: string