- 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>
239 lines
6.5 KiB
TypeScript
239 lines
6.5 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 {
|
|
useServices,
|
|
useService,
|
|
useCreateService,
|
|
useUpdateService,
|
|
useDeleteService,
|
|
useReorderServices,
|
|
} from '../useServices';
|
|
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('useServices hooks', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('useServices', () => {
|
|
it('fetches services and transforms data', async () => {
|
|
const mockServices = [
|
|
{ id: 1, name: 'Haircut', duration: 30, price: '25.00', description: 'Basic haircut' },
|
|
{ id: 2, name: 'Color', duration_minutes: 60, price: '75.00', variable_pricing: true },
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockServices });
|
|
|
|
const { result } = renderHook(() => useServices(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/services/');
|
|
expect(result.current.data).toHaveLength(2);
|
|
expect(result.current.data?.[0]).toEqual(expect.objectContaining({
|
|
id: '1',
|
|
name: 'Haircut',
|
|
durationMinutes: 30,
|
|
price: 25,
|
|
description: 'Basic haircut',
|
|
}));
|
|
expect(result.current.data?.[1].variable_pricing).toBe(true);
|
|
});
|
|
|
|
it('handles missing fields with defaults', async () => {
|
|
const mockServices = [
|
|
{ id: 1, name: 'Service', price: '10.00', duration: 15 },
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockServices });
|
|
|
|
const { result } = renderHook(() => useServices(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(result.current.data?.[0].description).toBe('');
|
|
expect(result.current.data?.[0].displayOrder).toBe(0);
|
|
expect(result.current.data?.[0].photos).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('useService', () => {
|
|
it('fetches single service by id', async () => {
|
|
const mockService = {
|
|
id: 1,
|
|
name: 'Premium Cut',
|
|
duration: 45,
|
|
price: '50.00',
|
|
description: 'Premium service',
|
|
photos: ['photo1.jpg'],
|
|
};
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockService });
|
|
|
|
const { result } = renderHook(() => useService('1'), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/services/1/');
|
|
expect(result.current.data?.name).toBe('Premium Cut');
|
|
});
|
|
|
|
it('does not fetch when id is empty', async () => {
|
|
const { result } = renderHook(() => useService(''), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
expect(apiClient.get).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('useCreateService', () => {
|
|
it('creates service with correct field mapping', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });
|
|
|
|
const { result } = renderHook(() => useCreateService(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync({
|
|
name: 'New Service',
|
|
durationMinutes: 45,
|
|
price: 35.99,
|
|
description: 'Test description',
|
|
photos: ['photo.jpg'],
|
|
});
|
|
});
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/services/', {
|
|
name: 'New Service',
|
|
duration: 45,
|
|
price: '35.99',
|
|
description: 'Test description',
|
|
photos: ['photo.jpg'],
|
|
});
|
|
});
|
|
|
|
it('includes pricing fields when provided', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });
|
|
|
|
const { result } = renderHook(() => useCreateService(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync({
|
|
name: 'Priced Service',
|
|
durationMinutes: 30,
|
|
price: 100,
|
|
variable_pricing: true,
|
|
deposit_amount: 25,
|
|
deposit_percent: 25,
|
|
});
|
|
});
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/services/', expect.objectContaining({
|
|
variable_pricing: true,
|
|
deposit_amount: 25,
|
|
deposit_percent: 25,
|
|
}));
|
|
});
|
|
});
|
|
|
|
describe('useUpdateService', () => {
|
|
it('updates service with mapped fields', async () => {
|
|
vi.mocked(apiClient.patch).mockResolvedValue({ data: {} });
|
|
|
|
const { result } = renderHook(() => useUpdateService(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync({
|
|
id: '1',
|
|
updates: { name: 'Updated Name', price: 50 },
|
|
});
|
|
});
|
|
|
|
expect(apiClient.patch).toHaveBeenCalledWith('/services/1/', {
|
|
name: 'Updated Name',
|
|
price: '50',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('useDeleteService', () => {
|
|
it('deletes service by id', async () => {
|
|
vi.mocked(apiClient.delete).mockResolvedValue({});
|
|
|
|
const { result } = renderHook(() => useDeleteService(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync('3');
|
|
});
|
|
|
|
expect(apiClient.delete).toHaveBeenCalledWith('/services/3/');
|
|
});
|
|
});
|
|
|
|
describe('useReorderServices', () => {
|
|
it('sends reorder request with converted ids', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: {} });
|
|
|
|
const { result } = renderHook(() => useReorderServices(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync(['3', '1', '2']);
|
|
});
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/services/reorder/', {
|
|
order: [3, 1, 2],
|
|
});
|
|
});
|
|
});
|
|
});
|