/** * Unit tests for HelpApiDocs component * * Tests cover: * - Component rendering * - Navigation sections display * - Code examples in multiple languages * - Token selector functionality * - Section navigation and scroll behavior * - No test tokens warning banner * - Back button functionality * - Language switcher in code blocks */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; import React from 'react'; import HelpApiDocs from '../HelpApiDocs'; // Mock the useTestTokensForDocs hook const mockTestTokensData = [ { id: 'token-1', name: 'Test Token 1', key_prefix: 'ss_test_abc123', created_at: '2025-01-01T00:00:00Z', }, { id: 'token-2', name: 'Test Token 2', key_prefix: 'ss_test_def456', created_at: '2025-01-02T00:00:00Z', }, ]; const mockUseTestTokensForDocs = vi.fn(() => ({ data: mockTestTokensData, isLoading: false, error: null, })); vi.mock('../../hooks/useApiTokens', () => ({ useTestTokensForDocs: mockUseTestTokensForDocs, })); // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => { const translations: Record = { 'common.back': 'Back', 'help.api.title': 'API Documentation', 'help.api.noTestTokensFound': 'No test tokens found', 'help.api.noTestTokensDescription': 'Create a test API token to see interactive examples with your credentials.', 'help.api.createTestToken': 'Create Test Token', 'help.api.introduction': 'Introduction', 'help.api.authentication': 'Authentication', 'help.api.errors': 'Errors', 'help.api.rateLimits': 'Rate Limits', 'help.api.services': 'Services', 'help.api.resources': 'Resources', 'help.api.availability': 'Availability', 'help.api.appointments': 'Appointments', 'help.api.customers': 'Customers', 'help.api.webhooks': 'Webhooks', 'help.api.filtering': 'Filtering', 'help.api.listServices': 'List all services', 'help.api.retrieveService': 'Retrieve a service', 'help.api.checkAvailability': 'Check availability', 'help.api.createAppointment': 'Create an appointment', 'help.api.retrieveAppointment': 'Retrieve an appointment', 'help.api.updateAppointment': 'Update an appointment', 'help.api.cancelAppointment': 'Cancel an appointment', 'help.api.listAppointments': 'List all appointments', 'help.api.businessObject': 'The business object', 'help.api.serviceObject': 'The service object', 'help.api.resourceObject': 'The resource object', 'help.api.appointmentObject': 'The appointment object', 'help.api.customerObject': 'The customer object', 'help.api.createCustomer': 'Create a customer', 'help.api.retrieveCustomer': 'Retrieve a customer', 'help.api.updateCustomer': 'Update a customer', 'help.api.listCustomers': 'List all customers', }; return translations[key] || key; }, }), })); // Mock useNavigate const mockNavigate = vi.fn(); vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useNavigate: () => mockNavigate, }; }); // Test wrapper with Router const createWrapper = () => { return ({ children }: { children: React.ReactNode }) => ( {children} ); }; describe('HelpApiDocs', () => { beforeEach(() => { vi.clearAllMocks(); // Reset scroll position window.scrollTo = vi.fn(); // Reset the mock to default behavior mockUseTestTokensForDocs.mockReturnValue({ data: mockTestTokensData, isLoading: false, error: null, }); }); describe('Rendering', () => { it('should render the API documentation page', () => { render(, { wrapper: createWrapper() }); const heading = screen.getByText('API Documentation'); expect(heading).toBeInTheDocument(); }); it('should render the back button', () => { render(, { wrapper: createWrapper() }); const backButton = screen.getByRole('button', { name: /back/i }); expect(backButton).toBeInTheDocument(); }); it('should render the page header', () => { render(, { wrapper: createWrapper() }); const title = screen.getByText('API Documentation'); expect(title).toBeInTheDocument(); }); }); describe('Navigation', () => { it('should navigate back when back button is clicked', async () => { const user = userEvent.setup(); render(, { wrapper: createWrapper() }); const backButton = screen.getByRole('button', { name: /back/i }); await user.click(backButton); expect(mockNavigate).toHaveBeenCalledWith(-1); }); }); describe('Main Sections', () => { it('should render introduction section', () => { render(, { wrapper: createWrapper() }); const section = document.getElementById('introduction'); expect(section).toBeInTheDocument(); }); it('should render authentication section', () => { render(, { wrapper: createWrapper() }); const section = document.getElementById('authentication'); expect(section).toBeInTheDocument(); }); it('should render errors section', () => { render(, { wrapper: createWrapper() }); const section = document.getElementById('errors'); expect(section).toBeInTheDocument(); }); it('should render rate limits section', () => { render(, { wrapper: createWrapper() }); const section = document.getElementById('rate-limits'); expect(section).toBeInTheDocument(); }); it('should render services section', () => { render(, { wrapper: createWrapper() }); const section = document.getElementById('list-services'); expect(section).toBeInTheDocument(); }); it('should render appointments section', () => { render(, { wrapper: createWrapper() }); const section = document.getElementById('create-appointment'); expect(section).toBeInTheDocument(); }); it('should render customers section', () => { render(, { wrapper: createWrapper() }); const section = document.getElementById('create-customer'); expect(section).toBeInTheDocument(); }); it('should render webhooks section', () => { render(, { wrapper: createWrapper() }); const section = document.getElementById('webhook-events'); expect(section).toBeInTheDocument(); }); }); describe('Token Selector', () => { it('should render token selector when tokens are available', () => { render(, { wrapper: createWrapper() }); const tokenSelector = screen.getByRole('combobox'); expect(tokenSelector).toBeInTheDocument(); }); it('should display all available test tokens', () => { render(, { wrapper: createWrapper() }); const tokenSelector = screen.getByRole('combobox'); const options = Array.from(tokenSelector.querySelectorAll('option')); expect(options).toHaveLength(2); expect(options[0]).toHaveTextContent('Test Token 1'); expect(options[1]).toHaveTextContent('Test Token 2'); }); it('should show token key prefix in selector', () => { render(, { wrapper: createWrapper() }); const tokenSelector = screen.getByRole('combobox'); expect(tokenSelector).toHaveTextContent('ss_test_abc123'); }); it('should allow selecting a different token', async () => { const user = userEvent.setup(); render(, { wrapper: createWrapper() }); const tokenSelector = screen.getByRole('combobox') as HTMLSelectElement; await user.selectOptions(tokenSelector, 'token-2'); expect(tokenSelector.value).toBe('token-2'); }); it('should display key icon next to token selector', () => { const { container } = render(, { wrapper: createWrapper() }); // Look for the Key icon (lucide-react renders as svg) const keyIcon = container.querySelector('svg'); expect(keyIcon).toBeInTheDocument(); }); }); describe('No Test Tokens Warning', () => { it('should show warning banner when no test tokens exist', async () => { mockUseTestTokensForDocs.mockReturnValue({ data: [], isLoading: false, error: null, }); render(, { wrapper: createWrapper() }); await waitFor(() => { const warning = screen.getByText('No test tokens found'); expect(warning).toBeInTheDocument(); }); }); it('should not show warning banner when tokens are loading', async () => { mockUseTestTokensForDocs.mockReturnValue({ data: undefined, isLoading: true, error: null, }); render(, { wrapper: createWrapper() }); const warning = screen.queryByText('No test tokens found'); expect(warning).not.toBeInTheDocument(); }); it('should not show warning banner when tokens exist', () => { render(, { wrapper: createWrapper() }); const warning = screen.queryByText('No test tokens found'); expect(warning).not.toBeInTheDocument(); }); }); describe('Code Examples', () => { it('should render code blocks for API examples', () => { const { container } = render(, { wrapper: createWrapper() }); // Code blocks typically have
 or  tags
      const codeBlocks = container.querySelectorAll('pre');
      expect(codeBlocks.length).toBeGreaterThan(0);
    });

    it('should display cURL examples by default', () => {
      const { container } = render(, { wrapper: createWrapper() });

      // Look for curl command in code blocks
      const pageText = container.textContent || '';
      expect(pageText).toContain('curl');
    });

    it('should include API token in code examples', async () => {
      const { container } = render(, { wrapper: createWrapper() });

      // Wait for the component to load and use the token
      await waitFor(() => {
        const pageText = container.textContent || '';
        expect(pageText).toContain('ss_test_abc123'); // The first token's prefix
      });
    });

    it('should include sandbox URL in examples', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const pageText = container.textContent || '';
      expect(pageText).toContain('sandbox.smoothschedule.com');
    });

    it('should render language selector tabs', () => {
      const { container } = render(, { wrapper: createWrapper() });

      // Look for language labels (cURL, Python, etc.)
      const pageText = container.textContent || '';
      expect(pageText).toContain('cURL');
    });

    it('should allow switching between code languages', async () => {
      const user = userEvent.setup();
      const { container } = render(, { wrapper: createWrapper() });

      // Find Python language button (if visible)
      // This is a simplified test - actual implementation may vary
      const buttons = container.querySelectorAll('button');
      const pythonButton = Array.from(buttons).find(btn =>
        btn.textContent?.includes('Python')
      );

      if (pythonButton) {
        await user.click(pythonButton);

        // After clicking, should show Python code
        await waitFor(() => {
          const pageText = container.textContent || '';
          expect(pageText).toContain('import requests');
        });
      }
    });
  });

  describe('HTTP Method Badges', () => {
    it('should render GET method badges', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const pageText = container.textContent || '';
      expect(pageText).toContain('GET');
    });

    it('should render POST method badges', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const pageText = container.textContent || '';
      expect(pageText).toContain('POST');
    });

    it('should render PATCH method badges', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const pageText = container.textContent || '';
      expect(pageText).toContain('PATCH');
    });

    it('should render DELETE method badges', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const pageText = container.textContent || '';
      expect(pageText).toContain('DELETE');
    });
  });

  describe('API Endpoints', () => {
    it('should document the list services endpoint', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const section = document.getElementById('list-services');
      expect(section).toBeInTheDocument();
    });

    it('should document the create appointment endpoint', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const section = document.getElementById('create-appointment');
      expect(section).toBeInTheDocument();
    });

    it('should document the check availability endpoint', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const section = document.getElementById('check-availability');
      expect(section).toBeInTheDocument();
    });

    it('should document customer endpoints', () => {
      render(, { wrapper: createWrapper() });

      expect(document.getElementById('create-customer')).toBeInTheDocument();
      expect(document.getElementById('retrieve-customer')).toBeInTheDocument();
      expect(document.getElementById('update-customer')).toBeInTheDocument();
      expect(document.getElementById('list-customers')).toBeInTheDocument();
    });

    it('should document webhook endpoints', () => {
      render(, { wrapper: createWrapper() });

      expect(document.getElementById('webhook-events')).toBeInTheDocument();
      expect(document.getElementById('create-webhook')).toBeInTheDocument();
      expect(document.getElementById('list-webhooks')).toBeInTheDocument();
    });
  });

  describe('Page Structure', () => {
    it('should have a sticky header', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const header = container.querySelector('header');
      expect(header).toHaveClass('sticky');
    });

    it('should render main content area', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const mainContent = container.querySelector('.min-h-screen');
      expect(mainContent).toBeInTheDocument();
    });

    it('should apply dark mode classes', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const mainDiv = container.querySelector('.dark\\:bg-gray-900');
      expect(mainDiv).toBeInTheDocument();
    });
  });

  describe('Accessibility', () => {
    it('should have semantic header element', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const header = container.querySelector('header');
      expect(header).toBeInTheDocument();
    });

    it('should have accessible back button', () => {
      render(, { wrapper: createWrapper() });

      const backButton = screen.getByRole('button', { name: /back/i });
      expect(backButton).toHaveAccessibleName();
    });

    it('should have accessible token selector', async () => {
      render(, { wrapper: createWrapper() });

      // Wait for the selector to be rendered
      await waitFor(() => {
        const selector = screen.getByRole('combobox');
        expect(selector).toBeInTheDocument();
      });
    });

    it('should use section elements for content sections', () => {
      const { container } = render(, { wrapper: createWrapper() });

      const sections = container.querySelectorAll('section');
      expect(sections.length).toBeGreaterThan(0);
    });
  });

  describe('Internationalization', () => {
    it('should translate page title', () => {
      render(, { wrapper: createWrapper() });

      const title = screen.getByText('API Documentation');
      expect(title).toBeInTheDocument();
    });

    it('should translate back button text', () => {
      render(, { wrapper: createWrapper() });

      const backButton = screen.getByRole('button', { name: /back/i });
      expect(backButton).toHaveTextContent('Back');
    });

    it('should translate no tokens warning', async () => {
      const { useTestTokensForDocs } = await import('../../hooks/useApiTokens');
      vi.mocked(useTestTokensForDocs).mockReturnValue({
        data: [],
        isLoading: false,
        error: null,
      } as any);

      render(, { wrapper: createWrapper() });

      await waitFor(() => {
        expect(screen.getByText('No test tokens found')).toBeInTheDocument();
      });
    });
  });

  describe('Integration', () => {
    it('should render complete API documentation page', async () => {
      render(, { wrapper: createWrapper() });

      // Check for key elements
      expect(screen.getByText('API Documentation')).toBeInTheDocument();
      expect(screen.getByRole('button', { name: /back/i })).toBeInTheDocument();

      // Wait for token selector to render
      await waitFor(() => {
        expect(screen.getByRole('combobox')).toBeInTheDocument();
      });

      expect(document.getElementById('introduction')).toBeInTheDocument();
      expect(document.getElementById('authentication')).toBeInTheDocument();
    });

    it('should handle token selection and update examples', async () => {
      const user = userEvent.setup();
      const { container } = render(, { wrapper: createWrapper() });

      // Wait for token selector to render
      const tokenSelector = await waitFor(() => {
        return screen.getByRole('combobox') as HTMLSelectElement;
      });

      // Wait for initial token to appear
      await waitFor(() => {
        expect(container.textContent).toContain('ss_test_abc123');
      });

      // Select second token
      await user.selectOptions(tokenSelector, 'token-2');

      // Should now show second token in examples
      await waitFor(() => {
        expect(container.textContent).toContain('ss_test_def456');
      });
    });

    it('should maintain structure with all sections present', () => {
      render(, { wrapper: createWrapper() });

      // Verify all main sections exist
      const sections = [
        'introduction',
        'authentication',
        'errors',
        'rate-limits',
        'list-services',
        'check-availability',
        'create-appointment',
        'create-customer',
        'webhook-events',
      ];

      sections.forEach(sectionId => {
        const section = document.getElementById(sectionId);
        expect(section).toBeInTheDocument();
      });
    });
  });

  describe('Error Handling', () => {
    it('should handle missing token data gracefully', async () => {
      const { useTestTokensForDocs } = await import('../../hooks/useApiTokens');
      vi.mocked(useTestTokensForDocs).mockReturnValue({
        data: undefined,
        isLoading: false,
        error: null,
      } as any);

      render(, { wrapper: createWrapper() });

      // Should still render the page
      expect(screen.getByText('API Documentation')).toBeInTheDocument();
    });

    it('should handle token loading state', async () => {
      const { useTestTokensForDocs } = await import('../../hooks/useApiTokens');
      vi.mocked(useTestTokensForDocs).mockReturnValue({
        data: undefined,
        isLoading: true,
        error: null,
      } as any);

      render(, { wrapper: createWrapper() });

      // Should render without token selector
      expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
    });

    it('should use default API key when no tokens available', async () => {
      const { useTestTokensForDocs } = await import('../../hooks/useApiTokens');
      vi.mocked(useTestTokensForDocs).mockReturnValue({
        data: [],
        isLoading: false,
        error: null,
      } as any);

      const { container } = render(, { wrapper: createWrapper() });

      // Should show default placeholder token
      await waitFor(() => {
        const pageText = container.textContent || '';
        expect(pageText).toContain('ss_test_');
      });
    });
  });
});