import { render, screen, fireEvent } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import ConfirmDialog from './ConfirmDialog';
import Modal from './Modal';
describe('ConfirmDialog', () => {
const onClose = vi.fn();
const onConfirm = vi.fn();
beforeEach(() => {
onClose.mockClear();
onConfirm.mockClear();
});
it('FE-COMP-CONFIRM-001: does not render when isOpen is false', () => {
render(
);
expect(screen.queryByText('Are you sure?')).toBeNull();
});
it('FE-COMP-CONFIRM-002: renders with default title "Confirm" and message', () => {
render(
);
expect(screen.getByText('Confirm')).toBeTruthy();
expect(screen.getByText('Are you sure?')).toBeTruthy();
});
it('FE-COMP-CONFIRM-003: renders custom title and message', () => {
render(
);
expect(screen.getByText('Remove item')).toBeTruthy();
expect(screen.getByText('This cannot be undone.')).toBeTruthy();
});
it('FE-COMP-CONFIRM-004: Cancel button calls onClose', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(onClose).toHaveBeenCalledOnce();
expect(onConfirm).not.toHaveBeenCalled();
});
it('FE-COMP-CONFIRM-005: Confirm button calls onConfirm and onClose', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole('button', { name: /delete/i }));
expect(onConfirm).toHaveBeenCalledOnce();
expect(onClose).toHaveBeenCalledOnce();
});
it('FE-COMP-CONFIRM-006: custom button labels render correctly', () => {
render(
);
expect(screen.getByRole('button', { name: 'Yes, remove' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'Go back' })).toBeTruthy();
});
it('FE-COMP-CONFIRM-007: Escape key calls onClose', () => {
render();
fireEvent.keyDown(document, { key: 'Escape' });
expect(onClose).toHaveBeenCalledOnce();
});
it('FE-COMP-CONFIRM-009: a rejecting async onConfirm does not escape as an unhandled rejection', async () => {
const unhandled = vi.fn();
process.on('unhandledRejection', unhandled);
const failing = vi.fn(() => Promise.reject(new Error('delete failed')));
render();
fireEvent.click(screen.getByRole('button', { name: /delete/i }));
await new Promise(resolve => setTimeout(resolve, 0));
expect(failing).toHaveBeenCalledOnce();
expect(onClose).toHaveBeenCalledOnce();
expect(unhandled).not.toHaveBeenCalled();
process.off('unhandledRejection', unhandled);
});
it('FE-COMP-CONFIRM-010: extra content renders under the message, and the card widens for it', () => {
render(
);
const list = screen.getByRole('list', { name: 'consequences' });
expect(list).toHaveTextContent('Stay at Harbour Hotel');
expect(screen.getByText('Delete it?').compareDocumentPosition(list) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(document.querySelector('.max-w-md')).not.toBeNull();
});
it('FE-COMP-CONFIRM-011: Escape takes back the question only, not the dialog it was asked from', () => {
const closeModal = vi.fn();
render(
<>
rows
>
);
fireEvent.keyDown(document.body, { key: 'Escape' });
expect(onClose).toHaveBeenCalledOnce();
expect(closeModal).not.toHaveBeenCalled();
// Other keys pass through untouched.
fireEvent.keyDown(document.body, { key: 'Enter' });
expect(onClose).toHaveBeenCalledOnce();
});
it('FE-COMP-CONFIRM-012: the danger look comes from the danger tokens, the plain one from the accent', () => {
const { unmount } = render();
expect(screen.getByRole('button', { name: /delete/i })).toHaveClass('bg-danger');
expect(document.querySelector('.bg-danger-soft')).not.toBeNull();
unmount();
render();
expect(screen.getByRole('button', { name: 'Keep' })).toHaveClass('bg-accent', 'text-accent-text');
expect(document.querySelector('.bg-danger-soft')).toBeNull();
});
it('FE-COMP-CONFIRM-013: a long list scrolls inside the card, and the buttons stay outside it, always in reach', () => {
render(
{Array.from({ length: 40 }, (_, i) => - Row {i}
)}
);
const scroller = screen.getByRole('list', { name: 'consequences' }).closest('.overflow-y-auto') as HTMLElement;
expect(scroller).not.toBeNull();
expect(scroller.parentElement).toHaveClass('flex-col');
expect(scroller.parentElement!.className).toMatch(/max-h-\[/);
const confirm = screen.getByRole('button', { name: /delete/i });
expect(scroller.contains(confirm)).toBe(false);
expect(confirm.parentElement).toHaveClass('flex-none');
});
it('FE-COMP-CONFIRM-008: clicking backdrop calls onClose', async () => {
const user = userEvent.setup();
render();
// The outermost fixed div is the backdrop — click outside the card
const backdrop = document.querySelector('.fixed') as HTMLElement;
// fireEvent click on the backdrop element directly
fireEvent.click(backdrop);
expect(onClose).toHaveBeenCalledOnce();
});
describe('on a phone', () => {
const desktopWidth = window.innerWidth;
beforeEach(() => { Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 390 }); });
afterEach(() => { Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: desktopWidth }); });
it('FE-COMP-CONFIRM-014: the phone card answers like the desktop one, and a tap on it does not close it', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByText('It is gone for good.'));
expect(onClose).not.toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: 'Remove' }));
expect(onConfirm).toHaveBeenCalledOnce();
expect(onClose).toHaveBeenCalledOnce();
});
it('FE-COMP-CONFIRM-015: the phone card without danger uses the accent and the default labels', async () => {
const user = userEvent.setup();
render(More
);
expect(screen.getByText('Confirm')).toBeInTheDocument();
expect(screen.getByText('More')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Delete' })).toHaveClass('bg-accent');
await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onClose).toHaveBeenCalledOnce();
expect(onConfirm).not.toHaveBeenCalled();
});
});
});