134 lines
3.7 KiB
JavaScript
134 lines
3.7 KiB
JavaScript
|
|
// @vitest-environment jsdom
|
||
|
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||
|
|
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
||
|
|
import Modal from '../../components/ui/Modal';
|
||
|
|
|
||
|
|
describe('Modal', () => {
|
||
|
|
afterEach(() => cleanup());
|
||
|
|
|
||
|
|
it('renders nothing when open is false', () => {
|
||
|
|
const { container } = render(
|
||
|
|
<Modal open={false} onClose={() => {}} title="Test">
|
||
|
|
<p>body</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
expect(container.querySelector('[role="dialog"]')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders dialog with correct ARIA shape when open', () => {
|
||
|
|
render(
|
||
|
|
<Modal
|
||
|
|
open={true}
|
||
|
|
onClose={() => {}}
|
||
|
|
title="My Title"
|
||
|
|
description="My description"
|
||
|
|
>
|
||
|
|
<p>body</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
const dialog = screen.getByRole('dialog');
|
||
|
|
expect(dialog.getAttribute('aria-modal')).toBe('true');
|
||
|
|
expect(dialog.getAttribute('aria-labelledby')).toBeTruthy();
|
||
|
|
expect(dialog.getAttribute('aria-describedby')).toBeTruthy();
|
||
|
|
expect(screen.getByText('My Title')).toBeTruthy();
|
||
|
|
expect(screen.getByText('My description')).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('omits aria-describedby when no description is provided', () => {
|
||
|
|
render(
|
||
|
|
<Modal open={true} onClose={() => {}} title="t">
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
const dialog = screen.getByRole('dialog');
|
||
|
|
expect(dialog.getAttribute('aria-describedby')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onClose on ESC keydown', () => {
|
||
|
|
const onClose = vi.fn();
|
||
|
|
render(
|
||
|
|
<Modal open={true} onClose={onClose} title="t">
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
fireEvent.keyDown(document, { key: 'Escape' });
|
||
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does NOT call onClose on ESC when closeOnEsc is false', () => {
|
||
|
|
const onClose = vi.fn();
|
||
|
|
render(
|
||
|
|
<Modal open={true} onClose={onClose} title="t" closeOnEsc={false}>
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
fireEvent.keyDown(document, { key: 'Escape' });
|
||
|
|
expect(onClose).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onClose on backdrop click', () => {
|
||
|
|
const onClose = vi.fn();
|
||
|
|
render(
|
||
|
|
<Modal open={true} onClose={onClose} title="t">
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
fireEvent.click(screen.getByRole('dialog'));
|
||
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does NOT call onClose on backdrop click when closeOnBackdrop is false', () => {
|
||
|
|
const onClose = vi.fn();
|
||
|
|
render(
|
||
|
|
<Modal
|
||
|
|
open={true}
|
||
|
|
onClose={onClose}
|
||
|
|
title="t"
|
||
|
|
closeOnBackdrop={false}
|
||
|
|
>
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
fireEvent.click(screen.getByRole('dialog'));
|
||
|
|
expect(onClose).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders a built-in close button with aria-label="Close" that triggers onClose', () => {
|
||
|
|
const onClose = vi.fn();
|
||
|
|
render(
|
||
|
|
<Modal open={true} onClose={onClose} title="t">
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
const closeBtn = screen.getByRole('button', { name: /close/i });
|
||
|
|
fireEvent.click(closeBtn);
|
||
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('hides the built-in close button when hideCloseButton is true', () => {
|
||
|
|
render(
|
||
|
|
<Modal open={true} onClose={() => {}} title="t" hideCloseButton>
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
screen.queryByRole('button', { name: /close/i })
|
||
|
|
).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('locks body-scroll when open and restores on close', () => {
|
||
|
|
const { rerender } = render(
|
||
|
|
<Modal open={true} onClose={() => {}} title="t">
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
expect(document.body.style.overflow).toBe('hidden');
|
||
|
|
rerender(
|
||
|
|
<Modal open={false} onClose={() => {}} title="t">
|
||
|
|
<p>b</p>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
expect(document.body.style.overflow).toBe('');
|
||
|
|
});
|
||
|
|
});
|