feat(fase-17): ChangelogModal component + unit tests
ChangelogModal renders the repo-root CHANGELOG.md (bundled at build time via Vite's ?raw query, 5 levels up from the component file) in an overlay+panel modal. Markdown is parsed inline (`#`/`##`/`###` headers, `-`/`*` lists, `**bold**`) with HTML-escape on every input line — input is static repo content so the surface is closed. Esc and overlay-click both call `onClose`. The keydown listener is attached only while `open` is true via a $effect. Three new Paraglide messages (en/es): `settings_about_view_changelog`, `changelog_modal_title`, `changelog_modal_close`. Unit tests (CHM-U-01..04) follow the Spinner.test.ts pattern (Svelte 5 mount/unmount + flushSync for the Escape spec). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
77
apps/web/src/lib/components/ChangelogModal.test.ts
Normal file
77
apps/web/src/lib/components/ChangelogModal.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Fase 17.4.1 — ChangelogModal unit tests.
|
||||
*
|
||||
* Mirrors the Spinner.test.ts pattern (Svelte 5 `mount`/`unmount`, no
|
||||
* @testing-library dependency). The modal imports CHANGELOG.md via Vite's
|
||||
* `?raw` query — vitest runs against the same Vite config (see
|
||||
* apps/web/vitest.config.ts) so the import resolves to the file contents at
|
||||
* test time.
|
||||
*
|
||||
* Specs:
|
||||
* CHM-U-01 — `open={false}` renders nothing.
|
||||
* CHM-U-02 — `open={true}` renders the CHANGELOG `<h1>`.
|
||||
* CHM-U-03 — click on the overlay fires `onClose`.
|
||||
* CHM-U-04 — pressing Escape fires `onClose`.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { flushSync, mount, unmount } from 'svelte';
|
||||
import ChangelogModal from './ChangelogModal.svelte';
|
||||
|
||||
let host: HTMLElement;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let instance: any;
|
||||
|
||||
function render(props: Record<string, unknown>) {
|
||||
host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
instance = mount(ChangelogModal, { target: host, props });
|
||||
return host;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
host = document.createElement('div');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (instance) unmount(instance);
|
||||
if (host?.parentNode) host.parentNode.removeChild(host);
|
||||
instance = null;
|
||||
});
|
||||
|
||||
describe('ChangelogModal component', () => {
|
||||
it('CHM-U-01: open=false renders nothing visible', () => {
|
||||
const root = render({ open: false, onClose: () => {} });
|
||||
expect(root.querySelector('[data-testid="changelog-modal"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('CHM-U-02: open=true renders the CHANGELOG h1 heading', () => {
|
||||
const root = render({ open: true, onClose: () => {} });
|
||||
const modal = root.querySelector('[data-testid="changelog-modal"]');
|
||||
expect(modal).not.toBeNull();
|
||||
const h1 = modal?.querySelector('h1');
|
||||
expect(h1).not.toBeNull();
|
||||
expect(h1?.textContent?.trim()).toBe('Changelog');
|
||||
});
|
||||
|
||||
it('CHM-U-03: click on overlay fires onClose', () => {
|
||||
const onClose = vi.fn();
|
||||
const root = render({ open: true, onClose });
|
||||
const overlay = root.querySelector('[data-testid="changelog-modal-overlay"]') as
|
||||
| HTMLElement
|
||||
| null;
|
||||
expect(overlay).not.toBeNull();
|
||||
overlay?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('CHM-U-04: Escape key fires onClose', () => {
|
||||
const onClose = vi.fn();
|
||||
render({ open: true, onClose });
|
||||
// $effect runs in a microtask after mount. flushSync drains pending
|
||||
// reactive effects so the keydown listener is attached before we
|
||||
// dispatch the event.
|
||||
flushSync();
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user