/** * Tests for GiftCardPaymentPanel Component * * Features: * - Gift card code input (manual entry or scan) * - Look up button to check balance * - Shows card balance when found * - Amount to redeem input * - Apply button to add gift card payment * - Error handling for invalid/expired cards */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; import GiftCardPaymentPanel from '../GiftCardPaymentPanel'; import * as useGiftCardsHooks from '../../hooks/useGiftCards'; import type { GiftCard } from '../../types'; // Mock the useGiftCards hooks vi.mock('../../hooks/useGiftCards'); describe('GiftCardPaymentPanel', () => { let queryClient: QueryClient; const mockOnApply = vi.fn(); const mockOnCancel = vi.fn(); const createWrapper = () => { queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); // eslint-disable-next-line react/display-name return ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); }; beforeEach(() => { vi.clearAllMocks(); }); it('should render gift card input form', () => { vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: false, isError: false, data: undefined, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); expect(screen.getByPlaceholderText(/enter gift card code/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /lookup/i })).toBeInTheDocument(); }); it('should allow user to enter gift card code', async () => { const user = userEvent.setup(); vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: false, isError: false, data: undefined, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); const input = screen.getByPlaceholderText(/enter gift card code/i); await user.type(input, 'GC-ABC123'); expect(input).toHaveValue('GC-ABC123'); }); it('should lookup gift card when lookup button is clicked', async () => { const user = userEvent.setup(); const mockMutate = vi.fn(); vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: mockMutate, mutateAsync: vi.fn(), isPending: false, isSuccess: false, isError: false, data: undefined, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); const input = screen.getByPlaceholderText(/enter gift card code/i); const lookupButton = screen.getByRole('button', { name: /lookup/i }); await user.type(input, 'GC-ABC123'); await user.click(lookupButton); expect(mockMutate).toHaveBeenCalledWith('GC-ABC123'); }); it('should display gift card balance when lookup succeeds', () => { const mockGiftCard: GiftCard = { id: 1, code: 'GC-ABC123', initial_balance_cents: 10000, current_balance_cents: 7500, status: 'active', purchased_by: null, recipient_email: '', recipient_name: '', created_at: '2024-01-01T00:00:00Z', expires_at: null, }; vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: true, isError: false, data: mockGiftCard, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); expect(screen.getByText(/GC-ABC123/i)).toBeInTheDocument(); expect(screen.getByText(/\$75\.00/)).toBeInTheDocument(); }); it('should show loading state during lookup', () => { vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: true, isSuccess: false, isError: false, data: undefined, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); expect(screen.getByRole('button', { name: /lookup/i })).toBeDisabled(); }); it('should show error for invalid gift card', () => { vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: false, isError: true, data: undefined, error: { message: 'Gift card not found' } as any, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); expect(screen.getByText(/gift card not found/i)).toBeInTheDocument(); }); it('should show error for expired gift card', () => { const expiredGiftCard: GiftCard = { id: 1, code: 'GC-EXPIRED', initial_balance_cents: 5000, current_balance_cents: 5000, status: 'expired', purchased_by: null, recipient_email: '', recipient_name: '', created_at: '2024-01-01T00:00:00Z', expires_at: '2024-06-01T00:00:00Z', }; vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: true, isError: false, data: expiredGiftCard, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); expect(screen.getByText(/expired/i)).toBeInTheDocument(); }); it('should show error for depleted gift card', () => { const depletedGiftCard: GiftCard = { id: 1, code: 'GC-DEPLETED', initial_balance_cents: 5000, current_balance_cents: 0, status: 'depleted', purchased_by: null, recipient_email: '', recipient_name: '', created_at: '2024-01-01T00:00:00Z', expires_at: null, }; vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: true, isError: false, data: depletedGiftCard, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); expect(screen.getByText(/no balance remaining/i)).toBeInTheDocument(); }); it('should default redemption amount to remaining balance or amount due, whichever is less', () => { const mockGiftCard: GiftCard = { id: 1, code: 'GC-ABC123', initial_balance_cents: 10000, current_balance_cents: 7500, status: 'active', purchased_by: null, recipient_email: '', recipient_name: '', created_at: '2024-01-01T00:00:00Z', expires_at: null, }; vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: true, isError: false, data: mockGiftCard, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); // FormCurrencyInput displays value in dollars as formatted text // With amountDue=5000 cents ($50), this should be the default expect(screen.getByDisplayValue('$50.00')).toBeInTheDocument(); }); it('should allow user to change redemption amount', async () => { const user = userEvent.setup(); const mockGiftCard: GiftCard = { id: 1, code: 'GC-ABC123', initial_balance_cents: 10000, current_balance_cents: 10000, status: 'active', purchased_by: null, recipient_email: '', recipient_name: '', created_at: '2024-01-01T00:00:00Z', expires_at: null, }; vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: true, isError: false, data: mockGiftCard, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); // Find the currency input by its placeholder or current value const amountInput = screen.getByDisplayValue('$50.00'); await user.clear(amountInput); await user.type(amountInput, '25'); // After typing "25", the display should show "$0.25" (25 cents) expect(screen.getByDisplayValue('$0.25')).toBeInTheDocument(); }); it('should call onApply with correct payment info when Apply is clicked', async () => { const user = userEvent.setup(); const mockGiftCard: GiftCard = { id: 1, code: 'GC-ABC123', initial_balance_cents: 10000, current_balance_cents: 7500, status: 'active', purchased_by: null, recipient_email: '', recipient_name: '', created_at: '2024-01-01T00:00:00Z', expires_at: null, }; vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: true, isError: false, data: mockGiftCard, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); const applyButton = screen.getByRole('button', { name: /apply/i }); await user.click(applyButton); expect(mockOnApply).toHaveBeenCalledWith({ gift_card_code: 'GC-ABC123', amount_cents: 5000, gift_card: mockGiftCard, }); }); it('should not allow applying more than gift card balance', async () => { const user = userEvent.setup(); const mockGiftCard: GiftCard = { id: 1, code: 'GC-ABC123', initial_balance_cents: 5000, current_balance_cents: 2500, status: 'active', purchased_by: null, recipient_email: '', recipient_name: '', created_at: '2024-01-01T00:00:00Z', expires_at: null, }; vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: true, isError: false, data: mockGiftCard, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); // Default amount should be $25 (2500 cents) since that's the card balance const amountInput = screen.getByDisplayValue('$25.00'); await user.clear(amountInput); await user.type(amountInput, '5000'); // Try to redeem $50.00 (5000 cents) when balance is $25 const applyButton = screen.getByRole('button', { name: /apply/i }); await user.click(applyButton); // Should show error, not call onApply expect(screen.getByText(/exceeds gift card balance/i)).toBeInTheDocument(); expect(mockOnApply).not.toHaveBeenCalled(); }); it('should call onCancel when Cancel button is clicked', async () => { const user = userEvent.setup(); vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: false, isError: false, data: undefined, error: null, reset: vi.fn(), } as any); render( , { wrapper: createWrapper() } ); const cancelButton = screen.getByRole('button', { name: /cancel/i }); await user.click(cancelButton); expect(mockOnCancel).toHaveBeenCalled(); }); it('should reset form when a new lookup is performed', async () => { const user = userEvent.setup(); const mockMutate = vi.fn(); const mockReset = vi.fn(); vi.mocked(useGiftCardsHooks.useLookupGiftCard).mockReturnValue({ mutate: mockMutate, mutateAsync: vi.fn(), isPending: false, isSuccess: false, isError: false, data: undefined, error: null, reset: mockReset, } as any); render( , { wrapper: createWrapper() } ); const input = screen.getByPlaceholderText(/enter gift card code/i); const lookupButton = screen.getByRole('button', { name: /lookup/i }); await user.type(input, 'GC-ABC123'); await user.click(lookupButton); // Clear and lookup another card await user.clear(input); await user.type(input, 'GC-XYZ789'); expect(mockReset).toHaveBeenCalled(); }); });