// FE-COMP-DIALOGSHELL-001 to FE-COMP-DIALOGSHELL-013 import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useState } from 'react'; import { render, screen, fireEvent } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { resetBodyScrollLock } from '../../utils/bodyScrollLock'; import { DeleteButton, DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs, } from './DialogShell'; const onClose = vi.fn(); beforeEach(() => { onClose.mockClear(); resetBodyScrollLock(); document.body.style.overflow = ''; }); describe('fs', () => { it('FE-COMP-DIALOGSHELL-001: scales a size by the user\'s text size for its tier', () => { expect(fs(12)).toEqual({ fontSize: 'calc(12px * var(--fs-scale-caption, 1))' }); expect(fs(20, 'subtitle')).toEqual({ fontSize: 'calc(20px * var(--fs-scale-subtitle, 1))' }); }); }); describe('DialogShell', () => { it('FE-COMP-DIALOGSHELL-002: a dialog that is all head and foot has no body, and takes its width and alignment', () => { render( i} tint={NEUTRAL_TINT} labelId="q-title" onClose={onClose} title="Leave?" />} footer={StayLeave} />, ); const dialog = screen.getByRole('dialog', { name: 'Leave?' }); expect(dialog.className).toContain('max-w-[520px]'); expect(document.querySelector('.trek-modal-backdrop')?.className).toContain('items-start'); expect(dialog.querySelector('form')).toBeNull(); expect(dialog.children).toHaveLength(2); }); it('FE-COMP-DIALOGSHELL-003: a body class replaces the padded column, and a paste anywhere reaches the dialog', () => { const onPaste = vi.fn(); render( }>

Document

