- 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>
688 lines
21 KiB
TypeScript
688 lines
21 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 the API client
|
|
vi.mock('../../api/client', () => ({
|
|
default: {
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
delete: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
import {
|
|
useCustomerBilling,
|
|
useCustomerPaymentMethods,
|
|
useCreateSetupIntent,
|
|
useDeletePaymentMethod,
|
|
useSetDefaultPaymentMethod,
|
|
} from '../useCustomerBilling';
|
|
import apiClient from '../../api/client';
|
|
|
|
// Create wrapper with fresh QueryClient for each test
|
|
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('useCustomerBilling hooks', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('useCustomerBilling', () => {
|
|
it('fetches customer billing data successfully', async () => {
|
|
const mockBillingData = {
|
|
outstanding: [
|
|
{
|
|
id: 1,
|
|
title: 'Haircut Appointment',
|
|
service_name: 'Basic Haircut',
|
|
amount: 5000,
|
|
amount_display: '$50.00',
|
|
status: 'confirmed',
|
|
start_time: '2025-12-08T10:00:00Z',
|
|
end_time: '2025-12-08T10:30:00Z',
|
|
payment_status: 'unpaid' as const,
|
|
payment_intent_id: null,
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'Massage Session',
|
|
service_name: 'Deep Tissue Massage',
|
|
amount: 8000,
|
|
amount_display: '$80.00',
|
|
status: 'confirmed',
|
|
start_time: '2025-12-09T14:00:00Z',
|
|
end_time: '2025-12-09T15:00:00Z',
|
|
payment_status: 'pending' as const,
|
|
payment_intent_id: 'pi_123456',
|
|
},
|
|
],
|
|
payment_history: [
|
|
{
|
|
id: 1,
|
|
event_id: 100,
|
|
event_title: 'Haircut - John Doe',
|
|
service_name: 'Premium Haircut',
|
|
amount: 7500,
|
|
amount_display: '$75.00',
|
|
currency: 'usd',
|
|
status: 'succeeded',
|
|
payment_intent_id: 'pi_completed_123',
|
|
created_at: '2025-12-01T10:00:00Z',
|
|
completed_at: '2025-12-01T10:05:00Z',
|
|
event_date: '2025-12-01T14:00:00Z',
|
|
},
|
|
{
|
|
id: 2,
|
|
event_id: 101,
|
|
event_title: 'Spa Treatment',
|
|
service_name: 'Facial Treatment',
|
|
amount: 12000,
|
|
amount_display: '$120.00',
|
|
currency: 'usd',
|
|
status: 'succeeded',
|
|
payment_intent_id: 'pi_completed_456',
|
|
created_at: '2025-11-28T09:00:00Z',
|
|
completed_at: '2025-11-28T09:02:00Z',
|
|
event_date: '2025-11-28T15:30:00Z',
|
|
},
|
|
],
|
|
summary: {
|
|
total_spent: 19500,
|
|
total_spent_display: '$195.00',
|
|
total_outstanding: 13000,
|
|
total_outstanding_display: '$130.00',
|
|
payment_count: 2,
|
|
},
|
|
};
|
|
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockBillingData } as any);
|
|
|
|
const { result } = renderHook(() => useCustomerBilling(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/payments/customer/billing/');
|
|
expect(apiClient.get).toHaveBeenCalledTimes(1);
|
|
expect(result.current.data).toEqual(mockBillingData);
|
|
});
|
|
|
|
it('handles empty billing data', async () => {
|
|
const mockEmptyData = {
|
|
outstanding: [],
|
|
payment_history: [],
|
|
summary: {
|
|
total_spent: 0,
|
|
total_spent_display: '$0.00',
|
|
total_outstanding: 0,
|
|
total_outstanding_display: '$0.00',
|
|
payment_count: 0,
|
|
},
|
|
};
|
|
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockEmptyData } as any);
|
|
|
|
const { result } = renderHook(() => useCustomerBilling(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(result.current.data?.outstanding).toHaveLength(0);
|
|
expect(result.current.data?.payment_history).toHaveLength(0);
|
|
expect(result.current.data?.summary.payment_count).toBe(0);
|
|
});
|
|
|
|
it('handles API errors gracefully', async () => {
|
|
const mockError = new Error('Failed to fetch billing data');
|
|
vi.mocked(apiClient.get).mockRejectedValue(mockError);
|
|
|
|
const { result } = renderHook(() => useCustomerBilling(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isError).toBe(true);
|
|
});
|
|
|
|
expect(result.current.error).toEqual(mockError);
|
|
});
|
|
|
|
it('uses 30 second staleTime', async () => {
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: { outstanding: [], payment_history: [], summary: {} } } as any);
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
|
|
|
renderHook(() => useCustomerBilling(), { wrapper });
|
|
|
|
await waitFor(() => {
|
|
const queryState = queryClient.getQueryState(['customerBilling']);
|
|
expect(queryState).toBeDefined();
|
|
});
|
|
|
|
const queryState = queryClient.getQueryState(['customerBilling']);
|
|
expect(queryState?.dataUpdatedAt).toBeDefined();
|
|
});
|
|
|
|
it('does not retry on failure', async () => {
|
|
vi.mocked(apiClient.get).mockRejectedValue(new Error('Network error'));
|
|
|
|
const { result } = renderHook(() => useCustomerBilling(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isError).toBe(true);
|
|
});
|
|
|
|
// Should only be called once (no retries)
|
|
expect(apiClient.get).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('useCustomerPaymentMethods', () => {
|
|
it('fetches payment methods successfully', async () => {
|
|
const mockPaymentMethods = {
|
|
payment_methods: [
|
|
{
|
|
id: 'pm_123456',
|
|
type: 'card',
|
|
brand: 'visa',
|
|
last4: '4242',
|
|
exp_month: 12,
|
|
exp_year: 2025,
|
|
is_default: true,
|
|
},
|
|
{
|
|
id: 'pm_789012',
|
|
type: 'card',
|
|
brand: 'mastercard',
|
|
last4: '5555',
|
|
exp_month: 6,
|
|
exp_year: 2026,
|
|
is_default: false,
|
|
},
|
|
],
|
|
has_stripe_customer: true,
|
|
};
|
|
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockPaymentMethods } as any);
|
|
|
|
const { result } = renderHook(() => useCustomerPaymentMethods(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/payments/customer/payment-methods/');
|
|
expect(apiClient.get).toHaveBeenCalledTimes(1);
|
|
expect(result.current.data).toEqual(mockPaymentMethods);
|
|
expect(result.current.data?.payment_methods).toHaveLength(2);
|
|
});
|
|
|
|
it('handles no payment methods', async () => {
|
|
const mockNoPaymentMethods = {
|
|
payment_methods: [],
|
|
has_stripe_customer: false,
|
|
message: 'No payment methods found',
|
|
};
|
|
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockNoPaymentMethods } as any);
|
|
|
|
const { result } = renderHook(() => useCustomerPaymentMethods(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(result.current.data?.payment_methods).toHaveLength(0);
|
|
expect(result.current.data?.has_stripe_customer).toBe(false);
|
|
expect(result.current.data?.message).toBe('No payment methods found');
|
|
});
|
|
|
|
it('handles API errors gracefully', async () => {
|
|
const mockError = new Error('Failed to fetch payment methods');
|
|
vi.mocked(apiClient.get).mockRejectedValue(mockError);
|
|
|
|
const { result } = renderHook(() => useCustomerPaymentMethods(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isError).toBe(true);
|
|
});
|
|
|
|
expect(result.current.error).toEqual(mockError);
|
|
});
|
|
|
|
it('uses 60 second staleTime', async () => {
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: { payment_methods: [], has_stripe_customer: false } } as any);
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
|
|
|
renderHook(() => useCustomerPaymentMethods(), { wrapper });
|
|
|
|
await waitFor(() => {
|
|
const queryState = queryClient.getQueryState(['customerPaymentMethods']);
|
|
expect(queryState).toBeDefined();
|
|
});
|
|
|
|
const queryState = queryClient.getQueryState(['customerPaymentMethods']);
|
|
expect(queryState?.dataUpdatedAt).toBeDefined();
|
|
});
|
|
|
|
it('does not retry on failure', async () => {
|
|
vi.mocked(apiClient.get).mockRejectedValue(new Error('Network error'));
|
|
|
|
const { result } = renderHook(() => useCustomerPaymentMethods(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isError).toBe(true);
|
|
});
|
|
|
|
// Should only be called once (no retries)
|
|
expect(apiClient.get).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('useCreateSetupIntent', () => {
|
|
it('creates setup intent successfully', async () => {
|
|
const mockSetupIntent = {
|
|
client_secret: 'seti_123_secret_456',
|
|
setup_intent_id: 'seti_123456',
|
|
customer_id: 'cus_789012',
|
|
stripe_account: '',
|
|
publishable_key: 'pk_test_123456',
|
|
};
|
|
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockSetupIntent } as any);
|
|
|
|
const { result } = renderHook(() => useCreateSetupIntent(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
const response = await result.current.mutateAsync();
|
|
expect(response).toEqual(mockSetupIntent);
|
|
});
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/payments/customer/setup-intent/');
|
|
expect(apiClient.post).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('creates setup intent with connected account', async () => {
|
|
const mockSetupIntent = {
|
|
client_secret: 'seti_123_secret_789',
|
|
setup_intent_id: 'seti_789012',
|
|
customer_id: 'cus_345678',
|
|
stripe_account: 'acct_connect_123',
|
|
publishable_key: undefined,
|
|
};
|
|
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockSetupIntent } as any);
|
|
|
|
const { result } = renderHook(() => useCreateSetupIntent(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
let response;
|
|
await act(async () => {
|
|
response = await result.current.mutateAsync();
|
|
});
|
|
|
|
expect(response).toEqual(mockSetupIntent);
|
|
expect(response.stripe_account).toBe('acct_connect_123');
|
|
});
|
|
|
|
it('handles setup intent creation errors', async () => {
|
|
const mockError = new Error('Failed to create setup intent');
|
|
vi.mocked(apiClient.post).mockRejectedValue(mockError);
|
|
|
|
const { result } = renderHook(() => useCreateSetupIntent(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
let caughtError;
|
|
await act(async () => {
|
|
try {
|
|
await result.current.mutateAsync();
|
|
} catch (error) {
|
|
caughtError = error;
|
|
}
|
|
});
|
|
|
|
expect(caughtError).toEqual(mockError);
|
|
});
|
|
|
|
it('tracks mutation loading state', async () => {
|
|
vi.mocked(apiClient.post).mockImplementation(
|
|
() =>
|
|
new Promise((resolve) =>
|
|
setTimeout(() => resolve({ data: { client_secret: 'seti_test' } }), 50)
|
|
)
|
|
);
|
|
|
|
const { result } = renderHook(() => useCreateSetupIntent(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
expect(result.current.isPending).toBe(false);
|
|
|
|
const promise = act(async () => {
|
|
await result.current.mutateAsync();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
await promise;
|
|
});
|
|
});
|
|
|
|
describe('useDeletePaymentMethod', () => {
|
|
it('deletes payment method successfully', async () => {
|
|
const mockDeleteResponse = {
|
|
success: true,
|
|
message: 'Payment method deleted successfully',
|
|
};
|
|
|
|
vi.mocked(apiClient.delete).mockResolvedValue({ data: mockDeleteResponse } as any);
|
|
|
|
const { result } = renderHook(() => useDeletePaymentMethod(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
const response = await result.current.mutateAsync('pm_123456');
|
|
expect(response).toEqual(mockDeleteResponse);
|
|
});
|
|
|
|
expect(apiClient.delete).toHaveBeenCalledWith('/payments/customer/payment-methods/pm_123456/');
|
|
expect(apiClient.delete).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('invalidates payment methods query on success', async () => {
|
|
vi.mocked(apiClient.delete).mockResolvedValue({
|
|
data: { success: true, message: 'Deleted' },
|
|
} as any);
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
|
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
|
|
|
const { result } = renderHook(() => useDeletePaymentMethod(), { wrapper });
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync('pm_123456');
|
|
});
|
|
|
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['customerPaymentMethods'] });
|
|
});
|
|
|
|
it('handles delete errors gracefully', async () => {
|
|
const mockError = new Error('Cannot delete default payment method');
|
|
vi.mocked(apiClient.delete).mockRejectedValue(mockError);
|
|
|
|
const { result } = renderHook(() => useDeletePaymentMethod(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
let caughtError;
|
|
await act(async () => {
|
|
try {
|
|
await result.current.mutateAsync('pm_123456');
|
|
} catch (error) {
|
|
caughtError = error;
|
|
}
|
|
});
|
|
|
|
expect(caughtError).toEqual(mockError);
|
|
});
|
|
|
|
it('does not invalidate queries on error', async () => {
|
|
vi.mocked(apiClient.delete).mockRejectedValue(new Error('Delete failed'));
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
|
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
|
|
|
const { result } = renderHook(() => useDeletePaymentMethod(), { wrapper });
|
|
|
|
await act(async () => {
|
|
try {
|
|
await result.current.mutateAsync('pm_123456');
|
|
} catch {
|
|
// Expected to fail
|
|
}
|
|
});
|
|
|
|
expect(invalidateQueriesSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles multiple payment method deletions', async () => {
|
|
vi.mocked(apiClient.delete).mockResolvedValue({
|
|
data: { success: true, message: 'Deleted' },
|
|
} as any);
|
|
|
|
const { result } = renderHook(() => useDeletePaymentMethod(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync('pm_111111');
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync('pm_222222');
|
|
});
|
|
|
|
expect(apiClient.delete).toHaveBeenCalledTimes(2);
|
|
expect(apiClient.delete).toHaveBeenNthCalledWith(1, '/payments/customer/payment-methods/pm_111111/');
|
|
expect(apiClient.delete).toHaveBeenNthCalledWith(2, '/payments/customer/payment-methods/pm_222222/');
|
|
});
|
|
});
|
|
|
|
describe('useSetDefaultPaymentMethod', () => {
|
|
it('sets default payment method successfully', async () => {
|
|
const mockSetDefaultResponse = {
|
|
success: true,
|
|
message: 'Default payment method updated',
|
|
};
|
|
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockSetDefaultResponse } as any);
|
|
|
|
const { result } = renderHook(() => useSetDefaultPaymentMethod(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await act(async () => {
|
|
const response = await result.current.mutateAsync('pm_789012');
|
|
expect(response).toEqual(mockSetDefaultResponse);
|
|
});
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/payments/customer/payment-methods/pm_789012/default/');
|
|
expect(apiClient.post).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('invalidates payment methods query on success', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({
|
|
data: { success: true, message: 'Updated' },
|
|
} as any);
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
|
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
|
|
|
const { result } = renderHook(() => useSetDefaultPaymentMethod(), { wrapper });
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync('pm_789012');
|
|
});
|
|
|
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['customerPaymentMethods'] });
|
|
});
|
|
|
|
it('handles set default errors gracefully', async () => {
|
|
const mockError = new Error('Payment method not found');
|
|
vi.mocked(apiClient.post).mockRejectedValue(mockError);
|
|
|
|
const { result } = renderHook(() => useSetDefaultPaymentMethod(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
let caughtError;
|
|
await act(async () => {
|
|
try {
|
|
await result.current.mutateAsync('pm_invalid');
|
|
} catch (error) {
|
|
caughtError = error;
|
|
}
|
|
});
|
|
|
|
expect(caughtError).toEqual(mockError);
|
|
});
|
|
|
|
it('does not invalidate queries on error', async () => {
|
|
vi.mocked(apiClient.post).mockRejectedValue(new Error('Update failed'));
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
|
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
|
|
|
const { result } = renderHook(() => useSetDefaultPaymentMethod(), { wrapper });
|
|
|
|
await act(async () => {
|
|
try {
|
|
await result.current.mutateAsync('pm_123456');
|
|
} catch {
|
|
// Expected to fail
|
|
}
|
|
});
|
|
|
|
expect(invalidateQueriesSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles switching default between payment methods', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({
|
|
data: { success: true, message: 'Updated' },
|
|
} as any);
|
|
|
|
const { result } = renderHook(() => useSetDefaultPaymentMethod(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
// Set first method as default
|
|
await act(async () => {
|
|
await result.current.mutateAsync('pm_111111');
|
|
});
|
|
|
|
// Switch to second method as default
|
|
await act(async () => {
|
|
await result.current.mutateAsync('pm_222222');
|
|
});
|
|
|
|
expect(apiClient.post).toHaveBeenCalledTimes(2);
|
|
expect(apiClient.post).toHaveBeenNthCalledWith(1, '/payments/customer/payment-methods/pm_111111/default/');
|
|
expect(apiClient.post).toHaveBeenNthCalledWith(2, '/payments/customer/payment-methods/pm_222222/default/');
|
|
});
|
|
|
|
it('tracks mutation loading state', async () => {
|
|
vi.mocked(apiClient.post).mockImplementation(
|
|
() =>
|
|
new Promise((resolve) =>
|
|
setTimeout(() => resolve({ data: { success: true, message: 'Updated' } }), 50)
|
|
)
|
|
);
|
|
|
|
const { result } = renderHook(() => useSetDefaultPaymentMethod(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
expect(result.current.isPending).toBe(false);
|
|
|
|
const promise = act(async () => {
|
|
await result.current.mutateAsync('pm_123456');
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
await promise;
|
|
});
|
|
});
|
|
});
|