T08b: Frontend Calendar UI (month view, kanban, ICS, resources, sharing)
- 8 calendar components (MonthView, KanbanBoard, AppointmentModal, TaskDetailPanel, IcsControls, ResourceBooking, SharingSettings + API client) - 2 pages (/calendar, /calendar/kanban) + zustand store - 17 vitest tests (MonthView 5, KanbanBoard 6, AppointmentModal 6) all passing - i18n: calendar namespace in en/de (104 lines each, +exportSuccess key) - TS strict mode pass, npm run build pass
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MonthView } from '@/components/calendar/MonthView';
|
||||
import type { CalendarEntry } from '@/api/calendar';
|
||||
|
||||
const sampleEntries: CalendarEntry[] = [
|
||||
{
|
||||
id: 'a-1',
|
||||
calendar_id: 'cal-1',
|
||||
entry_type: 'appointment',
|
||||
subtype: 'normal',
|
||||
title: 'Team-Meeting',
|
||||
start_at: '2026-06-15T09:00:00.000Z',
|
||||
end_at: '2026-06-15T10:00:00.000Z',
|
||||
all_day: false,
|
||||
priority: 'medium',
|
||||
status: 'open',
|
||||
created_by: 'u-1',
|
||||
},
|
||||
{
|
||||
id: 'a-2',
|
||||
calendar_id: 'cal-1',
|
||||
entry_type: 'appointment',
|
||||
subtype: 'normal',
|
||||
title: 'Kundentermin',
|
||||
start_at: '2026-06-22T14:00:00.000Z',
|
||||
end_at: '2026-06-22T15:00:00.000Z',
|
||||
all_day: false,
|
||||
priority: 'high',
|
||||
status: 'open',
|
||||
created_by: 'u-1',
|
||||
},
|
||||
];
|
||||
|
||||
describe('MonthView', () => {
|
||||
it('renders the month grid with header', () => {
|
||||
render(
|
||||
<MonthView
|
||||
visibleMonth={new Date(2026, 5, 1)}
|
||||
entries={[]}
|
||||
onCreateAt={vi.fn()}
|
||||
onEditEntry={vi.fn()}
|
||||
onMoveEntry={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('month-view')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('month-view-grid')).toBeInTheDocument();
|
||||
// Header includes month name and year
|
||||
expect(screen.getByTestId('month-view-header').textContent).toContain('2026');
|
||||
});
|
||||
|
||||
it('renders all 42 day cells (6 weeks × 7 days)', () => {
|
||||
render(
|
||||
<MonthView
|
||||
visibleMonth={new Date(2026, 5, 1)}
|
||||
entries={[]}
|
||||
onCreateAt={vi.fn()}
|
||||
onEditEntry={vi.fn()}
|
||||
onMoveEntry={vi.fn()}
|
||||
/>
|
||||
);
|
||||
const cells = screen.getAllByRole('button', { hidden: false }).filter((el) =>
|
||||
el.getAttribute('data-testid')?.startsWith('month-cell-')
|
||||
);
|
||||
expect(cells.length).toBe(42);
|
||||
});
|
||||
|
||||
it('renders an entry chip for each appointment and clicking it fires onEditEntry', () => {
|
||||
const onEdit = vi.fn();
|
||||
render(
|
||||
<MonthView
|
||||
visibleMonth={new Date(2026, 5, 1)}
|
||||
entries={sampleEntries}
|
||||
onCreateAt={vi.fn()}
|
||||
onEditEntry={onEdit}
|
||||
onMoveEntry={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('month-entry-a-1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('month-entry-a-2')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('month-entry-a-1'));
|
||||
expect(onEdit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'a-1', title: 'Team-Meeting' })
|
||||
);
|
||||
});
|
||||
|
||||
it('clicking an empty cell fires onCreateAt with the day Date', () => {
|
||||
const onCreate = vi.fn();
|
||||
render(
|
||||
<MonthView
|
||||
visibleMonth={new Date(2026, 5, 1)}
|
||||
entries={[]}
|
||||
onCreateAt={onCreate}
|
||||
onEditEntry={vi.fn()}
|
||||
onMoveEntry={vi.fn()}
|
||||
/>
|
||||
);
|
||||
// Pick the first cell of the grid
|
||||
const cells = screen.getAllByRole('button', { hidden: false }).filter((el) =>
|
||||
el.getAttribute('data-testid')?.startsWith('month-cell-')
|
||||
);
|
||||
fireEvent.click(cells[0]);
|
||||
expect(onCreate).toHaveBeenCalledTimes(1);
|
||||
const arg = onCreate.mock.calls[0][0] as Date;
|
||||
expect(arg).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('drag-and-drop fires onMoveEntry with the new start time-of-day preserved', () => {
|
||||
const onMove = vi.fn();
|
||||
const { container } = render(
|
||||
<MonthView
|
||||
visibleMonth={new Date(2026, 5, 1)}
|
||||
entries={sampleEntries}
|
||||
onCreateAt={vi.fn()}
|
||||
onEditEntry={vi.fn()}
|
||||
onMoveEntry={onMove}
|
||||
/>
|
||||
);
|
||||
const entryEl = screen.getByTestId('month-entry-a-1');
|
||||
const cells = container.querySelectorAll('[data-testid^="month-cell-"]');
|
||||
// Pick a cell that's clearly different from June 15
|
||||
const targetCell = cells[20] as HTMLElement;
|
||||
const dataTransfer = {
|
||||
setData: vi.fn(),
|
||||
getData: (k: string) => (k === 'application/x-calendar-entry' ? 'a-1' : ''),
|
||||
types: ['application/x-calendar-entry'],
|
||||
effectAllowed: '',
|
||||
dropEffect: '',
|
||||
};
|
||||
fireEvent.dragStart(entryEl, { dataTransfer });
|
||||
fireEvent.dragOver(targetCell, { dataTransfer });
|
||||
fireEvent.drop(targetCell, { dataTransfer });
|
||||
expect(onMove).toHaveBeenCalledTimes(1);
|
||||
const [, newStart] = onMove.mock.calls[0];
|
||||
expect(newStart).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user