mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
test(modal): cover Modal and ConfirmDialog primitives
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user