Files
smoothschedule/frontend/src/pages/__tests__/HelpApiDocs.test.tsx.tmp.770091.1766728081370
poduck 47657e7076 Add staff permission controls for editing staff and customers
- Add can_edit_staff and can_edit_customers dangerous permissions
- Move Site Builder, Services, Locations, Time Blocks, Payments to Settings permissions
- Link Edit Others' Schedules and Edit Own Schedule permissions
- Add permission checks to StaffViewSet (partial_update, toggle_active, verify_email)
- Add permission checks to CustomerViewSet (update, partial_update, verify_email)
- Fix CustomerViewSet permission key mismatch (can_access_customers)
- Hide Edit/Verify buttons on Staff and Customers pages without permission
- Make dangerous permissions section more visually distinct (darker red)
- Fix StaffDashboard links to use correct paths (/dashboard/my-schedule)
- Disable settings sub-permissions when Access Settings is unchecked

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 17:38:48 -05:00

631 lines
21 KiB
Plaintext

/**
* 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<string, string> = {
'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 }) => (
<BrowserRouter>{children}</BrowserRouter>
);
};
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(<HelpApiDocs />, { wrapper: createWrapper() });
const heading = screen.getByText('API Documentation');
expect(heading).toBeInTheDocument();
});
it('should render the back button', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const backButton = screen.getByRole('button', { name: /back/i });
expect(backButton).toBeInTheDocument();
});
it('should render the page header', () => {
render(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('introduction');
expect(section).toBeInTheDocument();
});
it('should render authentication section', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('authentication');
expect(section).toBeInTheDocument();
});
it('should render errors section', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('errors');
expect(section).toBeInTheDocument();
});
it('should render rate limits section', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('rate-limits');
expect(section).toBeInTheDocument();
});
it('should render services section', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('list-services');
expect(section).toBeInTheDocument();
});
it('should render appointments section', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('create-appointment');
expect(section).toBeInTheDocument();
});
it('should render customers section', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('create-customer');
expect(section).toBeInTheDocument();
});
it('should render webhooks section', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('webhook-events');
expect(section).toBeInTheDocument();
});
});
describe('Token Selector', () => {
it('should render token selector when tokens are available', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const tokenSelector = screen.getByRole('combobox');
expect(tokenSelector).toBeInTheDocument();
});
it('should display all available test tokens', () => {
render(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
const warning = screen.queryByText('No test tokens found');
expect(warning).not.toBeInTheDocument();
});
it('should not show warning banner when tokens exist', () => {
render(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
// Code blocks typically have <pre> or <code> tags
const codeBlocks = container.querySelectorAll('pre');
expect(codeBlocks.length).toBeGreaterThan(0);
});
it('should display cURL examples by default', () => {
const { container } = render(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
const pageText = container.textContent || '';
expect(pageText).toContain('sandbox.smoothschedule.com');
});
it('should render language selector tabs', () => {
const { container } = render(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
const pageText = container.textContent || '';
expect(pageText).toContain('GET');
});
it('should render POST method badges', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const pageText = container.textContent || '';
expect(pageText).toContain('POST');
});
it('should render PATCH method badges', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const pageText = container.textContent || '';
expect(pageText).toContain('PATCH');
});
it('should render DELETE method badges', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const pageText = container.textContent || '';
expect(pageText).toContain('DELETE');
});
});
describe('API Endpoints', () => {
it('should document the list services endpoint', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('list-services');
expect(section).toBeInTheDocument();
});
it('should document the create appointment endpoint', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('create-appointment');
expect(section).toBeInTheDocument();
});
it('should document the check availability endpoint', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const section = document.getElementById('check-availability');
expect(section).toBeInTheDocument();
});
it('should document customer endpoints', () => {
render(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
const header = container.querySelector('header');
expect(header).toHaveClass('sticky');
});
it('should render main content area', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const mainContent = container.querySelector('.min-h-screen');
expect(mainContent).toBeInTheDocument();
});
it('should apply dark mode classes', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const mainDiv = container.querySelector('.dark\\:bg-gray-900');
expect(mainDiv).toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('should have semantic header element', () => {
const { container } = render(<HelpApiDocs />, { wrapper: createWrapper() });
const header = container.querySelector('header');
expect(header).toBeInTheDocument();
});
it('should have accessible back button', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const backButton = screen.getByRole('button', { name: /back/i });
expect(backButton).toHaveAccessibleName();
});
it('should have accessible token selector', async () => {
render(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
const sections = container.querySelectorAll('section');
expect(sections.length).toBeGreaterThan(0);
});
});
describe('Internationalization', () => {
it('should translate page title', () => {
render(<HelpApiDocs />, { wrapper: createWrapper() });
const title = screen.getByText('API Documentation');
expect(title).toBeInTheDocument();
});
it('should translate back button text', () => {
render(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText('No test tokens found')).toBeInTheDocument();
});
});
});
describe('Integration', () => {
it('should render complete API documentation page', async () => {
render(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { 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(<HelpApiDocs />, { wrapper: createWrapper() });
// Should show default placeholder token
await waitFor(() => {
const pageText = container.textContent || '';
expect(pageText).toContain('ss_test_');
});
});
});
});