Files
smoothschedule/frontend/src/components/Timeline/__tests__/DraggableEvent.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

229 lines
7.2 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { DraggableEvent } from '../DraggableEvent';
// Mock DnD Kit
vi.mock('@dnd-kit/core', () => ({
DndContext: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
useDraggable: vi.fn(() => ({
attributes: {},
listeners: {},
setNodeRef: vi.fn(),
transform: null,
isDragging: false,
})),
}));
vi.mock('@dnd-kit/utilities', () => ({
CSS: {
Translate: {
toString: (transform: any) => transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
},
},
}));
describe('DraggableEvent', () => {
const defaultProps = {
id: 1,
title: 'Test Event',
serviceName: 'Test Service',
status: 'CONFIRMED' as const,
isPaid: false,
start: new Date('2024-01-01T10:00:00'),
end: new Date('2024-01-01T11:00:00'),
laneIndex: 0,
height: 80,
left: 100,
width: 200,
top: 10,
onResizeStart: vi.fn(),
};
it('renders the event title', () => {
render(<DraggableEvent {...defaultProps} />);
expect(screen.getByText('Test Event')).toBeInTheDocument();
});
it('renders the service name when provided', () => {
render(<DraggableEvent {...defaultProps} />);
expect(screen.getByText('Test Service')).toBeInTheDocument();
});
it('does not render service name when not provided', () => {
render(<DraggableEvent {...defaultProps} serviceName={undefined} />);
expect(screen.queryByText('Test Service')).not.toBeInTheDocument();
});
it('displays the start time formatted correctly', () => {
render(<DraggableEvent {...defaultProps} />);
expect(screen.getByText('10:00 AM')).toBeInTheDocument();
});
it('applies correct position styles', () => {
const { container } = render(<DraggableEvent {...defaultProps} />);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveStyle({
left: '100px',
width: '200px',
top: '10px',
height: '80px',
});
});
it('applies confirmed status border color', () => {
const { container } = render(
<DraggableEvent {...defaultProps} status="CONFIRMED" />
);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveClass('border-blue-500');
});
it('applies completed status border color', () => {
const { container } = render(
<DraggableEvent {...defaultProps} status="COMPLETED" />
);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveClass('border-green-500');
});
it('applies cancelled status border color', () => {
const { container } = render(
<DraggableEvent {...defaultProps} status="CANCELLED" />
);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveClass('border-red-500');
});
it('applies no-show status border color', () => {
const { container } = render(
<DraggableEvent {...defaultProps} status="NO_SHOW" />
);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveClass('border-gray-500');
});
it('applies green border when paid', () => {
const { container } = render(
<DraggableEvent {...defaultProps} isPaid={true} />
);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveClass('border-green-500');
});
it('applies default brand border color for scheduled status', () => {
const { container } = render(
<DraggableEvent {...defaultProps} status="SCHEDULED" />
);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveClass('border-brand-500');
});
it('calls onResizeStart when top resize handle is clicked', () => {
const onResizeStart = vi.fn();
const { container } = render(
<DraggableEvent {...defaultProps} onResizeStart={onResizeStart} />
);
const topHandle = container.querySelector('.cursor-ns-resize');
if (topHandle) {
fireEvent.mouseDown(topHandle);
expect(onResizeStart).toHaveBeenCalledWith(
expect.any(Object),
'left',
1
);
}
});
it('calls onResizeStart when bottom resize handle is clicked', () => {
const onResizeStart = vi.fn();
const { container } = render(
<DraggableEvent {...defaultProps} onResizeStart={onResizeStart} />
);
const handles = container.querySelectorAll('.cursor-ns-resize');
const bottomHandle = handles[handles.length - 1]; // Get the last one (bottom)
if (bottomHandle) {
fireEvent.mouseDown(bottomHandle);
expect(onResizeStart).toHaveBeenCalledWith(
expect.any(Object),
'right',
1
);
}
});
it('renders grip icon', () => {
const { container } = render(<DraggableEvent {...defaultProps} />);
const gripIcon = container.querySelector('svg');
expect(gripIcon).toBeInTheDocument();
});
it('applies hover styles', () => {
const { container } = render(<DraggableEvent {...defaultProps} />);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveClass('group', 'hover:shadow-md');
});
it('renders with correct base styling classes', () => {
const { container } = render(<DraggableEvent {...defaultProps} />);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveClass(
'absolute',
'rounded-b',
'overflow-hidden',
'group',
'bg-brand-100'
);
});
it('has two resize handles', () => {
const { container } = render(<DraggableEvent {...defaultProps} />);
const handles = container.querySelectorAll('.cursor-ns-resize');
expect(handles).toHaveLength(2);
});
it('stops propagation when resize handle is clicked', () => {
const onResizeStart = vi.fn();
const { container } = render(
<DraggableEvent {...defaultProps} onResizeStart={onResizeStart} />
);
const topHandle = container.querySelector('.cursor-ns-resize');
const mockEvent = {
stopPropagation: vi.fn(),
} as any;
if (topHandle) {
fireEvent.mouseDown(topHandle, mockEvent);
// The event handler should call stopPropagation to prevent drag
expect(onResizeStart).toHaveBeenCalled();
}
});
it('renders content area with cursor-move', () => {
const { container } = render(<DraggableEvent {...defaultProps} />);
const contentArea = container.querySelector('.cursor-move');
expect(contentArea).toBeInTheDocument();
expect(contentArea).toHaveClass('select-none');
});
it('applies different heights correctly', () => {
const { container } = render(
<DraggableEvent {...defaultProps} height={100} />
);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveStyle({ height: '100px' });
});
it('applies different widths correctly', () => {
const { container } = render(
<DraggableEvent {...defaultProps} width={300} />
);
const eventElement = container.querySelector('.absolute.rounded-b');
expect(eventElement).toHaveStyle({ width: '300px' });
});
});