// @vitest-environment jsdom // // `/` — the reactive notes page, driven through `makeVoltroTestClient` // (`@voltro/testing/client`) rather than a `vi.mock` of anything. // // WHY THE HARNESS HERE. The old suite faked `src/lib/api` — the app's own typed // binding — with the argument that mocking `@voltro/client` underneath would be // the wrong seam. That argument was right about the seam and reached the wrong // conclusion, because the harness does not mock `@voltro/client` at all: it // PROVIDES the runtime context the real hooks read. So `createHooks` runs, the // real `useSubscription` / `useMutation` run, and the fake stops at the // transport — one layer below the binding rather than on top of it. // // What that buys, concretely: the page is the framework's flagship example of // the reactive loop, and every "different data" case here used to be a fresh // mount with a different mock return. Nothing pushed a delta into a MOUNTED // tree, which is the one behaviour the page exists to demonstrate. The last two // cases do, via `setSubscription`. import { afterEach, beforeEach, describe, expect, test } from 'vitest' import { act, createElement, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { I18nProvider } from '@voltro/i18n' import { makeVoltroTestClient, type VoltroTestClient } from '@voltro/testing/client' import enCatalog from '../locales/en' import IndexPage from './page' ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true const LIST = 'notes.list' let container: HTMLDivElement let root: Root let client: VoltroTestClient const note = (over: Partial<{ id: string; title: string; body: string; optimistic: boolean }> = {}) => ({ id: over.id ?? 'n_1', title: over.title ?? 'first', body: over.body ?? '', done: false, tenantId: 'acme', createdAt: new Date(0), ...(over.optimistic === true ? { optimistic: true } : {}), }) /** `notes.create` resolves by default; a case that needs the failure path * overrides it. The harness records every call on `client.calls`. */ const harness = ( subscriptions: Readonly> = {}, create: (input: unknown) => unknown = () => undefined, ): VoltroTestClient => makeVoltroTestClient({ subscriptions, mutations: { 'notes.create': create } }) // The page uses /useTFn, so it needs an . Wrap with the real // English catalog (the framework auto-wires this provider in the running app). const render = (): void => { container = document.createElement('div') document.body.appendChild(container) act(() => { root = createRoot(container) root.render( createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: createElement(client.Provider, null, createElement(IndexPage) as ReactNode), }), ) }) } const setInput = (el: HTMLInputElement | HTMLTextAreaElement, value: string): void => { const proto = el instanceof window.HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype const setter = Object.getOwnPropertyDescriptor(proto, 'value')!.set! act(() => { setter.call(el, value) el.dispatchEvent(new Event('input', { bubbles: true })) }) } const submit = async (): Promise => { const form = container.querySelector('form') as HTMLFormElement await act(async () => { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) }) } beforeEach(() => { client = harness() }) afterEach(() => { if (root) act(() => root.unmount()) container?.remove() document.body.innerHTML = '' }) describe('index — subscription states', () => { test('shows the loading hint before the first snapshot', () => { // A tag with no fixture IS the cold state — no stub had to invent one. render() expect(container.textContent).toContain('loading…') expect(container.querySelectorAll('li')).toHaveLength(0) }) test('shows the empty state once a snapshot arrives with no rows', () => { client = harness({ [LIST]: [] }) render() expect(container.textContent).toContain('No notes yet') }) test('renders each note with its title and body', () => { client = harness({ [LIST]: [note({ id: 'n_1', title: 'first', body: 'body one' }), note({ id: 'n_2', title: 'second' })] }) render() expect(container.querySelectorAll('li')).toHaveLength(2) expect(container.textContent).toContain('first') expect(container.textContent).toContain('body one') expect(container.textContent).toContain('second') }) test('renders optimistic rows faintly + italicised, with the pending count', () => { client = harness({ [LIST]: [] }) render() act(() => { client.setSubscription(LIST, [note({ id: 'n_opt', title: 'pending', optimistic: true })], { pendingPatches: 1 }) }) const li = container.querySelector('li')! expect(li.style.opacity).toBe('0.5') expect(li.style.fontStyle).toBe('italic') expect(container.textContent).toContain('1 pending') }) test('renders the subscription error banner when the stream fails', () => { render() act(() => { client.failSubscription(LIST, 'boom') }) expect(container.textContent).toContain('subscription error') expect(container.textContent).toContain('boom') }) }) describe('index — create form', () => { test('the Add button is disabled until a non-empty title is typed', () => { client = harness({ [LIST]: [] }) render() const button = container.querySelector('button[type="submit"]') as HTMLButtonElement expect(button.disabled).toBe(true) setInput(container.querySelector('input') as HTMLInputElement, 'a note') expect(button.disabled).toBe(false) }) test('submitting calls notes.create with the trimmed title + body + tenant, then clears', async () => { client = harness({ [LIST]: [] }) render() setInput(container.querySelector('input') as HTMLInputElement, ' my title ') setInput(container.querySelector('textarea') as HTMLTextAreaElement, ' my body ') await submit() expect(client.calls).toEqual([ { kind: 'mutation', tag: 'notes.create', input: { tenantId: 'acme', title: 'my title', body: 'my body' } }, ]) expect((container.querySelector('input') as HTMLInputElement).value).toBe('') expect((container.querySelector('textarea') as HTMLTextAreaElement).value).toBe('') }) test('a whitespace-only title does not fire a mutation', async () => { client = harness({ [LIST]: [] }) render() setInput(container.querySelector('input') as HTMLInputElement, ' ') await submit() expect(client.calls).toEqual([]) }) test('a REJECTED create leaves the form cleared and the list untouched', async () => { client = harness({ [LIST]: [] }, () => { throw new Error('TenantMismatch') }) render() setInput(container.querySelector('input') as HTMLInputElement, 'nope') await submit() expect(client.calls).toHaveLength(1) expect(container.textContent).toContain('No notes yet') }) }) // The reactive loop itself — a server delta into a MOUNTED tree. This is what // the page is a demonstration of, and what a per-mount mock cannot express. describe('index — the reactive loop', () => { test('a delta pushed by the server re-renders the list in place', () => { client = harness({ [LIST]: [note({ id: 'n_1', title: 'first' })] }) render() expect(container.querySelectorAll('li')).toHaveLength(1) act(() => { client.setSubscription(LIST, [note({ id: 'n_1', title: 'first' }), note({ id: 'n_2', title: 'second' })]) }) expect(container.querySelectorAll('li')).toHaveLength(2) expect(container.textContent).toContain('second') }) test('the confirming delta clears the optimistic styling AND the pending count', () => { client = harness({ [LIST]: [] }) render() act(() => { client.setSubscription(LIST, [note({ id: 'n_tmp', title: 'mine', optimistic: true })], { pendingPatches: 1 }) }) expect(container.querySelector('li')!.style.opacity).toBe('0.5') expect(container.textContent).toContain('1 pending') // The server confirms: the real row replaces the optimistic one. act(() => { client.setSubscription(LIST, [note({ id: 'n_1', title: 'mine' })]) }) expect(container.querySelector('li')!.style.opacity).toBe('1') expect(container.textContent).not.toContain('1 pending') }) })