diff --git a/src/components/ConfirmDialog.test.tsx b/src/components/ConfirmDialog.test.tsx
new file mode 100644
index 00000000..1d110fdb
--- /dev/null
+++ b/src/components/ConfirmDialog.test.tsx
@@ -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(
+ ,
+ );
+ 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(
+ ,
+ );
+ 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(
+ ,
+ );
+ expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Confirm' }));
+ });
+
+ it('applies danger styling to the confirm button', () => {
+ renderWithProviders(
+ ,
+ );
+ 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(
+ ,
+ );
+ 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(
+ ,
+ );
+ 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();
+ });
+});
diff --git a/src/components/Modal.test.tsx b/src/components/Modal.test.tsx
new file mode 100644
index 00000000..0a687b55
--- /dev/null
+++ b/src/components/Modal.test.tsx
@@ -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(
+
+ body
+ ,
+ );
+ expect(screen.queryByRole('dialog')).toBeNull();
+ });
+
+ it('renders title, subtitle, icon, body and footer when open', () => {
+ renderWithProviders(
+ }
+ footer={}
+ >
+
body content
+ ,
+ );
+ 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(
+
+ body
+ ,
+ );
+ 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(
+
+ body
+ ,
+ );
+ 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(
+
+ body
+ ,
+ );
+
+ 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(
+
+ body
+ ,
+ );
+ 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(
+
+ body
+ ,
+ );
+ fireEvent.click(screen.getByRole('dialog').parentElement!);
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it('hides the close button when hideClose is set', () => {
+ renderWithProviders(
+
+ body
+ ,
+ );
+ expect(screen.queryByRole('button', { name: 'Close' })).toBeNull();
+ });
+
+ it('focuses the initialFocusRef target on open', () => {
+ const ref = createRef();
+ renderWithProviders(
+ Primary}>
+ body
+ ,
+ );
+ expect(document.activeElement).toBe(ref.current);
+ });
+
+ it('focuses the first body field when no initialFocusRef is given', () => {
+ renderWithProviders(
+
+
+ ,
+ );
+ expect(document.activeElement).toBe(screen.getByLabelText('name'));
+ });
+
+ it('traps Tab focus inside the dialog', () => {
+ renderWithProviders(
+ Last}>
+ body
+ ,
+ );
+ 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(
+ <>
+
+
+ body
+
+ >,
+ );
+ const opener = screen.getByTestId('opener');
+ opener.focus();
+ expect(document.activeElement).toBe(opener);
+
+ rerender(
+ <>
+
+
+ body
+
+ >,
+ );
+ // Modal grabbed focus.
+ expect(document.activeElement).not.toBe(opener);
+
+ rerender(
+ <>
+
+
+ body
+
+ >,
+ );
+ expect(document.activeElement).toBe(opener);
+ });
+});