Add global navigation search, cancellation policies, and UI improvements
- Add global search in top bar for navigating to dashboard pages - Add cancellation policy settings (window hours, late fee, deposit refund) - Display booking policies on customer confirmation page - Filter API tokens by sandbox/live mode - Widen settings layout and full-width site builder - Add help documentation search with OpenAI integration - Add blocked time ranges API for calendar visualization - Update business hours settings with holiday management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
277
frontend/src/hooks/__tests__/useServiceAddons.test.ts
Normal file
277
frontend/src/hooks/__tests__/useServiceAddons.test.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
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';
|
||||
import {
|
||||
useServiceAddons,
|
||||
usePublicServiceAddons,
|
||||
useServiceAddon,
|
||||
useCreateServiceAddon,
|
||||
useUpdateServiceAddon,
|
||||
useDeleteServiceAddon,
|
||||
useToggleServiceAddon,
|
||||
useReorderServiceAddons,
|
||||
} from '../useServiceAddons';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
vi.mock('../../api/client');
|
||||
|
||||
const mockAddon = {
|
||||
id: 1,
|
||||
service: 1,
|
||||
resource: 2,
|
||||
resource_name: 'John Doe',
|
||||
resource_type: 'STAFF',
|
||||
name: 'Extra Massage',
|
||||
description: 'Additional massage time',
|
||||
display_order: 0,
|
||||
price: '25.00',
|
||||
price_cents: 2500,
|
||||
duration_mode: 'SEQUENTIAL' as const,
|
||||
additional_duration: 30,
|
||||
is_active: true,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
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('useServiceAddons hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useServiceAddons', () => {
|
||||
it('fetches addons for a service', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: [mockAddon] });
|
||||
|
||||
const { result } = renderHook(() => useServiceAddons(1), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/service-addons/', {
|
||||
params: { service: 1, show_inactive: 'true' },
|
||||
});
|
||||
expect(result.current.data).toHaveLength(1);
|
||||
expect(result.current.data?.[0].name).toBe('Extra Massage');
|
||||
});
|
||||
|
||||
it('transforms price correctly', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: [mockAddon] });
|
||||
|
||||
const { result } = renderHook(() => useServiceAddons(1), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data?.[0].price).toBe(25);
|
||||
expect(result.current.data?.[0].price_cents).toBe(2500);
|
||||
});
|
||||
|
||||
it('does not fetch when serviceId is null', () => {
|
||||
const { result } = renderHook(() => useServiceAddons(null), { wrapper: createWrapper() });
|
||||
|
||||
expect(result.current.fetchStatus).toBe('idle');
|
||||
expect(apiClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles string service IDs', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: [] });
|
||||
|
||||
const { result } = renderHook(() => useServiceAddons('123'), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/service-addons/', {
|
||||
params: { service: '123', show_inactive: 'true' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePublicServiceAddons', () => {
|
||||
it('fetches public addons for a service', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
||||
data: { addons: [mockAddon], count: 1 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => usePublicServiceAddons(1), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/public/service-addons/', {
|
||||
params: { service_id: 1 },
|
||||
});
|
||||
expect(result.current.data?.addons).toHaveLength(1);
|
||||
expect(result.current.data?.count).toBe(1);
|
||||
});
|
||||
|
||||
it('does not fetch when serviceId is null', () => {
|
||||
const { result } = renderHook(() => usePublicServiceAddons(null), { wrapper: createWrapper() });
|
||||
|
||||
expect(result.current.fetchStatus).toBe('idle');
|
||||
expect(apiClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useServiceAddon', () => {
|
||||
it('fetches a single addon by ID', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockAddon });
|
||||
|
||||
const { result } = renderHook(() => useServiceAddon(1), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/service-addons/1/');
|
||||
expect(result.current.data?.name).toBe('Extra Massage');
|
||||
});
|
||||
|
||||
it('does not fetch when id is null', () => {
|
||||
const { result } = renderHook(() => useServiceAddon(null), { wrapper: createWrapper() });
|
||||
|
||||
expect(result.current.fetchStatus).toBe('idle');
|
||||
expect(apiClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateServiceAddon', () => {
|
||||
it('creates a new addon', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockAddon });
|
||||
|
||||
const { result } = renderHook(() => useCreateServiceAddon(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
service: 1,
|
||||
resource: 2,
|
||||
name: 'Extra Massage',
|
||||
price_cents: 2500,
|
||||
duration_mode: 'SEQUENTIAL',
|
||||
additional_duration: 30,
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/service-addons/', {
|
||||
service: 1,
|
||||
resource: 2,
|
||||
name: 'Extra Massage',
|
||||
price_cents: 2500,
|
||||
duration_mode: 'SEQUENTIAL',
|
||||
additional_duration: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates addon without resource (price-only addon)', async () => {
|
||||
const priceOnlyAddon = { ...mockAddon, resource: null, resource_name: null };
|
||||
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: priceOnlyAddon });
|
||||
|
||||
const { result } = renderHook(() => useCreateServiceAddon(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
service: 1,
|
||||
resource: null,
|
||||
name: 'Gift Wrapping',
|
||||
price_cents: 500,
|
||||
duration_mode: 'PARALLEL',
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/service-addons/', expect.objectContaining({
|
||||
resource: null,
|
||||
duration_mode: 'PARALLEL',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateServiceAddon', () => {
|
||||
it('updates an addon', async () => {
|
||||
const updatedAddon = { ...mockAddon, name: 'Updated Addon' };
|
||||
vi.mocked(apiClient.patch).mockResolvedValueOnce({ data: updatedAddon });
|
||||
|
||||
const { result } = renderHook(() => useUpdateServiceAddon(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
updates: { name: 'Updated Addon' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/service-addons/1/', { name: 'Updated Addon' });
|
||||
});
|
||||
|
||||
it('updates price_cents', async () => {
|
||||
const updatedAddon = { ...mockAddon, price_cents: 3000 };
|
||||
vi.mocked(apiClient.patch).mockResolvedValueOnce({ data: updatedAddon });
|
||||
|
||||
const { result } = renderHook(() => useUpdateServiceAddon(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
updates: { price_cents: 3000 },
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/service-addons/1/', { price_cents: 3000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteServiceAddon', () => {
|
||||
it('deletes an addon', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValueOnce({});
|
||||
|
||||
const { result } = renderHook(() => useDeleteServiceAddon(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({ id: 1, serviceId: 1 });
|
||||
});
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/service-addons/1/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useToggleServiceAddon', () => {
|
||||
it('toggles addon active status', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValueOnce({
|
||||
data: { ...mockAddon, is_active: false },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useToggleServiceAddon(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({ id: 1, serviceId: 1 });
|
||||
});
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/service-addons/1/toggle_active/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useReorderServiceAddons', () => {
|
||||
it('reorders addons', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValueOnce({
|
||||
data: { success: true },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useReorderServiceAddons(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
serviceId: 1,
|
||||
orderedIds: [3, 1, 2],
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/service-addons/reorder/', { order: [3, 1, 2] });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user