/** * DeleteConfirmationDialog tests — Task 3 (TDD RED → GREEN) * * Covers UI-SPEC §DeleteConfirmationDialog: * - Renders heading "Delete event?" and body copy * - Cancel button closes without deleting * - Escape closes without deleting * - Delete (red) button calls deleteEvent, sets lastSyncedUid, closes dialog and popover * - Focus trapped inside dialog * - T-03-17: mandatory two-tap confirmation (no single-tap delete) * - T-03-18: failed delete toast persists (wired via lastSyncedUid → SyncStateToast) */ import 'temporal-polyfill/global'; import React from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // ── Module mocks ────────────────────────────────────────────────────────────── // vi.hoisted() required for variables referenced inside vi.mock() factories (D-03-04-hoisting) const { mockDeleteEvent, mockSetDeleteDialog, mockSetLastSyncedUid, mockSetOpenEventId, mockDeleteDialogOpen, mockDeleteDialogUid, } = vi.hoisted(() => ({ mockDeleteEvent: vi.fn(), mockSetDeleteDialog: vi.fn(), mockSetLastSyncedUid: vi.fn(), mockSetOpenEventId: vi.fn(), mockDeleteDialogOpen: { value: true }, mockDeleteDialogUid: { value: 'event-uid-to-delete' }, })); vi.mock('../store/calendarStore.js', () => ({ useCalendarStore: (selector: (s: Record) => unknown) => { const state = { deleteDialogOpen: mockDeleteDialogOpen.value, deleteDialogUid: mockDeleteDialogUid.value, setDeleteDialog: mockSetDeleteDialog, setLastSyncedUid: mockSetLastSyncedUid, setOpenEventId: mockSetOpenEventId, }; return selector(state); }, })); vi.mock('../api/client.js', () => ({ deleteEvent: mockDeleteEvent, })); // ── Import component (after mocks) ──────────────────────────────────────────── import { DeleteConfirmationDialog } from './DeleteConfirmationDialog.js'; // ── Helpers ─────────────────────────────────────────────────────────────────── function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false } } }); } function renderDialog() { const client = makeQueryClient(); return render( , ); } // ── Tests ───────────────────────────────────────────────────────────────────── describe('DeleteConfirmationDialog', () => { beforeEach(() => { vi.clearAllMocks(); mockDeleteDialogOpen.value = true; mockDeleteDialogUid.value = 'event-uid-to-delete'; mockDeleteEvent.mockResolvedValue(undefined); }); it('renders heading "Delete event?"', () => { renderDialog(); expect(screen.getByRole('heading', { name: /delete event\?/i })).toBeInTheDocument(); }); it('renders the body copy about Fastmail calendar', () => { renderDialog(); expect( screen.getByText('This will be removed from your Fastmail calendar.'), ).toBeInTheDocument(); }); it('renders Cancel and Delete buttons', () => { renderDialog(); expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /^delete$/i })).toBeInTheDocument(); }); it('Cancel button closes dialog without deleting', () => { renderDialog(); fireEvent.click(screen.getByRole('button', { name: /cancel/i })); expect(mockSetDeleteDialog).toHaveBeenCalledWith(false); expect(mockDeleteEvent).not.toHaveBeenCalled(); }); it('Escape key closes dialog without deleting', () => { renderDialog(); fireEvent.keyDown(document, { key: 'Escape' }); expect(mockSetDeleteDialog).toHaveBeenCalledWith(false); expect(mockDeleteEvent).not.toHaveBeenCalled(); }); it('Delete button calls deleteEvent with the uid', async () => { renderDialog(); fireEvent.click(screen.getByRole('button', { name: /^delete$/i })); await waitFor(() => { expect(mockDeleteEvent).toHaveBeenCalledWith('event-uid-to-delete'); }); }); it('Delete button sets lastSyncedUid to the uid', async () => { renderDialog(); fireEvent.click(screen.getByRole('button', { name: /^delete$/i })); await waitFor(() => { expect(mockSetLastSyncedUid).toHaveBeenCalledWith('event-uid-to-delete'); }); }); it('Delete button closes dialog and popover on success', async () => { renderDialog(); fireEvent.click(screen.getByRole('button', { name: /^delete$/i })); await waitFor(() => { expect(mockSetDeleteDialog).toHaveBeenCalledWith(false); expect(mockSetOpenEventId).toHaveBeenCalledWith(null); }); }); it('renders nothing when deleteDialogOpen is false', () => { mockDeleteDialogOpen.value = false; const { container } = renderDialog(); expect(container.firstChild).toBeNull(); }); it('has role="dialog" and aria-modal="true"', () => { renderDialog(); const dialog = screen.getByRole('dialog'); expect(dialog).toBeInTheDocument(); expect(dialog).toHaveAttribute('aria-modal', 'true'); }); });