106 lines
4.2 KiB
TypeScript
106 lines
4.2 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { renderHook, waitFor } from '@testing-library/react'
|
|
|
|
// Reset module between tests so the module-level cache is cleared
|
|
async function freshHook() {
|
|
vi.resetModules()
|
|
const mod = await import('../useLatestRelease')
|
|
return mod.useLatestRelease
|
|
}
|
|
|
|
const CURRENT = '1.8.3'
|
|
|
|
function mockFetch(payload: unknown, ok = true) {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok,
|
|
json: () => Promise.resolve(payload),
|
|
}),
|
|
)
|
|
}
|
|
|
|
describe('useLatestRelease', () => {
|
|
beforeEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('returns no update when latest version matches current', async () => {
|
|
mockFetch({ tag_name: 'v1.8.3', html_url: 'https://github.com/Pouzor/homelable/releases/tag/v1.8.3' })
|
|
const useLatestRelease = await freshHook()
|
|
const { result } = renderHook(() => useLatestRelease(CURRENT))
|
|
await waitFor(() => expect(result.current.latest).not.toBeNull())
|
|
expect(result.current.hasUpdate).toBe(false)
|
|
})
|
|
|
|
it('returns update when latest version is newer', async () => {
|
|
mockFetch({ tag_name: 'v1.9.0', html_url: 'https://github.com/Pouzor/homelable/releases/tag/v1.9.0' })
|
|
const useLatestRelease = await freshHook()
|
|
const { result } = renderHook(() => useLatestRelease(CURRENT))
|
|
await waitFor(() => expect(result.current.hasUpdate).toBe(true))
|
|
expect(result.current.latest?.version).toBe('1.9.0')
|
|
expect(result.current.latest?.url).toBe('https://github.com/Pouzor/homelable/releases/tag/v1.9.0')
|
|
})
|
|
|
|
it('strips leading v from tag_name', async () => {
|
|
mockFetch({ tag_name: 'v2.0.0', html_url: 'https://github.com/example' })
|
|
const useLatestRelease = await freshHook()
|
|
const { result } = renderHook(() => useLatestRelease(CURRENT))
|
|
await waitFor(() => expect(result.current.latest).not.toBeNull())
|
|
expect(result.current.latest?.version).toBe('2.0.0')
|
|
})
|
|
|
|
it('does not show update when API returns non-ok response', async () => {
|
|
mockFetch({ message: 'Not Found' }, false)
|
|
const useLatestRelease = await freshHook()
|
|
const { result } = renderHook(() => useLatestRelease(CURRENT))
|
|
await new Promise((r) => setTimeout(r, 50))
|
|
expect(result.current.hasUpdate).toBe(false)
|
|
expect(result.current.latest).toBeNull()
|
|
})
|
|
|
|
it('does not show update when API returns missing tag_name', async () => {
|
|
mockFetch({ html_url: 'https://github.com/example' })
|
|
const useLatestRelease = await freshHook()
|
|
const { result } = renderHook(() => useLatestRelease(CURRENT))
|
|
await new Promise((r) => setTimeout(r, 50))
|
|
expect(result.current.hasUpdate).toBe(false)
|
|
})
|
|
|
|
it('does not show update when API returns missing html_url', async () => {
|
|
mockFetch({ tag_name: 'v2.0.0' })
|
|
const useLatestRelease = await freshHook()
|
|
const { result } = renderHook(() => useLatestRelease(CURRENT))
|
|
await new Promise((r) => setTimeout(r, 50))
|
|
expect(result.current.hasUpdate).toBe(false)
|
|
})
|
|
|
|
it('does not show update when fetch throws', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')))
|
|
const useLatestRelease = await freshHook()
|
|
const { result } = renderHook(() => useLatestRelease(CURRENT))
|
|
await new Promise((r) => setTimeout(r, 50))
|
|
expect(result.current.hasUpdate).toBe(false)
|
|
})
|
|
|
|
it('fetches only once when hook is mounted multiple times concurrently', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ tag_name: 'v1.8.3', html_url: 'https://github.com' }),
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
const useLatestRelease = await freshHook()
|
|
// Mount all three before the fetch resolves — cache is set to 'pending' after first mount
|
|
const a = renderHook(() => useLatestRelease(CURRENT))
|
|
const b = renderHook(() => useLatestRelease(CURRENT))
|
|
const c = renderHook(() => useLatestRelease(CURRENT))
|
|
await waitFor(() => {
|
|
expect(a.result.current.latest).not.toBeNull()
|
|
})
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
// All hooks see the same result once cache resolves
|
|
expect(b.result.current.hasUpdate).toBe(false)
|
|
expect(c.result.current.hasUpdate).toBe(false)
|
|
})
|
|
})
|