Create a frontend wireframe. (#7)
Sets up API methods and types. Sets up a colorscheme. Sets up a homepage. Removes tailwind in favor of mui for now. Reviewed-on: #7 Co-authored-by: Drew Galbraith <drew@tiramisu.one> Co-committed-by: Drew Galbraith <drew@tiramisu.one>
This commit is contained in:
parent
7d2b7fc90c
commit
d60d834f38
27 changed files with 3114 additions and 977 deletions
211
frontend/app/hooks/useTask.test.ts
Normal file
211
frontend/app/hooks/useTask.test.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { renderHook, act, waitFor } from '@testing-library/react'
|
||||
import { useTask } from './useTask'
|
||||
import type { Task, UpdateTaskRequest } from '~/types/task'
|
||||
import { TaskStatus } 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 unknown 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue