test(modal): cover Modal and ConfirmDialog primitives

This commit is contained in:
Psychotoxical
2026-06-21 02:10:44 +02:00
parent 0859e856f7
commit c604a6d645
2 changed files with 285 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
import { describe, it, expect, vi } from 'vitest';
import { screen, fireEvent, within } from '@testing-library/react';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import ConfirmDialog from './ConfirmDialog';
/** Footer-scoped button query — the header X shares the cancel label by design. */
const footer = () => within(document.querySelector('.ui-modal-footer') as HTMLElement);
describe('ConfirmDialog', () => {
it('renders title, message and both buttons', () => {
renderWithProviders(
<ConfirmDialog
open
title="Delete playlist?"
message="This cannot be undone."
confirmLabel="Delete"
cancelLabel="Cancel"
onConfirm={vi.fn()}
onCancel={vi.fn()}
/>,
);
expect(screen.getByText('Delete playlist?')).toBeInTheDocument();
expect(screen.getByText('This cannot be undone.')).toBeInTheDocument();
expect(footer().getByRole('button', { name: 'Delete' })).toBeInTheDocument();
expect(footer().getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
});
it('fires onConfirm and onCancel from their buttons', () => {
const onConfirm = vi.fn();
const onCancel = vi.fn();
renderWithProviders(
<ConfirmDialog
open
title="t"
message="m"
confirmLabel="OK"
cancelLabel="No"
onConfirm={onConfirm}
onCancel={onCancel}
/>,
);
fireEvent.click(footer().getByRole('button', { name: 'OK' }));
expect(onConfirm).toHaveBeenCalledOnce();
fireEvent.click(footer().getByRole('button', { name: 'No' }));
expect(onCancel).toHaveBeenCalledOnce();
});
it('gives the confirm button initial focus', () => {
renderWithProviders(
<ConfirmDialog
open
title="t"
message="m"
confirmLabel="Confirm"
cancelLabel="Cancel"
onConfirm={vi.fn()}
onCancel={vi.fn()}
/>,
);
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Confirm' }));
});
it('applies danger styling to the confirm button', () => {
renderWithProviders(
<ConfirmDialog open title="t" message="m" confirmLabel="Delete" danger onConfirm={vi.fn()} />,
);
expect(footer().getByRole('button', { name: 'Delete' })).toHaveStyle({
background: 'var(--danger)',
});
});
it('renders a single button and resolves via onConfirm on Escape', () => {
const onConfirm = vi.fn();
renderWithProviders(
<ConfirmDialog open title="t" message="m" confirmLabel="Got it" onConfirm={onConfirm} />,
);
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
fireEvent.keyDown(window, { key: 'Escape' });
expect(onConfirm).toHaveBeenCalledOnce();
});
it('disables buttons and blocks dismissal while busy', () => {
const onConfirm = vi.fn();
const onCancel = vi.fn();
renderWithProviders(
<ConfirmDialog
open
busy
title="t"
message="m"
confirmLabel="OK"
cancelLabel="Cancel"
onConfirm={onConfirm}
onCancel={onCancel}
/>,
);
expect(footer().getByRole('button', { name: 'OK' })).toBeDisabled();
expect(footer().getByRole('button', { name: 'Cancel' })).toBeDisabled();
fireEvent.keyDown(window, { key: 'Escape' });
fireEvent.click(screen.getByRole('dialog').parentElement!);
expect(onCancel).not.toHaveBeenCalled();
expect(onConfirm).not.toHaveBeenCalled();
});
});
+180
View File
@@ -0,0 +1,180 @@
import { describe, it, expect, vi } from 'vitest';
import { createRef } from 'react';
import { screen, fireEvent, render } from '@testing-library/react';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import Modal from './Modal';
describe('Modal', () => {
it('renders nothing when closed', () => {
renderWithProviders(
<Modal open={false} onClose={vi.fn()} title="Hidden">
body
</Modal>,
);
expect(screen.queryByRole('dialog')).toBeNull();
});
it('renders title, subtitle, icon, body and footer when open', () => {
renderWithProviders(
<Modal
open
onClose={vi.fn()}
title="My title"
subtitle="My subtitle"
icon={<span data-testid="icon" />}
footer={<button>Do it</button>}
>
<p>body content</p>
</Modal>,
);
expect(screen.getByText('My title')).toBeInTheDocument();
expect(screen.getByText('My subtitle')).toBeInTheDocument();
expect(screen.getByTestId('icon')).toBeInTheDocument();
expect(screen.getByText('body content')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Do it' })).toBeInTheDocument();
});
it('portals into document.body', () => {
renderWithProviders(
<Modal open onClose={vi.fn()} title="Portaled">
body
</Modal>,
);
const dialog = screen.getByRole('dialog');
expect(dialog.closest('.ui-modal-backdrop')?.parentElement).toBe(document.body);
});
it('labels the dialog with the title element id', () => {
renderWithProviders(
<Modal open onClose={vi.fn()} title="Accessible">
body
</Modal>,
);
const dialog = screen.getByRole('dialog');
const labelledBy = dialog.getAttribute('aria-labelledby');
expect(labelledBy).toBeTruthy();
expect(document.getElementById(labelledBy!)).toHaveTextContent('Accessible');
});
it('calls onClose on Escape, backdrop click and the X button', async () => {
const onClose = vi.fn();
renderWithProviders(
<Modal open onClose={onClose} title="Closable">
body
</Modal>,
);
fireEvent.keyDown(window, { key: 'Escape' });
expect(onClose).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole('dialog').parentElement!);
expect(onClose).toHaveBeenCalledTimes(2);
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
expect(onClose).toHaveBeenCalledTimes(3);
});
it('does not close on Escape when closeOnEscape is false', () => {
const onClose = vi.fn();
renderWithProviders(
<Modal open onClose={onClose} title="Guarded" closeOnEscape={false}>
body
</Modal>,
);
fireEvent.keyDown(window, { key: 'Escape' });
expect(onClose).not.toHaveBeenCalled();
});
it('does not close on backdrop click when closeOnBackdrop is false', () => {
const onClose = vi.fn();
renderWithProviders(
<Modal open onClose={onClose} title="Guarded" closeOnBackdrop={false}>
body
</Modal>,
);
fireEvent.click(screen.getByRole('dialog').parentElement!);
expect(onClose).not.toHaveBeenCalled();
});
it('hides the close button when hideClose is set', () => {
renderWithProviders(
<Modal open onClose={vi.fn()} title="No X" hideClose>
body
</Modal>,
);
expect(screen.queryByRole('button', { name: 'Close' })).toBeNull();
});
it('focuses the initialFocusRef target on open', () => {
const ref = createRef<HTMLButtonElement>();
renderWithProviders(
<Modal open onClose={vi.fn()} title="Focus" initialFocusRef={ref} footer={<button ref={ref}>Primary</button>}>
body
</Modal>,
);
expect(document.activeElement).toBe(ref.current);
});
it('focuses the first body field when no initialFocusRef is given', () => {
renderWithProviders(
<Modal open onClose={vi.fn()} title="Form">
<input aria-label="name" />
</Modal>,
);
expect(document.activeElement).toBe(screen.getByLabelText('name'));
});
it('traps Tab focus inside the dialog', () => {
renderWithProviders(
<Modal open onClose={vi.fn()} title="Trap" footer={<button>Last</button>}>
body
</Modal>,
);
const closeBtn = screen.getByRole('button', { name: 'Close' });
const lastBtn = screen.getByRole('button', { name: 'Last' });
lastBtn.focus();
fireEvent.keyDown(window, { key: 'Tab' });
expect(document.activeElement).toBe(closeBtn);
closeBtn.focus();
fireEvent.keyDown(window, { key: 'Tab', shiftKey: true });
expect(document.activeElement).toBe(lastBtn);
});
it('restores focus to the opening element on close', () => {
const onClose = vi.fn();
const { rerender } = render(
<>
<button data-testid="opener">opener</button>
<Modal open={false} onClose={onClose} title="Restore">
body
</Modal>
</>,
);
const opener = screen.getByTestId('opener');
opener.focus();
expect(document.activeElement).toBe(opener);
rerender(
<>
<button data-testid="opener">opener</button>
<Modal open onClose={onClose} title="Restore">
body
</Modal>
</>,
);
// Modal grabbed focus.
expect(document.activeElement).not.toBe(opener);
rerender(
<>
<button data-testid="opener">opener</button>
<Modal open={false} onClose={onClose} title="Restore">
body
</Modal>
</>,
);
expect(document.activeElement).toBe(opener);
});
});