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>
This commit is contained in:
224
frontend/src/hooks/__tests__/useCustomers.test.ts
Normal file
224
frontend/src/hooks/__tests__/useCustomers.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
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 {
|
||||
useCustomers,
|
||||
useCreateCustomer,
|
||||
useUpdateCustomer,
|
||||
useDeleteCustomer,
|
||||
} from '../useCustomers';
|
||||
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('useCustomers hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useCustomers', () => {
|
||||
it('fetches customers and transforms data', async () => {
|
||||
const mockCustomers = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phone: '555-1234',
|
||||
total_spend: '150.00',
|
||||
status: 'Active',
|
||||
user_id: 10,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: { name: 'Jane Smith', email: 'jane@example.com' },
|
||||
phone: '',
|
||||
total_spend: '0',
|
||||
status: 'Inactive',
|
||||
user: 20,
|
||||
},
|
||||
];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockCustomers });
|
||||
|
||||
const { result } = renderHook(() => useCustomers(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/customers/?');
|
||||
expect(result.current.data).toHaveLength(2);
|
||||
expect(result.current.data?.[0]).toEqual(expect.objectContaining({
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
totalSpend: 150,
|
||||
status: 'Active',
|
||||
}));
|
||||
});
|
||||
|
||||
it('applies status filter', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
|
||||
|
||||
renderHook(() => useCustomers({ status: 'Active' }), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/customers/?status=Active');
|
||||
});
|
||||
});
|
||||
|
||||
it('applies search filter', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
|
||||
|
||||
renderHook(() => useCustomers({ search: 'john' }), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/customers/?search=john');
|
||||
});
|
||||
});
|
||||
|
||||
it('applies multiple filters', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
|
||||
|
||||
renderHook(() => useCustomers({ status: 'Blocked', search: 'test' }), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/customers/?status=Blocked&search=test');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles customers with last_visit date', async () => {
|
||||
const mockCustomers = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Customer',
|
||||
email: 'c@example.com',
|
||||
total_spend: '0',
|
||||
last_visit: '2024-01-15T10:00:00Z',
|
||||
user_id: 1,
|
||||
},
|
||||
];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockCustomers });
|
||||
|
||||
const { result } = renderHook(() => useCustomers(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data?.[0].lastVisit).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateCustomer', () => {
|
||||
it('creates customer with field mapping', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });
|
||||
|
||||
const { result } = renderHook(() => useCreateCustomer(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
userId: '5',
|
||||
phone: '555-9999',
|
||||
city: 'Denver',
|
||||
state: 'CO',
|
||||
zip: '80202',
|
||||
status: 'Active',
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/customers/', {
|
||||
user: 5,
|
||||
phone: '555-9999',
|
||||
city: 'Denver',
|
||||
state: 'CO',
|
||||
zip: '80202',
|
||||
status: 'Active',
|
||||
avatar_url: undefined,
|
||||
tags: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateCustomer', () => {
|
||||
it('updates customer with mapped fields', async () => {
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: {} });
|
||||
|
||||
const { result } = renderHook(() => useUpdateCustomer(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: '1',
|
||||
updates: {
|
||||
phone: '555-0000',
|
||||
status: 'Blocked',
|
||||
tags: ['vip'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/customers/1/', {
|
||||
phone: '555-0000',
|
||||
city: undefined,
|
||||
state: undefined,
|
||||
zip: undefined,
|
||||
status: 'Blocked',
|
||||
avatar_url: undefined,
|
||||
tags: ['vip'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteCustomer', () => {
|
||||
it('deletes customer by id', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({});
|
||||
|
||||
const { result } = renderHook(() => useDeleteCustomer(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync('7');
|
||||
});
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/customers/7/');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user