Files
familysync/apps/pwa/src/components/DeleteConfirmationDialog.test.tsx
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

155 lines
5.6 KiB
TypeScript

/**
* 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<string, unknown>) => 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(
<QueryClientProvider client={client}>
<DeleteConfirmationDialog />
</QueryClientProvider>,
);
}
// ── 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');
});
});