Files
smoothschedule/frontend/src/pages/__tests__/ContractSigning.test.tsx
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

398 lines
11 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import React from 'react';
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: any) => {
if (typeof options === 'object' && options.customerName) {
return `Contract for ${options.customerName}`;
}
return key;
},
}),
}));
// Mock hooks
const mockUsePublicContract = vi.fn();
const mockUseSignContract = vi.fn();
vi.mock('../../hooks/useContracts', () => ({
usePublicContract: (token: string) => mockUsePublicContract(token),
useSignContract: () => mockUseSignContract(),
}));
import ContractSigning from '../ContractSigning';
describe('ContractSigning', () => {
const mockContractData = {
contract: {
id: '123',
content: '<h1>Contract Title</h1><p>Contract terms and conditions...</p>',
status: 'PENDING',
},
template: {
name: 'Service Agreement',
},
business: {
id: '1',
name: 'Test Business',
logo_url: 'https://example.com/logo.png',
},
customer: {
name: 'John Doe',
email: 'john@example.com',
},
can_sign: true,
is_expired: false,
};
const mockSignature = {
signer_name: 'John Doe',
signer_email: 'john@example.com',
signed_at: '2024-01-15T10:30:00Z',
};
beforeEach(() => {
vi.clearAllMocks();
mockUsePublicContract.mockReturnValue({
data: mockContractData,
isLoading: false,
error: null,
refetch: vi.fn(),
});
mockUseSignContract.mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
isSuccess: false,
isError: false,
});
});
const renderComponent = (token = 'test-token') => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`/contracts/sign/${token}`]}>
<Routes>
<Route path="/contracts/sign/:token" element={<ContractSigning />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
);
};
it('renders loading state', () => {
mockUsePublicContract.mockReturnValue({
data: null,
isLoading: true,
error: null,
refetch: vi.fn(),
});
renderComponent();
expect(screen.getByText('common.loading')).toBeInTheDocument();
const loader = document.querySelector('.animate-spin');
expect(loader).toBeInTheDocument();
});
it('renders contract not found error', () => {
mockUsePublicContract.mockReturnValue({
data: null,
isLoading: false,
error: new Error('Not found'),
refetch: vi.fn(),
});
renderComponent();
expect(screen.getByText('contracts.signing.notFound')).toBeInTheDocument();
expect(screen.getByText(/invalid or has expired/i)).toBeInTheDocument();
});
it('renders expired contract message', () => {
mockUsePublicContract.mockReturnValue({
data: { ...mockContractData, is_expired: true },
isLoading: false,
error: null,
refetch: vi.fn(),
});
renderComponent();
expect(screen.getByText('contracts.signing.expired')).toBeInTheDocument();
expect(screen.getByText(/can no longer be signed/i)).toBeInTheDocument();
});
it('renders contract signing form', () => {
renderComponent();
expect(screen.getByText('Test Business')).toBeInTheDocument();
expect(screen.getByText('Service Agreement')).toBeInTheDocument();
expect(screen.getByText('Contract for John Doe')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Enter your full name')).toBeInTheDocument();
});
it('renders contract content', () => {
renderComponent();
const contractContent = document.querySelector('.prose');
expect(contractContent).toBeInTheDocument();
expect(contractContent?.innerHTML).toContain('Contract Title');
expect(contractContent?.innerHTML).toContain('Contract terms and conditions');
});
it('displays business logo when available', () => {
renderComponent();
const logo = screen.getByAltText('Test Business');
expect(logo).toBeInTheDocument();
expect(logo).toHaveAttribute('src', 'https://example.com/logo.png');
});
it('shows signature input and preview', async () => {
const user = userEvent.setup();
renderComponent();
const nameInput = screen.getByPlaceholderText('Enter your full name');
await user.type(nameInput, 'John Doe');
expect(nameInput).toHaveValue('John Doe');
// Check for signature preview
await waitFor(() => {
expect(screen.getByText('Signature Preview:')).toBeInTheDocument();
});
// Preview should show the typed name in cursive
const preview = document.querySelector('[style*="cursive"]');
expect(preview).toBeInTheDocument();
expect(preview?.textContent).toBe('John Doe');
});
it('requires all consent checkboxes', async () => {
const user = userEvent.setup();
renderComponent();
const nameInput = screen.getByPlaceholderText('Enter your full name');
await user.type(nameInput, 'John Doe');
const signButton = screen.getByRole('button', { name: /sign contract/i });
// Button should be disabled without consent
expect(signButton).toBeDisabled();
// Check first checkbox
const checkbox1 = screen.getByLabelText(/have read and agree/i);
await user.click(checkbox1);
// Still disabled without second checkbox
expect(signButton).toBeDisabled();
// Check second checkbox
const checkbox2 = screen.getByLabelText(/consent to conduct business electronically/i);
await user.click(checkbox2);
// Now should be enabled
expect(signButton).toBeEnabled();
});
it('submits signature when all fields are valid', async () => {
const user = userEvent.setup();
const mockMutate = vi.fn().mockResolvedValue({});
mockUseSignContract.mockReturnValue({
mutateAsync: mockMutate,
isPending: false,
isSuccess: false,
isError: false,
});
renderComponent();
// Fill name
const nameInput = screen.getByPlaceholderText('Enter your full name');
await user.type(nameInput, 'John Doe');
// Check both checkboxes
const checkbox1 = screen.getByLabelText(/have read and agree/i);
const checkbox2 = screen.getByLabelText(/consent to conduct business electronically/i);
await user.click(checkbox1);
await user.click(checkbox2);
// Submit
const signButton = screen.getByRole('button', { name: /sign contract/i });
await user.click(signButton);
await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith({
token: 'test-token',
signer_name: 'John Doe',
consent_checkbox_checked: true,
electronic_consent_given: true,
});
});
});
it('shows loading state while signing', async () => {
const user = userEvent.setup();
mockUseSignContract.mockReturnValue({
mutateAsync: vi.fn().mockImplementation(() => new Promise(() => {})),
isPending: true,
isSuccess: false,
isError: false,
});
renderComponent();
expect(screen.getByRole('button', { name: /signing/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /signing/i })).toBeDisabled();
});
it('shows error message when signing fails', async () => {
const user = userEvent.setup();
mockUseSignContract.mockReturnValue({
mutateAsync: vi.fn().mockRejectedValue(new Error('Signing failed')),
isPending: false,
isSuccess: false,
isError: true,
});
renderComponent();
await waitFor(() => {
expect(screen.getByText(/failed to sign the contract/i)).toBeInTheDocument();
});
});
it('renders signed contract view after successful signing', () => {
mockUsePublicContract.mockReturnValue({
data: {
...mockContractData,
contract: { ...mockContractData.contract, status: 'SIGNED' },
signature: mockSignature,
},
isLoading: false,
error: null,
refetch: vi.fn(),
});
renderComponent();
expect(screen.getByText(/contract successfully signed/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /print contract/i })).toBeInTheDocument();
});
it('displays signature details in signed view', () => {
mockUsePublicContract.mockReturnValue({
data: {
...mockContractData,
contract: { ...mockContractData.contract, status: 'SIGNED' },
signature: mockSignature,
},
isLoading: false,
error: null,
refetch: vi.fn(),
});
renderComponent();
// Use getAllByText since "John Doe" and "john@example.com" appear multiple times
expect(screen.getAllByText('John Doe').length).toBeGreaterThan(0);
expect(screen.getAllByText('john@example.com').length).toBeGreaterThan(0);
// Check for "Signed" status badge
const signedBadges = screen.queryAllByText(/^signed$/i);
expect(signedBadges.length).toBeGreaterThan(0);
});
it('handles print button click', async () => {
const user = userEvent.setup();
const mockPrint = vi.fn();
window.print = mockPrint;
mockUsePublicContract.mockReturnValue({
data: {
...mockContractData,
contract: { ...mockContractData.contract, status: 'SIGNED' },
signature: mockSignature,
},
isLoading: false,
error: null,
refetch: vi.fn(),
});
renderComponent();
const printButton = screen.getByRole('button', { name: /print contract/i });
await user.click(printButton);
expect(mockPrint).toHaveBeenCalled();
});
it('shows legal compliance notice in signing form', () => {
renderComponent();
expect(screen.getByText(/ESIGN Act/i)).toBeInTheDocument();
expect(screen.getByText(/UETA/i)).toBeInTheDocument();
});
it('shows electronic consent disclosure', () => {
renderComponent();
expect(screen.getByText(/conduct business electronically/i)).toBeInTheDocument();
expect(screen.getByText(/right to receive documents in paper form/i)).toBeInTheDocument();
});
it('prevents signing when cannot sign', () => {
mockUsePublicContract.mockReturnValue({
data: {
...mockContractData,
can_sign: false,
},
isLoading: false,
error: null,
refetch: vi.fn(),
});
renderComponent();
// Should not show the signing form
expect(screen.queryByPlaceholderText('Enter your full name')).not.toBeInTheDocument();
});
it('validates name is not empty before enabling submit', async () => {
const user = userEvent.setup();
renderComponent();
const checkbox1 = screen.getByLabelText(/have read and agree/i);
const checkbox2 = screen.getByLabelText(/consent to conduct business electronically/i);
await user.click(checkbox1);
await user.click(checkbox2);
const signButton = screen.getByRole('button', { name: /sign contract/i });
// Should be disabled with empty name
expect(signButton).toBeDisabled();
// Type name
const nameInput = screen.getByPlaceholderText('Enter your full name');
await user.type(nameInput, 'John Doe');
// Should be enabled now
expect(signButton).toBeEnabled();
});
});