test: add frontend composable tests and remaining test files

useNostr (9), useFightCache (5), useOnlineStatus (4) composable tests.
Added fake-indexeddb dev dependency for IDB tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 00:08:01 +00:00
co-authored by Claude Opus 4.6
parent 806163c6c5
commit 1c296c6f1c
6 changed files with 471 additions and 0 deletions
@@ -0,0 +1,86 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// Mock IndexedDB with fake-indexeddb
import 'fake-indexeddb/auto'
import { cacheFight, getCachedFight } from '../useFightCache'
function makeFight(id: string): any {
return {
id,
botA: { id: `a_${id}`, name: 'Bot A' },
botB: { id: `b_${id}`, name: 'Bot B' },
rounds: [],
status: 'finished',
}
}
describe('useFightCache — IndexedDB', () => {
beforeEach(async () => {
// Clear any prior DB state
const dbs = await indexedDB.databases?.() || []
for (const db of dbs) {
if (db.name) indexedDB.deleteDatabase(db.name)
}
})
it('caches and retrieves a fight', async () => {
const fight = makeFight('f1')
await cacheFight(fight)
const result = await getCachedFight('f1')
expect(result).toBeTruthy()
expect(result!.id).toBe('f1')
})
it('returns null for uncached fight', async () => {
const result = await getCachedFight('nonexistent')
expect(result).toBeNull()
})
it('overwrites existing entry with same id', async () => {
const fight1 = { ...makeFight('f1'), status: 'finished' }
const fight2 = { ...makeFight('f1'), status: 'updated' }
await cacheFight(fight1)
await cacheFight(fight2)
const result = await getCachedFight('f1')
expect((result as any).status).toBe('updated')
})
it('evicts oldest when exceeding MAX_CACHED (5)', async () => {
// Cache 6 fights with staggered timestamps
for (let i = 1; i <= 6; i++) {
await cacheFight(makeFight(`f${i}`))
// Small delay to ensure different _cachedAt timestamps
await new Promise(r => setTimeout(r, 10))
}
// f1 (oldest) should be evicted
const oldest = await getCachedFight('f1')
expect(oldest).toBeNull()
// f6 (newest) should exist
const newest = await getCachedFight('f6')
expect(newest).toBeTruthy()
})
it('gracefully handles missing IndexedDB', async () => {
const orig = globalThis.indexedDB
// Temporarily break indexedDB
Object.defineProperty(globalThis, 'indexedDB', {
value: { open: () => { throw new Error('Not available') } },
writable: true,
configurable: true,
})
await expect(cacheFight(makeFight('f_err'))).resolves.toBeUndefined()
const result = await getCachedFight('f_err')
expect(result).toBeNull()
Object.defineProperty(globalThis, 'indexedDB', {
value: orig,
writable: true,
configurable: true,
})
})
})
@@ -0,0 +1,131 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// Mock nostr-tools before importing useNostr
vi.mock('nostr-tools', () => ({
generateSecretKey: () => new Uint8Array(32).fill(1),
getPublicKey: () => 'a'.repeat(64),
}))
vi.mock('nostr-tools/utils', () => ({
bytesToHex: (b: Uint8Array) => Array.from(b).map(x => x.toString(16).padStart(2, '0')).join(''),
hexToBytes: (h: string) => new Uint8Array(h.match(/.{2}/g)!.map(x => parseInt(x, 16))),
}))
vi.mock('nostr-tools/nip19', () => ({
nsecEncode: (b: Uint8Array) => 'nsec1mock',
}))
// Mock the auth module
const mockAuthFetch = vi.fn().mockResolvedValue({ json: () => Promise.resolve({ exists: false }) })
const mockSetToken = vi.fn()
const mockGetToken = vi.fn().mockReturnValue(null)
const mockIsTokenExpired = vi.fn().mockReturnValue(true)
vi.mock('../../lib/nostr-auth', () => ({
buildNip98Token: vi.fn().mockResolvedValue('mock-nip98-token'),
setToken: (...args: unknown[]) => mockSetToken(...args),
getToken: () => mockGetToken(),
isTokenExpired: () => mockIsTokenExpired(),
authFetch: (...args: unknown[]) => mockAuthFetch(...args),
}))
describe('useNostr', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
})
afterEach(() => {
localStorage.clear()
})
it('isLoggedIn is false when no pubkey or bot stored', async () => {
vi.resetModules()
const { useNostr } = await import('../useNostr')
const { isLoggedIn, pubkey, bot } = useNostr()
expect(isLoggedIn.value).toBe(false)
expect(pubkey.value).toBeNull()
expect(bot.value).toBeNull()
})
it('logout clears all auth state from localStorage', async () => {
localStorage.setItem('bf_pubkey', JSON.stringify('testpub'))
localStorage.setItem('bf_bot', JSON.stringify({ id: 'b1', name: 'Bot' }))
localStorage.setItem('bf_pic', JSON.stringify('https://example.com/pic.jpg'))
localStorage.setItem('bf_nsec', 'secretkey')
vi.resetModules()
const { useNostr } = await import('../useNostr')
const { logout } = useNostr()
logout()
expect(localStorage.getItem('bf_pubkey')).toBeNull()
expect(localStorage.getItem('bf_bot')).toBeNull()
expect(localStorage.getItem('bf_pic')).toBeNull()
expect(localStorage.getItem('bf_nsec')).toBeNull()
expect(mockSetToken).toHaveBeenCalledWith(null)
})
it('hasExtension detects window.nostr', async () => {
vi.resetModules()
const { useNostr } = await import('../useNostr')
const { hasExtension } = useNostr()
// No NIP-07 extension in jsdom
expect(hasExtension.value).toBe(false)
})
it('hasStoredKey is false when no nsec in localStorage', async () => {
vi.resetModules()
const { useNostr } = await import('../useNostr')
const { hasStoredKey } = useNostr()
expect(hasStoredKey.value).toBe(false)
})
it('hasStoredKey is true when nsec pre-set in localStorage', async () => {
localStorage.setItem('bf_nsec', 'test-nsec')
vi.resetModules()
const { useNostr } = await import('../useNostr')
const { hasStoredKey } = useNostr()
expect(hasStoredKey.value).toBe(true)
})
it('getStoredNsec returns null when no key stored', async () => {
vi.resetModules()
const { useNostr } = await import('../useNostr')
const { getStoredNsec } = useNostr()
expect(getStoredNsec()).toBeNull()
})
it('persistKey saves session nsec to localStorage', async () => {
vi.resetModules()
const { useNostr } = await import('../useNostr')
const { persistKey } = useNostr()
// Without a session key, persist should be a no-op
persistKey()
expect(localStorage.getItem('bf_nsec')).toBeNull()
})
it('pubkey and bot are readonly refs', async () => {
vi.resetModules()
const { useNostr } = await import('../useNostr')
const result = useNostr()
// pubkey and bot should be Readonly refs
expect(result.pubkey).toBeDefined()
expect(result.bot).toBeDefined()
expect(result.isLoggedIn).toBeDefined()
expect(result.isLoading).toBeDefined()
})
it('waitForSigner resolves false when no extension', async () => {
vi.resetModules()
vi.useFakeTimers()
const { useNostr } = await import('../useNostr')
const { waitForSigner } = useNostr()
const promise = waitForSigner(500)
// Advance timers past the timeout
vi.advanceTimersByTime(600)
const result = await promise
expect(result).toBe(false)
vi.useRealTimers()
})
})
@@ -0,0 +1,53 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent } from 'vue'
import { useOnlineStatus } from '../useOnlineStatus'
// Wrapper component to test the composable in a real Vue lifecycle
const TestComponent = defineComponent({
setup() {
const { isOnline } = useOnlineStatus()
return { isOnline }
},
template: '<span>{{ isOnline }}</span>',
})
describe('useOnlineStatus', () => {
it('returns isOnline ref that reflects navigator.onLine', () => {
const wrapper = mount(TestComponent)
expect(wrapper.vm.isOnline).toBe(true)
wrapper.unmount()
})
it('adds event listeners on mount', () => {
const addSpy = vi.spyOn(window, 'addEventListener')
const wrapper = mount(TestComponent)
expect(addSpy).toHaveBeenCalledWith('online', expect.any(Function))
expect(addSpy).toHaveBeenCalledWith('offline', expect.any(Function))
wrapper.unmount()
addSpy.mockRestore()
})
it('removes event listeners on unmount', () => {
const removeSpy = vi.spyOn(window, 'removeEventListener')
const wrapper = mount(TestComponent)
wrapper.unmount()
expect(removeSpy).toHaveBeenCalledWith('online', expect.any(Function))
expect(removeSpy).toHaveBeenCalledWith('offline', expect.any(Function))
removeSpy.mockRestore()
})
it('singleton: multiple instances share same ref', () => {
const w1 = mount(TestComponent)
const w2 = mount(TestComponent)
expect(w1.vm.isOnline).toBe(w2.vm.isOnline)
w1.unmount()
w2.unmount()
})
})