Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 110 additions & 6 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
"typecheck:cli": "cd cli && bun run typecheck",
"typecheck:hub": "cd hub && bun run typecheck",
"typecheck:web": "cd web && bun run typecheck",
"test": "bun run test:cli && bun run test:hub",
"test": "bun run test:cli && bun run test:hub && bun run test:web",
"test:cli": "cd cli && bun run test",
"test:hub": "cd hub && bun run test",
"test:web": "cd web && bun run test",
"clean-session": "bun run hub/scripts/cleanup-sessions.ts",
"release-all": "cd cli && bun run release-all"
},
Expand Down
9 changes: 7 additions & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"dev": "vite",
"build": "vite build && cp dist/index.html dist/404.html",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@assistant-ui/react": "^0.11.53",
Expand Down Expand Up @@ -43,14 +44,18 @@
"workbox-window": "^7.4.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@tailwindcss/postcss": "^4.1.18",
"@vitejs/plugin-react": "^5.1.2",
"autoprefixer": "^10.4.23",
"jsdom": "^26.1.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.0"
"vite": "^7.3.0",
"vitest": "^4.0.16"
}
}
4 changes: 4 additions & 0 deletions web/src/lib/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions web/src/lib/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': '无可用机器',
Expand Down
101 changes: 101 additions & 0 deletions web/src/routes/settings/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions web/src/routes/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions web/src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest'
1 change: 1 addition & 0 deletions web/src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare const __APP_VERSION__: string
5 changes: 5 additions & 0 deletions web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
import { resolve } from 'node:path'
import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)
const base = process.env.VITE_BASE_URL || '/'

export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(require('../cli/package.json').version),
},
server: {
host: true,
allowedHosts: ['hapidev.weishu.me'],
Expand Down
14 changes: 14 additions & 0 deletions web/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'

export default mergeConfig(
viteConfig,
defineConfig({
test: {
globals: false,
environment: 'jsdom',
include: ['src/**/*.test.{ts,tsx}'],
setupFiles: ['./src/test/setup.ts'],
},
})
)