/** * Unit tests for CustomerSupport component * * Tests support ticket functionality including: * - Ticket list display * - Ticket creation * - Ticket detail view * - Comments/conversation * - Status badges * - Loading and error states */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { MemoryRouter, Outlet, Routes, Route } from 'react-router-dom'; import CustomerSupport from '../CustomerSupport'; // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, fallback?: string, options?: any) => { if (options?.number !== undefined) { return fallback?.replace('{{number}}', options.number) || key; } if (options?.date !== undefined) { return fallback?.replace('{{date}}', options.date) || key; } return fallback || key; }, }), })); // Mock the ticket hooks const mockTickets = vi.fn(); const mockCreateTicket = vi.fn(); const mockTicketComments = vi.fn(); const mockCreateTicketComment = vi.fn(); vi.mock('../../../hooks/useTickets', () => ({ useTickets: () => mockTickets(), useCreateTicket: () => ({ mutateAsync: mockCreateTicket, isPending: false, }), useTicketComments: (ticketId: string) => mockTicketComments(ticketId), useCreateTicketComment: () => ({ mutateAsync: mockCreateTicketComment, isPending: false, }), })); const mockUser = { id: 'user-1', email: 'customer@example.com', name: 'John Doe', role: 'customer' as const, }; const mockBusiness = { id: 'biz-1', name: 'Test Business', subdomain: 'test', cancellationWindowHours: 24, lateCancellationFeePercent: 50, }; const defaultTickets = [ { id: '1', ticketNumber: 'TKT-001', ticketType: 'CUSTOMER', subject: 'Appointment rescheduling', description: 'I need to reschedule my appointment', status: 'OPEN', priority: 'MEDIUM', category: 'APPOINTMENT', createdAt: '2024-12-01T10:00:00Z', updatedAt: '2024-12-01T10:00:00Z', creatorEmail: 'customer@example.com', creatorFullName: 'John Doe', }, { id: '2', ticketNumber: 'TKT-002', ticketType: 'CUSTOMER', subject: 'Refund request', description: 'I would like a refund for my last appointment', status: 'RESOLVED', priority: 'HIGH', category: 'REFUND', createdAt: '2024-11-20T14:00:00Z', updatedAt: '2024-11-22T16:00:00Z', resolvedAt: '2024-11-22T16:00:00Z', creatorEmail: 'customer@example.com', creatorFullName: 'John Doe', }, { id: '3', ticketNumber: 'TKT-003', ticketType: 'PLATFORM', subject: 'Platform issue', description: 'This should be filtered out', status: 'OPEN', priority: 'LOW', category: 'OTHER', createdAt: '2024-11-15T09:00:00Z', updatedAt: '2024-11-15T09:00:00Z', creatorEmail: 'platform@example.com', creatorFullName: 'Platform User', }, ]; const defaultComments = [ { id: '1', ticket: '1', author: 'staff-1', authorEmail: 'staff@example.com', authorFullName: 'Support Staff', commentText: 'We have received your request and are looking into it.', isInternal: false, createdAt: '2024-12-01T11:00:00Z', }, { id: '2', ticket: '1', author: 'staff-2', authorEmail: 'manager@example.com', authorFullName: 'Manager', commentText: 'Internal note: escalate this', isInternal: true, createdAt: '2024-12-01T12:00:00Z', }, { id: '3', ticket: '1', author: 'user-1', authorEmail: 'customer@example.com', authorFullName: 'John Doe', commentText: 'Thank you for your help!', isInternal: false, createdAt: '2024-12-01T13:00:00Z', }, ]; const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); const OutletWrapper = () => { return React.createElement(Outlet, { context: { user: mockUser, business: mockBusiness }, }); }; return ({ children }: { children: React.ReactNode }) => React.createElement( QueryClientProvider, { client: queryClient }, React.createElement( MemoryRouter, { initialEntries: ['/customer/support'] }, React.createElement( Routes, null, React.createElement(Route, { element: React.createElement(OutletWrapper), children: React.createElement(Route, { path: 'customer/support', element: children, }), }) ) ) ); }; describe('CustomerSupport', () => { beforeEach(() => { vi.clearAllMocks(); mockTickets.mockReturnValue({ data: defaultTickets, isLoading: false, refetch: vi.fn(), }); mockTicketComments.mockReturnValue({ data: [], isLoading: false, }); }); describe('Page Header', () => { it('should render the page title', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('Support')).toBeInTheDocument(); }); it('should render the page subtitle', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText(/Get help with your appointments and account/)).toBeInTheDocument(); }); it('should render New Request button', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('New Request')).toBeInTheDocument(); }); }); describe('Quick Help Section', () => { it('should render Quick Help heading', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('Quick Help')).toBeInTheDocument(); }); it('should render Contact Us option', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('Contact Us')).toBeInTheDocument(); expect(screen.getByText('Submit a support request')).toBeInTheDocument(); }); it('should render Email Us option', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('Email Us')).toBeInTheDocument(); expect(screen.getByText('Get help via email')).toBeInTheDocument(); }); it('should have email link with business subdomain', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const emailLink = screen.getByText('Email Us').closest('a'); expect(emailLink).toHaveAttribute('href', 'mailto:support@test.smoothschedule.com'); }); it('should open new ticket form when Contact Us is clicked', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const contactButton = screen.getAllByText('Contact Us')[0].closest('a'); fireEvent.click(contactButton!); expect(screen.getByText('Submit a Support Request')).toBeInTheDocument(); }); }); describe('My Support Requests Section', () => { it('should render section heading', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('My Support Requests')).toBeInTheDocument(); }); it('should display customer tickets only', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('Appointment rescheduling')).toBeInTheDocument(); expect(screen.getByText('Refund request')).toBeInTheDocument(); expect(screen.queryByText('Platform issue')).not.toBeInTheDocument(); }); it('should display ticket numbers', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText(/TKT-001/)).toBeInTheDocument(); expect(screen.getByText(/TKT-002/)).toBeInTheDocument(); }); it('should display ticket status badges', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText(/open/i)).toBeInTheDocument(); expect(screen.getByText(/resolved/i)).toBeInTheDocument(); }); it('should display ticket creation dates', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const dateElements = screen.getAllByText(/12\/1\/2024|11\/20\/2024/); expect(dateElements.length).toBeGreaterThan(0); }); it('should render loading state', () => { mockTickets.mockReturnValue({ data: [], isLoading: true, refetch: vi.fn(), }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); it('should render empty state when no tickets', () => { mockTickets.mockReturnValue({ data: [], isLoading: false, refetch: vi.fn(), }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText(/haven't submitted any support requests yet/)).toBeInTheDocument(); }); it('should show Submit first request button in empty state', () => { mockTickets.mockReturnValue({ data: [], isLoading: false, refetch: vi.fn(), }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); expect(screen.getByText('Submit your first request')).toBeInTheDocument(); }); it('should open ticket detail when ticket is clicked', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); expect(screen.getByText('Appointment Details')).toBeInTheDocument(); }); }); describe('New Ticket Form Modal', () => { beforeEach(() => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const newRequestButton = screen.getAllByText('New Request')[0]; fireEvent.click(newRequestButton); }); it('should render form modal when New Request is clicked', () => { expect(screen.getByText('Submit a Support Request')).toBeInTheDocument(); }); it('should render subject input field', () => { expect(screen.getByLabelText(/subject/i)).toBeInTheDocument(); }); it('should render category dropdown', () => { expect(screen.getByLabelText(/category/i)).toBeInTheDocument(); }); it('should render priority dropdown', () => { expect(screen.getByLabelText(/priority/i)).toBeInTheDocument(); }); it('should render description textarea', () => { expect(screen.getByLabelText(/description/i)).toBeInTheDocument(); }); it('should render Cancel button', () => { expect(screen.getByText('Cancel')).toBeInTheDocument(); }); it('should render Submit Request button', () => { expect(screen.getByText('Submit Request')).toBeInTheDocument(); }); it('should close modal when Cancel is clicked', () => { const cancelButton = screen.getByText('Cancel'); fireEvent.click(cancelButton); expect(screen.queryByText('Submit a Support Request')).not.toBeInTheDocument(); }); it('should close modal when clicking outside', () => { const backdrop = screen.getByText('Submit a Support Request').closest('.fixed'); fireEvent.click(backdrop!); expect(screen.queryByText('Submit a Support Request')).not.toBeInTheDocument(); }); it('should not close modal when clicking inside form', () => { const formContent = screen.getByLabelText(/subject/i).closest('.bg-white'); fireEvent.click(formContent!); expect(screen.getByText('Submit a Support Request')).toBeInTheDocument(); }); it('should submit form with correct data', async () => { const subjectInput = screen.getByLabelText(/subject/i); const descriptionInput = screen.getByLabelText(/description/i); const submitButton = screen.getByText('Submit Request'); fireEvent.change(subjectInput, { target: { value: 'Test ticket' } }); fireEvent.change(descriptionInput, { target: { value: 'Test description' } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockCreateTicket).toHaveBeenCalledWith({ subject: 'Test ticket', description: 'Test description', category: 'GENERAL_INQUIRY', priority: 'MEDIUM', ticketType: 'CUSTOMER', }); }); }); it('should display category options', () => { const categorySelect = screen.getByLabelText(/category/i); expect(categorySelect).toBeInTheDocument(); // Options are rendered as part of select expect(screen.getByText(/appointment/i)).toBeInTheDocument(); }); it('should display priority options', () => { const prioritySelect = screen.getByLabelText(/priority/i); expect(prioritySelect).toBeInTheDocument(); expect(screen.getByText(/medium/i)).toBeInTheDocument(); }); it('should require subject field', () => { const subjectInput = screen.getByLabelText(/subject/i) as HTMLInputElement; expect(subjectInput.required).toBe(true); }); it('should require description field', () => { const descriptionInput = screen.getByLabelText(/description/i) as HTMLTextAreaElement; expect(descriptionInput.required).toBe(true); }); }); describe('Ticket Detail View', () => { beforeEach(() => { mockTicketComments.mockReturnValue({ data: defaultComments, isLoading: false, }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); }); it('should display ticket subject', () => { expect(screen.getByText('Appointment rescheduling')).toBeInTheDocument(); }); it('should display ticket number', () => { expect(screen.getByText(/Ticket #TKT-001/)).toBeInTheDocument(); }); it('should display creation date', () => { expect(screen.getByText(/Created.*12\/1\/2024/)).toBeInTheDocument(); }); it('should display status badge', () => { expect(screen.getByText(/open/i)).toBeInTheDocument(); }); it('should display priority badge', () => { expect(screen.getByText(/medium/i)).toBeInTheDocument(); }); it('should display ticket description', () => { expect(screen.getByText('I need to reschedule my appointment')).toBeInTheDocument(); }); it('should display back button', () => { expect(screen.getByText(/Back to tickets/)).toBeInTheDocument(); }); it('should return to ticket list when back button is clicked', () => { const backButton = screen.getByText(/Back to tickets/); fireEvent.click(backButton); expect(screen.getByText('My Support Requests')).toBeInTheDocument(); expect(screen.queryByText(/Ticket #TKT-001/)).not.toBeInTheDocument(); }); it('should display Conversation heading', () => { expect(screen.getByText('Conversation')).toBeInTheDocument(); }); it('should filter out internal comments', () => { expect(screen.getByText('We have received your request and are looking into it.')).toBeInTheDocument(); expect(screen.getByText('Thank you for your help!')).toBeInTheDocument(); expect(screen.queryByText('Internal note: escalate this')).not.toBeInTheDocument(); }); it('should display comment author names', () => { expect(screen.getByText('Support Staff')).toBeInTheDocument(); expect(screen.getByText('John Doe')).toBeInTheDocument(); }); it('should display comment timestamps', () => { const timestamps = screen.getAllByText(/12\/1\/2024/); expect(timestamps.length).toBeGreaterThan(0); }); it('should render reply form', () => { expect(screen.getByLabelText('Your Reply')).toBeInTheDocument(); }); it('should render Send Reply button', () => { expect(screen.getByText('Send Reply')).toBeInTheDocument(); }); it('should submit reply when Send Reply is clicked', async () => { const replyInput = screen.getByLabelText('Your Reply'); const sendButton = screen.getByText('Send Reply'); fireEvent.change(replyInput, { target: { value: 'My reply message' } }); fireEvent.click(sendButton); await waitFor(() => { expect(mockCreateTicketComment).toHaveBeenCalledWith({ ticketId: '1', commentData: { commentText: 'My reply message', isInternal: false, }, }); }); }); it('should clear reply input after submission', async () => { const replyInput = screen.getByLabelText('Your Reply') as HTMLTextAreaElement; const sendButton = screen.getByText('Send Reply'); fireEvent.change(replyInput, { target: { value: 'My reply' } }); fireEvent.click(sendButton); await waitFor(() => { expect(replyInput.value).toBe(''); }); }); it('should disable Send Reply button when input is empty', () => { const sendButton = screen.getByText('Send Reply'); expect(sendButton).toBeDisabled(); }); it('should enable Send Reply button when input has text', () => { const replyInput = screen.getByLabelText('Your Reply'); const sendButton = screen.getByText('Send Reply'); fireEvent.change(replyInput, { target: { value: 'Some text' } }); expect(sendButton).not.toBeDisabled(); }); it('should show empty state when no comments', () => { mockTicketComments.mockReturnValue({ data: [], isLoading: false, }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); expect(screen.getByText(/No replies yet/)).toBeInTheDocument(); }); it('should show loading state for comments', () => { mockTicketComments.mockReturnValue({ data: [], isLoading: true, }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); }); describe('Closed Ticket Behavior', () => { beforeEach(() => { const closedTicket = { ...defaultTickets[0], status: 'CLOSED', }; mockTickets.mockReturnValue({ data: [closedTicket], isLoading: false, refetch: vi.fn(), }); mockTicketComments.mockReturnValue({ data: [], isLoading: false, }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); }); it('should not show reply form for closed tickets', () => { expect(screen.queryByLabelText('Your Reply')).not.toBeInTheDocument(); }); it('should show closed ticket message', () => { expect(screen.getByText(/This ticket is closed/)).toBeInTheDocument(); }); it('should suggest opening new request for closed tickets', () => { expect(screen.getByText(/open a new support request/)).toBeInTheDocument(); }); }); describe('Status Messages', () => { it('should show open status message', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); expect(screen.getByText(/request has been received/)).toBeInTheDocument(); }); it('should show in progress status message', () => { const inProgressTicket = { ...defaultTickets[0], status: 'IN_PROGRESS', }; mockTickets.mockReturnValue({ data: [inProgressTicket], isLoading: false, refetch: vi.fn(), }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); expect(screen.getByText(/currently working on your request/)).toBeInTheDocument(); }); it('should show awaiting response status message', () => { const awaitingTicket = { ...defaultTickets[0], status: 'AWAITING_RESPONSE', }; mockTickets.mockReturnValue({ data: [awaitingTicket], isLoading: false, refetch: vi.fn(), }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); expect(screen.getByText(/need additional information/)).toBeInTheDocument(); }); it('should show resolved status message', () => { mockTickets.mockReturnValue({ data: [defaultTickets[1]], // Resolved ticket isLoading: false, refetch: vi.fn(), }); render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Refund request').closest('button'); fireEvent.click(ticketButton!); expect(screen.getByText(/request has been resolved/)).toBeInTheDocument(); }); }); describe('Status and Priority Badges', () => { it('should render OPEN status badge correctly', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const openBadge = screen.getByText(/open/i); expect(openBadge).toBeInTheDocument(); }); it('should render RESOLVED status badge correctly', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const resolvedBadge = screen.getByText(/resolved/i); expect(resolvedBadge).toBeInTheDocument(); }); it('should render MEDIUM priority badge correctly', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Appointment rescheduling').closest('button'); fireEvent.click(ticketButton!); const mediumBadge = screen.getByText(/medium/i); expect(mediumBadge).toBeInTheDocument(); }); it('should render HIGH priority badge correctly', () => { render(React.createElement(CustomerSupport), { wrapper: createWrapper() }); const ticketButton = screen.getByText('Refund request').closest('button'); fireEvent.click(ticketButton!); const highBadge = screen.getByText(/high/i); expect(highBadge).toBeInTheDocument(); }); }); });