test: achieve 80%+ branch/function coverage on frontend logic (E2E-03)

515 tests across 38 files. Branch coverage 88%, function coverage 83%
on testable logic (stores, composables, api, utils, services, router).

New test files: websocket, useLoginSounds, useMobileBackButton,
useControllerNav, routes. Extended: rpc-client (99.5%), container store
(100%). Fixed: useNavSounds AudioContext mock, type errors across tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-11 17:18:37 +00:00
parent 0b6068f452
commit 1697af725b
14 changed files with 2161 additions and 2 deletions

View File

@@ -128,4 +128,209 @@ describe('useContainerStore', () => {
expect(store.isAppLoading('bitcoin-knots')).toBe(false)
})
it('fetchHealthStatus loads health data', async () => {
mockedClient.getHealthStatus.mockResolvedValue({ 'bitcoin-knots': 'healthy', 'lnd': 'degraded' })
const store = useContainerStore()
await store.fetchHealthStatus()
expect(store.getHealthStatus('bitcoin-knots')).toBe('healthy')
expect(store.getHealthStatus('lnd')).toBe('degraded')
expect(store.getHealthStatus('unknown-app')).toBe('unknown')
})
it('fetchHealthStatus handles errors silently', async () => {
mockedClient.getHealthStatus.mockRejectedValue(new Error('fail'))
const store = useContainerStore()
await store.fetchHealthStatus()
// Should not throw, healthStatus stays empty
expect(store.healthStatus).toEqual({})
})
it('installApp installs and refreshes containers', async () => {
mockedClient.installApp.mockResolvedValue('new-app')
mockedClient.listContainers.mockResolvedValue(mockContainers)
const store = useContainerStore()
const result = await store.installApp('/path/to/manifest')
expect(result).toBe('new-app')
expect(mockedClient.installApp).toHaveBeenCalledWith('/path/to/manifest')
expect(mockedClient.listContainers).toHaveBeenCalled()
expect(store.loading).toBe(false)
})
it('installApp sets error and rethrows on failure', async () => {
mockedClient.installApp.mockRejectedValue(new Error('Install failed'))
const store = useContainerStore()
await expect(store.installApp('/bad/manifest')).rejects.toThrow('Install failed')
expect(store.error).toBe('Install failed')
expect(store.loading).toBe(false)
})
it('startContainer sets error on failure', async () => {
mockedClient.startContainer.mockRejectedValue(new Error('Start failed'))
const store = useContainerStore()
await expect(store.startContainer('bitcoin-knots')).rejects.toThrow('Start failed')
expect(store.error).toBe('Start failed')
expect(store.isAppLoading('bitcoin-knots')).toBe(false)
})
it('stopContainer sets error on failure', async () => {
mockedClient.stopContainer.mockRejectedValue(new Error('Stop failed'))
const store = useContainerStore()
await expect(store.stopContainer('lnd')).rejects.toThrow('Stop failed')
expect(store.error).toBe('Stop failed')
expect(store.isAppLoading('lnd')).toBe(false)
})
it('removeContainer removes and refreshes', async () => {
mockedClient.removeContainer.mockResolvedValue(undefined)
mockedClient.listContainers.mockResolvedValue([])
const store = useContainerStore()
await store.removeContainer('old-app')
expect(mockedClient.removeContainer).toHaveBeenCalledWith('old-app')
expect(mockedClient.listContainers).toHaveBeenCalled()
expect(store.loading).toBe(false)
})
it('removeContainer sets error on failure', async () => {
mockedClient.removeContainer.mockRejectedValue(new Error('Remove failed'))
const store = useContainerStore()
await expect(store.removeContainer('old-app')).rejects.toThrow('Remove failed')
expect(store.error).toBe('Remove failed')
})
it('getContainerLogs returns logs', async () => {
mockedClient.getContainerLogs.mockResolvedValue(['line1', 'line2', 'line3'])
const store = useContainerStore()
const logs = await store.getContainerLogs('bitcoin-knots', 50)
expect(logs).toEqual(['line1', 'line2', 'line3'])
expect(mockedClient.getContainerLogs).toHaveBeenCalledWith('bitcoin-knots', 50)
})
it('getContainerLogs defaults to 100 lines', async () => {
mockedClient.getContainerLogs.mockResolvedValue(['log output'])
const store = useContainerStore()
await store.getContainerLogs('bitcoin-knots')
expect(mockedClient.getContainerLogs).toHaveBeenCalledWith('bitcoin-knots', 100)
})
it('getContainerLogs sets error on failure', async () => {
mockedClient.getContainerLogs.mockRejectedValue(new Error('Log error'))
const store = useContainerStore()
await expect(store.getContainerLogs('bitcoin-knots')).rejects.toThrow('Log error')
expect(store.error).toBe('Log error')
})
it('getContainerStatus returns status', async () => {
const status = { name: 'bitcoin-knots', state: 'running', uptime: '5h' }
mockedClient.getContainerStatus.mockResolvedValue(status as never)
const store = useContainerStore()
const result = await store.getContainerStatus('bitcoin-knots')
expect(result).toEqual(status)
})
it('getContainerStatus sets error on failure', async () => {
mockedClient.getContainerStatus.mockRejectedValue(new Error('Status error'))
const store = useContainerStore()
await expect(store.getContainerStatus('bitcoin-knots')).rejects.toThrow('Status error')
expect(store.error).toBe('Status error')
})
it('startBundledApp starts and refreshes', async () => {
mockedClient.startBundledApp.mockResolvedValue(undefined)
mockedClient.listContainers.mockResolvedValue(mockContainers)
mockedClient.getHealthStatus.mockResolvedValue({})
const store = useContainerStore()
const app = { id: 'bitcoin-knots', name: 'Bitcoin Knots', image: 'btc:29', description: '', icon: '', ports: [], volumes: [], category: 'bitcoin' as const }
await store.startBundledApp(app)
expect(mockedClient.startBundledApp).toHaveBeenCalledWith(app)
expect(store.isAppLoading('bitcoin-knots')).toBe(false)
})
it('startBundledApp sets error on failure', async () => {
mockedClient.startBundledApp.mockRejectedValue(new Error('Start failed'))
const store = useContainerStore()
const app = { id: 'test', name: 'Test', image: 'test:1', description: '', icon: '', ports: [], volumes: [], category: 'other' as const }
await expect(store.startBundledApp(app)).rejects.toThrow('Start failed')
expect(store.error).toBe('Start failed')
expect(store.isAppLoading('test')).toBe(false)
})
it('stopBundledApp stops and refreshes', async () => {
mockedClient.stopBundledApp.mockResolvedValue(undefined)
mockedClient.listContainers.mockResolvedValue([])
const store = useContainerStore()
await store.stopBundledApp('bitcoin-knots')
expect(mockedClient.stopBundledApp).toHaveBeenCalledWith('bitcoin-knots')
expect(store.isAppLoading('bitcoin-knots')).toBe(false)
})
it('stopBundledApp sets error on failure', async () => {
mockedClient.stopBundledApp.mockRejectedValue(new Error('Stop failed'))
const store = useContainerStore()
await expect(store.stopBundledApp('bitcoin-knots')).rejects.toThrow('Stop failed')
expect(store.error).toBe('Stop failed')
expect(store.isAppLoading('bitcoin-knots')).toBe(false)
})
it('getContainerById finds by name substring', async () => {
mockedClient.listContainers.mockResolvedValue(mockContainers)
const store = useContainerStore()
await store.fetchContainers()
expect(store.getContainerById('bitcoin')?.name).toBe('bitcoin-knots')
expect(store.getContainerById('nonexistent')).toBeUndefined()
})
it('getContainerForApp matches by exact name', async () => {
mockedClient.listContainers.mockResolvedValue(mockContainers)
const store = useContainerStore()
await store.fetchContainers()
expect(store.getContainerForApp('bitcoin-knots')?.name).toBe('bitcoin-knots')
expect(store.getContainerForApp('lnd')?.name).toBe('lnd')
})
it('enrichedBundledApps includes lan_address from matching containers', async () => {
mockedClient.listContainers.mockResolvedValue(mockContainers)
const store = useContainerStore()
await store.fetchContainers()
const enriched = store.enrichedBundledApps
const btc = enriched.find(a => a.id === 'bitcoin-knots')
expect(btc?.lan_address).toBe('http://localhost:8332')
})
it('fetchContainers handles non-Error exceptions', async () => {
mockedClient.listContainers.mockRejectedValue('string error')
const store = useContainerStore()
await store.fetchContainers()
expect(store.error).toBe('Failed to fetch containers')
})
})