Files
smoothschedule/frontend/src/hooks/__tests__/useResources.test.ts
poduck 8dc2248f1f feat: Add comprehensive test suite and misc improvements
- Add frontend unit tests with Vitest for components, hooks, pages, and utilities
- Add backend tests for webhooks, notifications, middleware, and edge cases
- Add ForgotPassword, NotFound, and ResetPassword pages
- Add migration for orphaned staff resources conversion
- Add coverage directory to gitignore (generated reports)
- Various bug fixes and improvements from previous work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 02:36:46 -05:00

243 lines
6.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
// Mock apiClient
vi.mock('../../api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
},
}));
import {
useResources,
useResource,
useCreateResource,
useUpdateResource,
useDeleteResource,
} from '../useResources';
import apiClient from '../../api/client';
// Create wrapper
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function Wrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('useResources hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useResources', () => {
it('fetches resources and transforms data', async () => {
const mockResources = [
{ id: 1, name: 'Room 1', type: 'ROOM', max_concurrent_events: 2 },
{ id: 2, name: 'Staff 1', type: 'STAFF', user_id: 10 },
];
vi.mocked(apiClient.get).mockResolvedValue({ data: mockResources });
const { result } = renderHook(() => useResources(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(apiClient.get).toHaveBeenCalledWith('/resources/?');
expect(result.current.data).toHaveLength(2);
expect(result.current.data?.[0]).toEqual({
id: '1',
name: 'Room 1',
type: 'ROOM',
userId: undefined,
maxConcurrentEvents: 2,
savedLaneCount: undefined,
userCanEditSchedule: false,
});
});
it('applies type filter', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
renderHook(() => useResources({ type: 'STAFF' }), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(apiClient.get).toHaveBeenCalledWith('/resources/?type=STAFF');
});
});
});
describe('useResource', () => {
it('fetches single resource by id', async () => {
const mockResource = {
id: 1,
name: 'Room 1',
type: 'ROOM',
max_concurrent_events: 1,
};
vi.mocked(apiClient.get).mockResolvedValue({ data: mockResource });
const { result } = renderHook(() => useResource('1'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(apiClient.get).toHaveBeenCalledWith('/resources/1/');
expect(result.current.data?.name).toBe('Room 1');
});
it('does not fetch when id is empty', async () => {
const { result } = renderHook(() => useResource(''), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(apiClient.get).not.toHaveBeenCalled();
});
});
describe('useCreateResource', () => {
it('creates resource with backend field mapping', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });
const { result } = renderHook(() => useCreateResource(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
name: 'New Room',
type: 'ROOM',
maxConcurrentEvents: 3,
});
});
expect(apiClient.post).toHaveBeenCalledWith('/resources/', {
name: 'New Room',
type: 'ROOM',
user: null,
timezone: 'UTC',
max_concurrent_events: 3,
});
});
it('converts userId to user integer', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });
const { result } = renderHook(() => useCreateResource(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
name: 'Staff',
type: 'STAFF',
userId: '42',
});
});
expect(apiClient.post).toHaveBeenCalledWith('/resources/', expect.objectContaining({
user: 42,
}));
});
});
describe('useUpdateResource', () => {
it('updates resource with mapped fields', async () => {
vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } });
const { result } = renderHook(() => useUpdateResource(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
id: '1',
updates: { name: 'Updated Room', maxConcurrentEvents: 5 },
});
});
expect(apiClient.patch).toHaveBeenCalledWith('/resources/1/', {
name: 'Updated Room',
max_concurrent_events: 5,
});
});
it('handles userId update', async () => {
vi.mocked(apiClient.patch).mockResolvedValue({ data: {} });
const { result } = renderHook(() => useUpdateResource(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
id: '1',
updates: { userId: '10' },
});
});
expect(apiClient.patch).toHaveBeenCalledWith('/resources/1/', {
user: 10,
});
});
it('sets user to null when userId is empty', async () => {
vi.mocked(apiClient.patch).mockResolvedValue({ data: {} });
const { result } = renderHook(() => useUpdateResource(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
id: '1',
updates: { userId: '' },
});
});
expect(apiClient.patch).toHaveBeenCalledWith('/resources/1/', {
user: null,
});
});
});
describe('useDeleteResource', () => {
it('deletes resource by id', async () => {
vi.mocked(apiClient.delete).mockResolvedValue({});
const { result } = renderHook(() => useDeleteResource(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('5');
});
expect(apiClient.delete).toHaveBeenCalledWith('/resources/5/');
});
});
});