diff --git a/web/src/lib/components/Modal.svelte b/web/src/lib/components/Modal.svelte new file mode 100644 index 00000000..32257700 --- /dev/null +++ b/web/src/lib/components/Modal.svelte @@ -0,0 +1,58 @@ + + + + +{#if open} + +{/if} diff --git a/web/src/lib/components/Modal.test.ts b/web/src/lib/components/Modal.test.ts new file mode 100644 index 00000000..2e743baa --- /dev/null +++ b/web/src/lib/components/Modal.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/svelte'; +import { createRawSnippet } from 'svelte'; +import Modal from './Modal.svelte'; + +// children is a Snippet; createRawSnippet renders raw HTML inside the modal +// panel for assertions on backdrop vs panel click targeting. +const bodySnippet = createRawSnippet(() => ({ + render: () => `

body content

` +})); + +// The component's children-Snippet generic isn't inferable through +// testing-library's render; cast to any to satisfy the prop shape. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyProps = any; + +describe('Modal', () => { + test('renders nothing when open=false', () => { + render(Modal, { + props: { + title: 'Hidden', + open: false, + onClose: vi.fn(), + children: bodySnippet + } as AnyProps + }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + test('renders dialog with role/aria attrs when open=true', () => { + render(Modal, { + props: { + title: 'Visible', + open: true, + onClose: vi.fn(), + children: bodySnippet + } as AnyProps + }); + const dialog = screen.getByRole('dialog'); + expect(dialog).toHaveAttribute('aria-modal', 'true'); + expect(dialog).toHaveAttribute('aria-labelledby', 'modal-title'); + }); + + test('title text appears in