- Add comprehensive TDD documentation to CLAUDE.md with coverage requirements and examples - Extract reusable UI components to frontend/src/components/ui/ (Modal, FormInput, Button, Alert, etc.) - Add shared constants (schedulePresets) and utility hooks (useCrudMutation, useFormValidation) - Update frontend/CLAUDE.md with component documentation and usage examples - Refactor CreateTaskModal to use shared components and constants - Fix test assertions to be more robust and accurate across all test files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
922 lines
26 KiB
TypeScript
922 lines
26 KiB
TypeScript
/**
|
|
* Unit tests for Sidebar component
|
|
*
|
|
* Tests cover:
|
|
* - Component rendering
|
|
* - Resources list display
|
|
* - Pending appointments list
|
|
* - Empty state handling
|
|
* - Drag source setup with @dnd-kit
|
|
* - Scrolling reference setup
|
|
* - Multi-lane resource badges
|
|
* - Archive drop zone display
|
|
* - Internationalization (i18n)
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
|
|
import { render, screen, within } from '@testing-library/react';
|
|
import { DndContext } from '@dnd-kit/core';
|
|
import React from 'react';
|
|
import Sidebar, { PendingAppointment, ResourceLayout } from '../Sidebar';
|
|
|
|
// Setup proper mocks for @dnd-kit
|
|
beforeAll(() => {
|
|
// Mock IntersectionObserver properly as a constructor
|
|
class IntersectionObserverMock {
|
|
observe = vi.fn();
|
|
unobserve = vi.fn();
|
|
disconnect = vi.fn();
|
|
constructor() {
|
|
return this;
|
|
}
|
|
}
|
|
global.IntersectionObserver = IntersectionObserverMock as any;
|
|
|
|
// Mock ResizeObserver properly as a constructor
|
|
class ResizeObserverMock {
|
|
observe = vi.fn();
|
|
unobserve = vi.fn();
|
|
disconnect = vi.fn();
|
|
constructor() {
|
|
return this;
|
|
}
|
|
}
|
|
global.ResizeObserver = ResizeObserverMock as any;
|
|
});
|
|
|
|
// Mock react-i18next
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => {
|
|
const translations: Record<string, string> = {
|
|
'scheduler.resources': 'Resources',
|
|
'scheduler.resource': 'Resource',
|
|
'scheduler.lanes': 'lanes',
|
|
'scheduler.pendingRequests': 'Pending Requests',
|
|
'scheduler.noPendingRequests': 'No pending requests',
|
|
'scheduler.dropToArchive': 'Drop here to archive',
|
|
'scheduler.min': 'min',
|
|
};
|
|
return translations[key] || key;
|
|
},
|
|
}),
|
|
}));
|
|
|
|
// Helper function to create a wrapper with DndContext
|
|
const createDndWrapper = () => {
|
|
return ({ children }: { children: React.ReactNode }) => (
|
|
<DndContext>{children}</DndContext>
|
|
);
|
|
};
|
|
|
|
describe('Sidebar', () => {
|
|
const mockScrollRef = { current: null } as React.RefObject<HTMLDivElement>;
|
|
|
|
const mockResourceLayouts: ResourceLayout[] = [
|
|
{
|
|
resourceId: 1,
|
|
resourceName: 'Dr. Smith',
|
|
height: 100,
|
|
laneCount: 1,
|
|
},
|
|
{
|
|
resourceId: 2,
|
|
resourceName: 'Conference Room A',
|
|
height: 120,
|
|
laneCount: 2,
|
|
},
|
|
{
|
|
resourceId: 3,
|
|
resourceName: 'Equipment Bay',
|
|
height: 100,
|
|
laneCount: 3,
|
|
},
|
|
];
|
|
|
|
const mockPendingAppointments: PendingAppointment[] = [
|
|
{
|
|
id: 1,
|
|
customerName: 'John Doe',
|
|
serviceName: 'Consultation',
|
|
durationMinutes: 30,
|
|
},
|
|
{
|
|
id: 2,
|
|
customerName: 'Jane Smith',
|
|
serviceName: 'Follow-up',
|
|
durationMinutes: 15,
|
|
},
|
|
{
|
|
id: 3,
|
|
customerName: 'Bob Johnson',
|
|
serviceName: 'Initial Assessment',
|
|
durationMinutes: 60,
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render the sidebar container', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const sidebar = container.firstChild as HTMLElement;
|
|
expect(sidebar).toBeInTheDocument();
|
|
expect(sidebar).toHaveClass('flex', 'flex-col', 'bg-white');
|
|
});
|
|
|
|
it('should render with fixed width of 250px', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const sidebar = container.firstChild as HTMLElement;
|
|
expect(sidebar).toHaveStyle({ width: '250px' });
|
|
});
|
|
|
|
it('should render resources header', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const header = screen.getByText('Resources');
|
|
expect(header).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render pending requests section', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const pendingHeader = screen.getByText(/Pending Requests/);
|
|
expect(pendingHeader).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render archive drop zone', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const dropZone = screen.getByText('Drop here to archive');
|
|
expect(dropZone).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Resources List', () => {
|
|
it('should render all resources from resourceLayouts', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('Dr. Smith')).toBeInTheDocument();
|
|
expect(screen.getByText('Conference Room A')).toBeInTheDocument();
|
|
expect(screen.getByText('Equipment Bay')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should apply correct height to each resource row', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// The height style is on the resource row container (3 levels up from the text)
|
|
const drSmith = screen.getByText('Dr. Smith').closest('[style*="height"]');
|
|
const confRoom = screen.getByText('Conference Room A').closest('[style*="height"]');
|
|
|
|
expect(drSmith).toHaveStyle({ height: '100px' });
|
|
expect(confRoom).toHaveStyle({ height: '120px' });
|
|
});
|
|
|
|
it('should display "Resource" label for each resource', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const resourceLabels = screen.getAllByText('Resource');
|
|
expect(resourceLabels.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should render grip icons for resources', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const gripIcons = container.querySelectorAll('svg');
|
|
expect(gripIcons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should not render lane count badge for single-lane resources', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[mockResourceLayouts[0]]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.queryByText(/lanes/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should render lane count badge for multi-lane resources', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[mockResourceLayouts[1]]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('2 lanes')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render all multi-lane badges correctly', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('2 lanes')).toBeInTheDocument();
|
|
expect(screen.getByText('3 lanes')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should apply correct styling to multi-lane badges', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[mockResourceLayouts[1]]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const badge = screen.getByText('2 lanes');
|
|
expect(badge).toHaveClass('text-blue-600', 'bg-blue-50');
|
|
});
|
|
|
|
it('should attach scroll ref to resource list container', () => {
|
|
const testRef = React.createRef<HTMLDivElement>();
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={[]}
|
|
scrollRef={testRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(testRef.current).toBeInstanceOf(HTMLDivElement);
|
|
});
|
|
|
|
it('should render empty resources list when no resources provided', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.queryByText('Dr. Smith')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Pending Appointments List', () => {
|
|
it('should render all pending appointments', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
|
|
expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should display customer names correctly', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
mockPendingAppointments.forEach((apt) => {
|
|
expect(screen.getByText(apt.customerName)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should display service names correctly', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('Consultation')).toBeInTheDocument();
|
|
expect(screen.getByText('Follow-up')).toBeInTheDocument();
|
|
expect(screen.getByText('Initial Assessment')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should display duration in minutes for each appointment', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('30 min')).toBeInTheDocument();
|
|
expect(screen.getByText('15 min')).toBeInTheDocument();
|
|
expect(screen.getByText('60 min')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should display clock icon for each appointment', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// Clock icons are SVGs
|
|
const clockIcons = container.querySelectorAll('svg');
|
|
expect(clockIcons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should display grip vertical icon for drag handle', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[mockPendingAppointments[0]]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// Navigate up to the draggable container which has the svg
|
|
const appointment = screen.getByText('John Doe').closest('.cursor-grab');
|
|
const svg = appointment?.querySelector('svg');
|
|
expect(svg).toBeInTheDocument();
|
|
});
|
|
|
|
it('should show appointment count in header', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText(/Pending Requests \(3\)/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should update count when appointments change', () => {
|
|
const { rerender } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText(/Pending Requests \(3\)/)).toBeInTheDocument();
|
|
|
|
rerender(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[mockPendingAppointments[0]]}
|
|
scrollRef={mockScrollRef}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText(/Pending Requests \(1\)/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Empty State', () => {
|
|
it('should display empty message when no pending appointments', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('No pending requests')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should show count of 0 in header when empty', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText(/Pending Requests \(0\)/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should apply italic styling to empty message', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const emptyMessage = screen.getByText('No pending requests');
|
|
expect(emptyMessage).toHaveClass('italic');
|
|
});
|
|
|
|
it('should not render appointment items when empty', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Drag and Drop Setup', () => {
|
|
it('should setup draggable for each pending appointment', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// Each appointment should have drag cursor classes
|
|
const appointments = container.querySelectorAll('[class*="cursor-grab"]');
|
|
expect(appointments.length).toBe(mockPendingAppointments.length);
|
|
});
|
|
|
|
it('should apply cursor-grab class to draggable items', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[mockPendingAppointments[0]]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// Use the specific class selector since .closest('div') returns the inner div
|
|
const appointmentCard = screen.getByText('John Doe').closest('.cursor-grab');
|
|
expect(appointmentCard).toBeInTheDocument();
|
|
});
|
|
|
|
it('should apply active cursor-grabbing class to draggable items', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[mockPendingAppointments[0]]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// Verify the draggable container has the active:cursor-grabbing class
|
|
const appointmentCard = screen.getByText('John Doe').closest('[class*="active:cursor-grabbing"]');
|
|
expect(appointmentCard).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render pending items with orange left border', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[mockPendingAppointments[0]]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// Use the specific class selector
|
|
const appointmentCard = screen.getByText('John Doe').closest('.border-l-orange-400');
|
|
expect(appointmentCard).toBeInTheDocument();
|
|
});
|
|
|
|
it('should apply shadow on hover for draggable items', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[mockPendingAppointments[0]]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// Use the specific class selector
|
|
const appointmentCard = screen.getByText('John Doe').closest('[class*="hover:shadow-md"]');
|
|
expect(appointmentCard).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Archive Drop Zone', () => {
|
|
it('should render drop zone with trash icon', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const dropZone = screen.getByText('Drop here to archive').parentElement;
|
|
const trashIcon = dropZone?.querySelector('svg');
|
|
expect(trashIcon).toBeInTheDocument();
|
|
});
|
|
|
|
it('should apply dashed border to drop zone', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const dropZone = screen.getByText('Drop here to archive').parentElement;
|
|
expect(dropZone).toHaveClass('border-dashed');
|
|
});
|
|
|
|
it('should apply opacity to drop zone container', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const dropZoneContainer = screen
|
|
.getByText('Drop here to archive')
|
|
.closest('.opacity-50');
|
|
expect(dropZoneContainer).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Layout and Styling', () => {
|
|
it('should apply fixed height to resources header', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// The height style is on the header div itself
|
|
const header = screen.getByText('Resources').closest('[style*="height"]');
|
|
expect(header).toHaveStyle({ height: '48px' });
|
|
});
|
|
|
|
it('should apply fixed height to pending requests section', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const pendingSection = screen
|
|
.getByText(/Pending Requests/)
|
|
.closest('.h-80');
|
|
expect(pendingSection).toBeInTheDocument();
|
|
});
|
|
|
|
it('should have overflow-hidden on resource list', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const resourceList = container.querySelector('.overflow-hidden');
|
|
expect(resourceList).toBeInTheDocument();
|
|
});
|
|
|
|
it('should have overflow-y-auto on pending appointments list', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const pendingList = container.querySelector('.overflow-y-auto');
|
|
expect(pendingList).toBeInTheDocument();
|
|
});
|
|
|
|
it('should apply border-right to sidebar', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const sidebar = container.firstChild as HTMLElement;
|
|
expect(sidebar).toHaveClass('border-r');
|
|
});
|
|
|
|
it('should apply shadow to sidebar', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const sidebar = container.firstChild as HTMLElement;
|
|
expect(sidebar).toHaveClass('shadow-lg');
|
|
});
|
|
|
|
it('should have dark mode classes', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const sidebar = container.firstChild as HTMLElement;
|
|
expect(sidebar).toHaveClass('dark:bg-gray-800');
|
|
expect(sidebar).toHaveClass('dark:border-gray-700');
|
|
});
|
|
});
|
|
|
|
describe('Internationalization', () => {
|
|
it('should use translation for resources header', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('Resources')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should use translation for pending requests header', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText(/Pending Requests/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should use translation for empty state message', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('No pending requests')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should use translation for drop zone text', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('Drop here to archive')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should use translation for duration units', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={[mockPendingAppointments[0]]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('30 min')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should use translation for resource label', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[mockResourceLayouts[0]]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('Resource')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should use translation for lanes label', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[mockResourceLayouts[1]]}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('2 lanes')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Integration', () => {
|
|
it('should render correctly with all props together', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
// Verify resources
|
|
expect(screen.getByText('Dr. Smith')).toBeInTheDocument();
|
|
expect(screen.getByText('Conference Room A')).toBeInTheDocument();
|
|
expect(screen.getByText('Equipment Bay')).toBeInTheDocument();
|
|
|
|
// Verify pending appointments
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
|
|
expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
|
|
|
|
// Verify count
|
|
expect(screen.getByText(/Pending Requests \(3\)/)).toBeInTheDocument();
|
|
|
|
// Verify archive drop zone
|
|
expect(screen.getByText('Drop here to archive')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should handle empty resources with full pending appointments', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={[]}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.queryByText('Dr. Smith')).not.toBeInTheDocument();
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
expect(screen.getByText(/Pending Requests \(3\)/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should handle full resources with empty pending appointments', () => {
|
|
render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={[]}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
expect(screen.getByText('Dr. Smith')).toBeInTheDocument();
|
|
expect(screen.getByText('No pending requests')).toBeInTheDocument();
|
|
expect(screen.getByText(/Pending Requests \(0\)/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should maintain structure with resources and pending sections', () => {
|
|
const { container } = render(
|
|
<Sidebar
|
|
resourceLayouts={mockResourceLayouts}
|
|
pendingAppointments={mockPendingAppointments}
|
|
scrollRef={mockScrollRef}
|
|
/>,
|
|
{ wrapper: createDndWrapper() }
|
|
);
|
|
|
|
const sidebar = container.firstChild as HTMLElement;
|
|
|
|
// Should have header, resources list, and pending section
|
|
const sections = sidebar.querySelectorAll(
|
|
'.border-b, .border-t, .flex-col'
|
|
);
|
|
expect(sections.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|