/** * Tests for CloseShiftModal component */ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import CloseShiftModal from '../CloseShiftModal'; import { useCloseShift } from '../../hooks/useCashDrawer'; import type { CashShift } from '../../types'; // Mock hooks vi.mock('../../hooks/useCashDrawer'); const mockUseCloseShift = useCloseShift as any; const mockShift: CashShift = { id: 1, location: 1, status: 'open', opening_balance_cents: 10000, expected_balance_cents: 15000, opened_at: '2024-12-26T09:00:00Z', opened_by: 1, closed_by: null, actual_balance_cents: null, variance_cents: null, cash_breakdown: {}, closing_notes: '', opening_notes: '', closed_at: null, }; const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return ({ children }: { children: React.ReactNode }) => ( {children} ); }; describe('CloseShiftModal', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should render when open', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); expect(screen.getByText(/close cash drawer/i)).toBeInTheDocument(); expect(screen.getByText(/count cash/i)).toBeInTheDocument(); }); it('should display expected balance prominently', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); expect(screen.getByText(/expected balance/i)).toBeInTheDocument(); // Check for the expected balance in the blue box const expectedBalanceElements = screen.getAllByText(/\$150\.00/); expect(expectedBalanceElements.length).toBeGreaterThan(0); expect(expectedBalanceElements[0]).toHaveClass('text-blue-900'); }); it('should have denomination counter inputs', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); // Bills expect(screen.getByLabelText(/\$100 bills/i)).toBeInTheDocument(); expect(screen.getByLabelText(/\$50 bills/i)).toBeInTheDocument(); expect(screen.getByLabelText(/\$20 bills/i)).toBeInTheDocument(); expect(screen.getByLabelText(/\$10 bills/i)).toBeInTheDocument(); expect(screen.getByLabelText(/\$5 bills/i)).toBeInTheDocument(); expect(screen.getByLabelText(/\$1 bills/i)).toBeInTheDocument(); // Coins expect(screen.getByLabelText(/quarters/i)).toBeInTheDocument(); expect(screen.getByLabelText(/dimes/i)).toBeInTheDocument(); expect(screen.getByLabelText(/nickels/i)).toBeInTheDocument(); expect(screen.getByLabelText(/pennies/i)).toBeInTheDocument(); }); it('should calculate total from denominations', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); // Enter: 1x $100, 2x $20, 1x $5 fireEvent.change(screen.getByLabelText(/\$100 bills/i), { target: { value: '1' } }); fireEvent.change(screen.getByLabelText(/\$20 bills/i), { target: { value: '2' } }); fireEvent.change(screen.getByLabelText(/\$5 bills/i), { target: { value: '1' } }); // Total should be $145.00 - look for it in the "Actual Balance" section expect(screen.getByText('Actual Balance')).toBeInTheDocument(); const actualBalanceElements = screen.getAllByText(/\$145\.00/); expect(actualBalanceElements.length).toBeGreaterThan(0); }); it('should show variance in green when actual matches expected', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); // Enter exactly $150.00 (expected balance) fireEvent.change(screen.getByLabelText(/\$100 bills/i), { target: { value: '1' } }); fireEvent.change(screen.getByLabelText(/\$50 bills/i), { target: { value: '1' } }); // Find variance section - get parent container with background const varianceLabel = screen.getByText('Variance'); const parentDiv = varianceLabel.parentElement; expect(parentDiv).toHaveClass('bg-green-50'); // Variance amount should be green const varianceAmounts = screen.getAllByText(/\$0\.00/); const varianceAmount = varianceAmounts.find(el => el.classList.contains('text-green-600')); expect(varianceAmount).toBeDefined(); }); it('should show variance in red when actual is short', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); // Enter $100.00 (short by $50) fireEvent.change(screen.getByLabelText(/\$100 bills/i), { target: { value: '1' } }); // Find variance section - should be red const varianceLabel = screen.getByText('Variance'); const parentDiv = varianceLabel.parentElement; expect(parentDiv).toHaveClass('bg-red-50'); // Variance amount should be red const varianceText = screen.getByText(/-\$50\.00/); expect(varianceText).toHaveClass('text-red-600'); }); it('should show variance in green when actual is over', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); // Enter $200.00 (over by $50) fireEvent.change(screen.getByLabelText(/\$100 bills/i), { target: { value: '2' } }); // Find variance section - should be green const varianceLabel = screen.getByText('Variance'); const parentDiv = varianceLabel.parentElement; expect(parentDiv).toHaveClass('bg-green-50'); // Variance amount should be green and positive const varianceText = screen.getByText(/\+\$50\.00/); expect(varianceText).toHaveClass('text-green-600'); }); it('should allow adding closing notes', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); const notesInput = screen.getByPlaceholderText(/notes about the shift/i); fireEvent.change(notesInput, { target: { value: 'Short due to refund' } }); expect(notesInput).toHaveValue('Short due to refund'); }); it('should call onClose when Cancel clicked', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); const onClose = vi.fn(); render( , { wrapper: createWrapper() } ); fireEvent.click(screen.getByRole('button', { name: /cancel/i })); expect(onClose).toHaveBeenCalled(); }); it('should close shift with correct data when Close Shift clicked', async () => { const mockMutateAsync = vi.fn().mockResolvedValue({}); mockUseCloseShift.mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false, }); const onSuccess = vi.fn(); render( , { wrapper: createWrapper() } ); // Enter cash count fireEvent.change(screen.getByLabelText(/\$100 bills/i), { target: { value: '1' } }); fireEvent.change(screen.getByLabelText(/\$20 bills/i), { target: { value: '2' } }); fireEvent.change(screen.getByLabelText(/\$5 bills/i), { target: { value: '1' } }); // Add notes const notesInput = screen.getByPlaceholderText(/notes about the shift/i); fireEvent.change(notesInput, { target: { value: 'End of day' } }); // Submit fireEvent.click(screen.getByRole('button', { name: /close shift/i })); await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith({ shiftId: 1, actual_balance_cents: 14500, // $145.00 cash_breakdown: { '10000': 1, // 1x $100 '2000': 2, // 2x $20 '500': 1, // 1x $5 }, closing_notes: 'End of day', }); }); expect(onSuccess).toHaveBeenCalled(); }); it('should disable Close Shift button when amount is zero', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); const closeButton = screen.getByRole('button', { name: /close shift/i }); expect(closeButton).toBeDisabled(); }); it('should show loading state during submission', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: true, }); render( , { wrapper: createWrapper() } ); expect(screen.getByText(/closing\.\.\./i)).toBeInTheDocument(); }); it('should handle coin denominations correctly', () => { mockUseCloseShift.mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); render( , { wrapper: createWrapper() } ); // Enter: 4 quarters, 10 dimes, 2 nickels, 5 pennies // = $1.00 + $1.00 + $0.10 + $0.05 = $2.15 fireEvent.change(screen.getByLabelText(/quarters/i), { target: { value: '4' } }); fireEvent.change(screen.getByLabelText(/dimes/i), { target: { value: '10' } }); fireEvent.change(screen.getByLabelText(/nickels/i), { target: { value: '2' } }); fireEvent.change(screen.getByLabelText(/pennies/i), { target: { value: '5' } }); // Should show $2.15 in the Actual Balance section expect(screen.getByText('Actual Balance')).toBeInTheDocument(); const actualBalanceElements = screen.getAllByText(/\$2\.15/); expect(actualBalanceElements.length).toBeGreaterThan(0); }); });