, ); expect(screen.getByText('Document').parentElement?.className).toBe('preview-body'); fireEvent.paste(screen.getByRole('dialog', { name: 'Preview' })); expect(onPaste).toHaveBeenCalledTimes(1); }); }); describe('DialogShell unsaved-changes question (#2253)', () => { function Editor() { const [name, setName] = useState(''); return ( }> setName(e.target.value)} /> ); } const backdrop = () => document.querySelector('.trek-modal-backdrop') as HTMLElement; const clickBackdrop = () => { fireEvent.mouseDown(backdrop()); fireEvent.click(backdrop()); }; it('FE-COMP-DIALOGSHELL-2253-1: an untouched editor closes on the backdrop straight away', () => { render(); clickBackdrop(); expect(onClose).toHaveBeenCalledTimes(1); }); it('FE-COMP-DIALOGSHELL-2253-2: after typing, the backdrop asks; keep editing stays, discard closes', async () => { const user = userEvent.setup(); render(); await user.type(screen.getByLabelText('Name'), 'Cafe'); clickBackdrop(); expect(onClose).not.toHaveBeenCalled(); expect(screen.getByRole('alertdialog')).toHaveTextContent('Discard your changes?'); await user.click(screen.getByRole('button', { name: 'Keep editing' })); expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(); expect(screen.getByLabelText('Name')).toHaveValue('Cafe'); await user.keyboard('{Escape}'); expect(screen.getByRole('alertdialog')).toBeInTheDocument(); // Escape on the question only takes the question away. await user.keyboard('{Escape}'); expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(); expect(onClose).not.toHaveBeenCalled(); clickBackdrop(); await user.click(screen.getByRole('button', { name: 'Discard' })); expect(onClose).toHaveBeenCalledTimes(1); }); it('FE-COMP-DIALOGSHELL-2253-3: typing back to where it started closes without asking', async () => { const user = userEvent.setup(); render(); await user.type(screen.getByLabelText('Name'), 'a'); await user.clear(screen.getByLabelText('Name')); clickBackdrop(); expect(onClose).toHaveBeenCalledTimes(1); }); }); describe('DialogHeader', () => { it('FE-COMP-DIALOGSHELL-004: a plain title is the heading the dialog is named by', () => { render(} tint={NEUTRAL_TINT} labelId="t1" onClose={onClose} title="Export" />); expect(screen.getByRole('heading', { level: 2, name: 'Export' })).toHaveAttribute('id', 't1'); expect(screen.getByTestId('tile-icon')).toBeInTheDocument(); }); it('FE-COMP-DIALOGSHELL-005: a title with a click is a rename button, the eyebrow then names the dialog', async () => { const onTitleClick = vi.fn(); render(); expect(screen.getByRole('heading', { name: 'Booking' })).toHaveAttribute('id', 't2'); const rename = screen.getByRole('button', { name: 'Hotel Sakura' }); expect(rename).not.toHaveAttribute('id'); await userEvent.click(rename); expect(onTitleClick).toHaveBeenCalledTimes(1); }); it('FE-COMP-DIALOGSHELL-006: a rename button without a tooltip still works', async () => { const onTitleClick = vi.fn(); render(); const rename = screen.getByRole('button', { name: 'Dinner' }); expect(rename).toHaveAttribute('id', 't3'); await userEvent.click(rename); expect(onTitleClick).toHaveBeenCalledTimes(1); }); it('FE-COMP-DIALOGSHELL-007: Enter in the title field is spent there, other keys and the blur reach the caller', () => { const onKeyDown = vi.fn(); const onBlur = vi.fn(); const onChange = vi.fn(); render( , ); const input = screen.getByRole('textbox', { name: 'Title' }); expect(input).toHaveAttribute('id', 't4'); expect(input).toBeRequired(); expect(input).toHaveAttribute('maxLength', '40'); const enter = fireEvent.keyDown(input, { key: 'Enter' }); expect(enter).toBe(false); const other = fireEvent.keyDown(input, { key: 'a' }); expect(other).toBe(true); expect(onKeyDown).toHaveBeenCalledTimes(2); fireEvent.blur(input); expect(onBlur).toHaveBeenCalledTimes(1); }); it('FE-COMP-DIALOGSHELL-008: the title field works without a key handler', () => { render(); expect(fireEvent.keyDown(screen.getByRole('textbox', { name: 'Name' }), { key: 'Enter' })).toBe(false); }); it('FE-COMP-DIALOGSHELL-009: Close calls onClose, and the pills sit under the title', async () => { render(3 nights} />); expect(screen.getByText('Kyoto')).toBeInTheDocument(); expect(screen.getByText('3 nights')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', { name: 'Close' })); expect(onClose).toHaveBeenCalledTimes(1); }); }); describe('body and footer parts', () => { it('FE-COMP-DIALOGSHELL-010: a section is labelled and can carry an action', async () => { const onAdd = vi.fn(); render( <> Add} className="own">

Maria

None

, ); expect(screen.getByText('Travelers')).toBeInTheDocument(); expect(screen.getByText('Maria').closest('section')?.className).toBe('own'); await userEvent.click(screen.getByRole('button', { name: 'Add' })); expect(onAdd).toHaveBeenCalledTimes(1); expect(screen.getAllByRole('button')).toHaveLength(1); }); it('FE-COMP-DIALOGSHELL-011: footer buttons are plain buttons by default, the primary one can submit', async () => { const onSave = vi.fn(); render( }>Cancel On map Save Later , ); expect(screen.getByRole('button', { name: 'Cancel' })).toHaveAttribute('type', 'button'); expect(screen.getByTestId('btn-icon')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'On map' }).className).toContain('bg-accent'); expect(screen.getByRole('button', { name: 'Cancel' }).className).not.toContain('bg-accent'); const save = screen.getByRole('button', { name: 'Save' }); expect(save).toHaveAttribute('type', 'submit'); await userEvent.click(save); expect(onSave).toHaveBeenCalledTimes(1); expect(screen.getByRole('button', { name: 'Later' })).toBeDisabled(); }); it('FE-COMP-DIALOGSHELL-012: the delete button is named "Delete" unless told otherwise', async () => { const onDelete = vi.fn(); const { rerender } = render(); await userEvent.click(screen.getByRole('button', { name: 'Delete' })); expect(onDelete).toHaveBeenCalledTimes(1); rerender(); expect(screen.getByRole('button', { name: 'Delete note' })).toBeDisabled(); }); it('FE-COMP-DIALOGSHELL-013: the neutral tint is a faint wash of the accent', () => { expect(NEUTRAL_TINT).toContain('var(--accent)'); }); });