Phase 6.2: AppointmentModal on RHF + Zod
- Migrated AppointmentModal from useState to React Hook Form + Zod - Zod schema: title (required), calendar_id (required), start_at/end_at (required, end > start) - Object-level superRefine for date validation (end must be after start) - Error display via Input error prop and summary error div - Preserved all existing functionality: create, edit, delete, prefill - Added 3 validation tests (empty title, end before start, valid submit)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { AppointmentModal } from '@/components/calendar/AppointmentModal';
|
||||
import type { Calendar } from '@/api/calendar';
|
||||
|
||||
vi.mock('@/api/calendar', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/api/calendar')>('@/api/calendar');
|
||||
return {
|
||||
...actual,
|
||||
createEntry: vi.fn(),
|
||||
updateEntry: vi.fn(),
|
||||
deleteEntry: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { createEntry } from '@/api/calendar';
|
||||
|
||||
const calendars: Calendar[] = [
|
||||
{ id: 'cal-1', name: 'Persönlich', color: '#3B82F6', type: 'personal', owner_id: 'u-1' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('AppointmentModal validation (RHF + Zod)', () => {
|
||||
it('shows validation error when title is empty', async () => {
|
||||
render(
|
||||
<AppointmentModal
|
||||
open
|
||||
onClose={vi.fn()}
|
||||
prefillDate={new Date(2026, 6, 1)}
|
||||
calendars={calendars}
|
||||
defaultCalendarId="cal-1"
|
||||
onSaved={vi.fn()}
|
||||
onDeleted={vi.fn()}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('appointment-save'));
|
||||
const err = await screen.findByTestId('appointment-error');
|
||||
expect(err.textContent?.toLowerCase()).toContain('erforderlich');
|
||||
});
|
||||
|
||||
it('shows error when end is before start', async () => {
|
||||
render(
|
||||
<AppointmentModal
|
||||
open
|
||||
onClose={vi.fn()}
|
||||
prefillDate={new Date(2026, 6, 1)}
|
||||
calendars={calendars}
|
||||
defaultCalendarId="cal-1"
|
||||
onSaved={vi.fn()}
|
||||
onDeleted={vi.fn()}
|
||||
/>
|
||||
);
|
||||
// Set title so only date validation fails
|
||||
fireEvent.change(screen.getByTestId('appointment-title'), { target: { value: 'Test' } });
|
||||
// Set end before start
|
||||
fireEvent.change(screen.getByTestId('appointment-end'), { target: { value: '2026-07-01T08:00' } });
|
||||
fireEvent.click(screen.getByTestId('appointment-save'));
|
||||
await waitFor(() => {
|
||||
const err = screen.queryByTestId('appointment-error');
|
||||
expect(err).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('submits successfully with valid data', async () => {
|
||||
(createEntry as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
id: 'e-1', calendar_id: 'cal-1', entry_type: 'appointment', subtype: 'normal',
|
||||
title: 'Test', description: null, location: null,
|
||||
start_at: '2026-07-01T09:00:00.000Z', end_at: '2026-07-01T10:00:00.000Z',
|
||||
all_day: false, priority: 'medium', status: 'open', created_by: 'u-1',
|
||||
});
|
||||
const onSaved = vi.fn();
|
||||
render(
|
||||
<AppointmentModal
|
||||
open
|
||||
onClose={vi.fn()}
|
||||
prefillDate={new Date(2026, 6, 1, 9, 0)}
|
||||
calendars={calendars}
|
||||
defaultCalendarId="cal-1"
|
||||
onSaved={onSaved}
|
||||
onDeleted={vi.fn()}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByTestId('appointment-title'), { target: { value: 'Test Termin' } });
|
||||
fireEvent.click(screen.getByTestId('appointment-save'));
|
||||
await waitFor(() => {
|
||||
expect(createEntry).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(onSaved).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user