Frontend API structure.

This commit is contained in:
Drew 2025-09-22 02:00:26 -07:00
parent 7d2b7fc90c
commit c443a13a14
10 changed files with 1810 additions and 4 deletions

View file

@ -0,0 +1,210 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { useTask } from './useTask'
import { Task, TaskStatus, UpdateTaskRequest } from '~/types/task'
import { apiClient } from '~/services/api'
// Mock the API client
vi.mock('~/services/api', () => ({
apiClient: {
getTask: vi.fn(),
updateTask: vi.fn(),
deleteTask: vi.fn(),
},
}))
const mockApiClient = apiClient as {
getTask: ReturnType<typeof vi.fn>
updateTask: ReturnType<typeof vi.fn>
deleteTask: ReturnType<typeof vi.fn>
}
// Sample task data
const mockTask: Task = {
id: '550e8400-e29b-41d4-a716-446655440000',
title: 'Test Task',
description: 'Test Description',
status: TaskStatus.Todo,
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
completed_at: null,
}
describe('useTask', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('should initialize with default state', () => {
const { result } = renderHook(() => useTask())
expect(result.current.task).toBeNull()
expect(result.current.loading).toBe(false)
expect(result.current.error).toBeNull()
})
describe('getTask', () => {
it('should fetch task successfully', async () => {
mockApiClient.getTask.mockResolvedValueOnce(mockTask)
const { result } = renderHook(() => useTask())
act(() => {
result.current.getTask(mockTask.id)
})
expect(result.current.loading).toBe(true)
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.task).toEqual(mockTask)
expect(result.current.error).toBeNull()
expect(mockApiClient.getTask).toHaveBeenCalledWith(mockTask.id)
})
it('should handle fetch errors', async () => {
const errorMessage = 'Task not found'
mockApiClient.getTask.mockRejectedValueOnce({ message: errorMessage })
const { result } = renderHook(() => useTask())
act(() => {
result.current.getTask(mockTask.id)
})
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.task).toBeNull()
expect(result.current.error).toBe(errorMessage)
})
})
describe('updateTask', () => {
it('should update task with optimistic update', async () => {
const updateData: UpdateTaskRequest = {
title: 'Updated Task',
status: TaskStatus.Done,
}
const updatedTask = { ...mockTask, ...updateData }
mockApiClient.getTask.mockResolvedValueOnce(mockTask)
mockApiClient.updateTask.mockResolvedValueOnce(updatedTask)
const { result } = renderHook(() => useTask())
// Set initial task
await act(async () => {
await result.current.getTask(mockTask.id)
})
let updateResult: Task | null = null
await act(async () => {
updateResult = await result.current.updateTask(mockTask.id, updateData)
})
expect(updateResult).toEqual(updatedTask)
expect(mockApiClient.updateTask).toHaveBeenCalledWith(
mockTask.id,
updateData
)
})
it('should handle update errors and revert optimistic update', async () => {
const updateData: UpdateTaskRequest = { status: TaskStatus.Done }
const errorMessage = 'Update failed'
// Setup initial task
mockApiClient.getTask.mockResolvedValueOnce(mockTask)
const { result } = renderHook(() => useTask())
// Set initial task state
await act(async () => {
await result.current.getTask(mockTask.id)
})
expect(result.current.task).toEqual(mockTask)
// Mock update failure and revert call
mockApiClient.updateTask.mockRejectedValueOnce({ message: errorMessage })
mockApiClient.getTask.mockResolvedValueOnce(mockTask)
let updateResult: Task | null = null
await act(async () => {
updateResult = await result.current.updateTask(mockTask.id, updateData)
})
expect(updateResult).toBeNull()
expect(result.current.error).toBe(errorMessage)
expect(result.current.loading).toBe(false)
})
})
describe('deleteTask', () => {
it('should delete task successfully', async () => {
mockApiClient.deleteTask.mockResolvedValueOnce(undefined)
const { result } = renderHook(() => useTask())
// Set initial task
await act(async () => {
mockApiClient.getTask.mockResolvedValueOnce(mockTask)
await result.current.getTask(mockTask.id)
})
await act(async () => {
await result.current.deleteTask(mockTask.id)
})
expect(result.current.task).toBeNull()
expect(result.current.error).toBeNull()
expect(mockApiClient.deleteTask).toHaveBeenCalledWith(mockTask.id)
})
it('should handle delete errors', async () => {
const errorMessage = 'Delete failed'
mockApiClient.deleteTask.mockRejectedValueOnce({ message: errorMessage })
const { result } = renderHook(() => useTask())
await act(async () => {
await result.current.deleteTask(mockTask.id)
})
expect(result.current.error).toBe(errorMessage)
expect(result.current.loading).toBe(false)
})
})
describe('clearError', () => {
it('should clear error state', async () => {
mockApiClient.getTask.mockRejectedValueOnce({ message: 'Test error' })
const { result } = renderHook(() => useTask())
// Trigger an error
await act(async () => {
await result.current.getTask(mockTask.id)
})
expect(result.current.error).toBeTruthy()
// Clear the error
act(() => {
result.current.clearError()
})
expect(result.current.error).toBeNull()
})
})
})