import { describe, expect, it } from 'vitest'; import { html, render } from 'lit'; import './counter-button.js'; import type { CounterButton } from './counter-button.js'; /** * Mounts a template and waits for the element's first render to finish. * * Custom elements upgrade and render asynchronously, so a test that asserts * immediately after mounting sees an empty shadow root. Awaiting * `updateComplete` is the supported way to synchronise with Lit. */ async function mount(): Promise { const host = document.createElement('div'); document.body.append(host); render(html`Clicks`, host); const element = host.querySelector('counter-button'); if (!element) throw new Error('counter-button did not mount'); await element.updateComplete; return element; } function buttonOf(element: CounterButton): HTMLButtonElement { const button = element.shadowRoot?.querySelector('button'); if (!button) throw new Error('button not found in shadow root'); return button; } describe('counter-button', () => { it('renders its initial count', async () => { const element = await mount(); expect(element.count).toBe(0); expect(buttonOf(element).textContent).toContain('0'); }); it('increments when clicked', async () => { const element = await mount(); buttonOf(element).click(); await element.updateComplete; expect(element.count).toBe(1); expect(buttonOf(element).textContent).toContain('1'); }); it('honours the step-by attribute', async () => { const element = await mount(); element.setAttribute('step-by', '5'); await element.updateComplete; buttonOf(element).click(); await element.updateComplete; expect(element.count).toBe(5); }); it('dispatches a composed count-changed event that escapes the shadow root', async () => { const element = await mount(); // Listening on document proves the event is both bubbling and composed; // a non-composed event would stop at the shadow boundary. const seen = new Promise((resolve) => { document.addEventListener( 'count-changed', (event) => resolve((event as CustomEvent<{ count: number }>).detail.count), { once: true }, ); }); buttonOf(element).click(); expect(await seen).toBe(1); }); it('reflects count back to an attribute', async () => { const element = await mount(); buttonOf(element).click(); await element.updateComplete; expect(element.getAttribute('count')).toBe('1'); }); });