- 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>
229 lines
7.2 KiB
TypeScript
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' });
|
|
});
|
|
});
|