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
267
frontend/app/services/api.test.ts
Normal file
267
frontend/app/services/api.test.ts
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { apiClient } from './api'
|
||||
import type { Task, CreateTaskRequest, UpdateTaskRequest } from '~/types/task'
|
||||
import { TaskStatus } from '~/types/task'
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn()
|
||||
global.fetch = mockFetch
|
||||
|
||||
// Sample task data for testing
|
||||
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,
|
||||
}
|
||||
|
||||
const mockTasks: Task[] = [
|
||||
mockTask,
|
||||
{
|
||||
...mockTask,
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
title: 'Another Task',
|
||||
status: TaskStatus.Done,
|
||||
completed_at: '2023-01-01T01:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
describe('API Client', () => {
|
||||
beforeEach(() => {
|
||||
mockFetch.mockClear()
|
||||
// Silence console.log during tests
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('listTasks', () => {
|
||||
it('should fetch all tasks successfully', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve(mockTasks),
|
||||
})
|
||||
|
||||
const result = await apiClient.listTasks()
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/api/tasks', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
expect(result).toEqual(mockTasks)
|
||||
})
|
||||
|
||||
it('should handle empty task list', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve([]),
|
||||
})
|
||||
|
||||
const result = await apiClient.listTasks()
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should throw error on failed request', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
json: () => Promise.resolve({ message: 'Server error' }),
|
||||
})
|
||||
|
||||
await expect(apiClient.listTasks()).rejects.toThrow('Server error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getTask', () => {
|
||||
it('should fetch single task successfully', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve(mockTask),
|
||||
})
|
||||
|
||||
const result = await apiClient.getTask(mockTask.id)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(`/api/tasks/${mockTask.id}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
expect(result).toEqual(mockTask)
|
||||
})
|
||||
|
||||
it('should handle task not found', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
json: () => Promise.resolve({ message: 'Task not found' }),
|
||||
})
|
||||
|
||||
await expect(apiClient.getTask('nonexistent-id')).rejects.toThrow(
|
||||
'Task not found'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createTask', () => {
|
||||
it('should create task successfully', async () => {
|
||||
const newTaskData: CreateTaskRequest = {
|
||||
title: 'New Task',
|
||||
description: 'New Description',
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
statusText: 'Created',
|
||||
json: () => Promise.resolve(mockTask),
|
||||
})
|
||||
|
||||
const result = await apiClient.createTask(newTaskData)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/api/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newTaskData),
|
||||
})
|
||||
expect(result).toEqual(mockTask)
|
||||
})
|
||||
|
||||
it('should handle validation errors', async () => {
|
||||
const invalidData: CreateTaskRequest = {
|
||||
title: '',
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 422,
|
||||
statusText: 'Unprocessable Entity',
|
||||
json: () => Promise.resolve({ message: 'Title must not be empty' }),
|
||||
})
|
||||
|
||||
await expect(apiClient.createTask(invalidData)).rejects.toThrow(
|
||||
'Title must not be empty'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateTask', () => {
|
||||
it('should update task successfully', async () => {
|
||||
const updateData: UpdateTaskRequest = {
|
||||
title: 'Updated Task',
|
||||
status: TaskStatus.Done,
|
||||
}
|
||||
|
||||
const updatedTask = { ...mockTask, ...updateData }
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve(updatedTask),
|
||||
})
|
||||
|
||||
const result = await apiClient.updateTask(mockTask.id, updateData)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(`/api/tasks/${mockTask.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updateData),
|
||||
})
|
||||
expect(result).toEqual(updatedTask)
|
||||
})
|
||||
|
||||
it('should handle partial updates', async () => {
|
||||
const updateData: UpdateTaskRequest = {
|
||||
status: TaskStatus.Done,
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve({ ...mockTask, status: TaskStatus.Done }),
|
||||
})
|
||||
|
||||
const result = await apiClient.updateTask(mockTask.id, updateData)
|
||||
|
||||
expect(result.status).toBe(TaskStatus.Done)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteTask', () => {
|
||||
it('should delete task successfully', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 204,
|
||||
statusText: 'No Content',
|
||||
})
|
||||
|
||||
const result = await apiClient.deleteTask(mockTask.id)
|
||||
expect(result).toBeNull()
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(`/api/tasks/${mockTask.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle delete errors', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
json: () => Promise.resolve({ message: 'Task not found' }),
|
||||
})
|
||||
|
||||
await expect(apiClient.deleteTask('nonexistent-id')).rejects.toThrow(
|
||||
'Task not found'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle network errors', async () => {
|
||||
mockFetch.mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
await expect(apiClient.listTasks()).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('should handle malformed JSON responses', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
json: () => Promise.reject(new Error('Invalid JSON')),
|
||||
})
|
||||
|
||||
await expect(apiClient.listTasks()).rejects.toThrow(
|
||||
'HTTP 500: Internal Server Error'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle responses with no error message', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
|
||||
await expect(apiClient.listTasks()).rejects.toThrow(
|
||||
'HTTP 400: Bad Request'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue