jan/web-app/src/hooks/__tests__/useReleaseNotes.test.ts
2025-10-28 17:26:27 +07:00

363 lines
9.3 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { useReleaseNotes } from '../useReleaseNotes'
// Mock fetch
const mockFetch = vi.fn()
global.fetch = mockFetch
describe('useReleaseNotes', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset store state to defaults
useReleaseNotes.setState({
release: null,
loading: false,
error: null,
})
})
afterEach(() => {
vi.resetAllMocks()
})
it('should initialize with default values', () => {
const { result } = renderHook(() => useReleaseNotes())
expect(result.current.release).toBe(null)
expect(result.current.loading).toBe(false)
expect(result.current.error).toBe(null)
expect(typeof result.current.fetchLatestRelease).toBe('function')
})
describe('fetchLatestRelease', () => {
it('should fetch stable release when includeBeta is false', async () => {
const mockReleases = [
{
tag_name: 'v2.0.0-beta.1',
prerelease: true,
draft: false,
body: 'Beta release notes',
},
{
tag_name: 'v1.5.0',
prerelease: false,
draft: false,
body: 'Stable release notes',
},
{
tag_name: 'v1.4.0',
prerelease: false,
draft: false,
body: 'Previous stable release',
},
]
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockReleases,
})
const { result } = renderHook(() => useReleaseNotes())
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(mockFetch).toHaveBeenCalledWith(
'https://api.github.com/repos/janhq/jan/releases'
)
expect(result.current.loading).toBe(false)
expect(result.current.error).toBe(null)
expect(result.current.release).toEqual({
tag_name: 'v1.5.0',
prerelease: false,
draft: false,
body: 'Stable release notes',
})
})
it('should fetch beta release when includeBeta is true', async () => {
const mockReleases = [
{
tag_name: 'v2.0.0-beta.1',
prerelease: true,
draft: false,
body: 'Beta release notes',
},
{
tag_name: 'v1.5.0',
prerelease: false,
draft: false,
body: 'Stable release notes',
},
]
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockReleases,
})
const { result } = renderHook(() => useReleaseNotes())
await act(async () => {
await result.current.fetchLatestRelease(true)
})
expect(result.current.release).toEqual({
tag_name: 'v2.0.0-beta.1',
prerelease: true,
draft: false,
body: 'Beta release notes',
})
})
it('should fallback to stable release when includeBeta is true but no beta exists', async () => {
const mockReleases = [
{
tag_name: 'v1.5.0',
prerelease: false,
draft: false,
body: 'Stable release notes',
},
{
tag_name: 'v1.4.0',
prerelease: false,
draft: false,
body: 'Previous stable release',
},
]
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockReleases,
})
const { result } = renderHook(() => useReleaseNotes())
await act(async () => {
await result.current.fetchLatestRelease(true)
})
expect(result.current.release).toEqual({
tag_name: 'v1.5.0',
prerelease: false,
draft: false,
body: 'Stable release notes',
})
})
it('should ignore draft releases', async () => {
const mockReleases = [
{
tag_name: 'v2.0.0-draft',
prerelease: false,
draft: true,
body: 'Draft release',
},
{
tag_name: 'v1.5.0',
prerelease: false,
draft: false,
body: 'Stable release notes',
},
]
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockReleases,
})
const { result } = renderHook(() => useReleaseNotes())
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.release).toEqual({
tag_name: 'v1.5.0',
prerelease: false,
draft: false,
body: 'Stable release notes',
})
})
it('should set loading state during fetch', async () => {
const { result } = renderHook(() => useReleaseNotes())
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [],
})
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.loading).toBe(false)
expect(result.current.error).toBe(null)
})
it('should handle fetch errors', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'))
const { result } = renderHook(() => useReleaseNotes())
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.loading).toBe(false)
expect(result.current.error).toBe('Network error')
expect(result.current.release).toBe(null)
})
it('should handle HTTP errors', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
})
const { result } = renderHook(() => useReleaseNotes())
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.loading).toBe(false)
expect(result.current.error).toBe('Failed to fetch releases')
expect(result.current.release).toBe(null)
})
it('should handle empty releases array', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [],
})
const { result } = renderHook(() => useReleaseNotes())
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.loading).toBe(false)
expect(result.current.error).toBe(null)
expect(result.current.release).toBe(undefined)
})
it('should clear previous error on new fetch', async () => {
const { result } = renderHook(() => useReleaseNotes())
// First request fails
mockFetch.mockRejectedValueOnce(new Error('First error'))
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.error).toBe('First error')
// Second request succeeds
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [
{
tag_name: 'v1.0.0',
prerelease: false,
draft: false,
body: 'Release notes',
},
],
})
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.error).toBe(null)
expect(result.current.release).toEqual({
tag_name: 'v1.0.0',
prerelease: false,
draft: false,
body: 'Release notes',
})
})
it('should handle releases with additional properties', async () => {
const mockReleases = [
{
tag_name: 'v1.5.0',
prerelease: false,
draft: false,
body: 'Release notes',
published_at: '2024-01-01T00:00:00Z',
html_url: 'https://github.com/janhq/jan/releases/tag/v1.5.0',
assets: [],
},
]
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockReleases,
})
const { result } = renderHook(() => useReleaseNotes())
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.release).toEqual(mockReleases[0])
})
})
describe('state management', () => {
it('should maintain state across multiple hook instances', () => {
const { result: result1 } = renderHook(() => useReleaseNotes())
const { result: result2 } = renderHook(() => useReleaseNotes())
expect(result1.current.release).toBe(result2.current.release)
expect(result1.current.loading).toBe(result2.current.loading)
expect(result1.current.error).toBe(result2.current.error)
})
})
describe('multiple requests', () => {
it('should handle sequential requests', async () => {
const mockReleases = [
{
tag_name: 'v1.0.0',
prerelease: false,
draft: false,
body: 'Release notes',
},
]
mockFetch.mockResolvedValue({
ok: true,
json: async () => mockReleases,
})
const { result } = renderHook(() => useReleaseNotes())
// First request
await act(async () => {
await result.current.fetchLatestRelease(false)
})
expect(result.current.loading).toBe(false)
expect(result.current.error).toBe(null)
// Second request
await act(async () => {
await result.current.fetchLatestRelease(true)
})
expect(mockFetch).toHaveBeenCalledTimes(2)
expect(result.current.loading).toBe(false)
expect(result.current.error).toBe(null)
})
})
})