// @vitest-environment happy-dom import { act, createElement, useState } from 'react' import { useSyncExternalStore } from 'react' import { createRoot } from 'react-dom/client' import { expect, test } from 'vitest' import { Schema as S } from 'effect' import { Composition, slot, Ui, ui } from '@playfast/reform' import type { WireNode } from '@playfast/reform/internal' import type { ServerMessage } from './transport' import { connect } from './transport' import { inMemoryTransportPair } from './memory' import { remoteViews } from './client' class ChildUi extends ui('Child', { props: S.Struct({}) }) {} class ChildComp extends Composition.make('Child', { title: 'Child', ui: ChildUi })() {} class MainSlot extends slot('Main')() {} class HostUi extends ui('Host', { props: S.Struct({ count: S.Number }), slots: { Main: MainSlot }, }) {} const node = (over: Partial & Pick): WireNode => ({ parentId: null, childIndex: 0, slot: null, key: null, props: [], ...over, }) test('a background patch on the PARENT keeps its slot child mounted (local useState survives)', async () => { const { server: serverTransport, client: clientTransport } = inMemoryTransportPair< ServerMessage, unknown >() const mounts = { n: 0 } const ChildView = Ui.make(ChildUi, () => { const [id] = useState(() => { mounts.n += 1 return `mount-${mounts.n}` }) return createElement('span', { 'data-child-id': id }, id) }) const HostView = Ui.make(HostUi, ({ count }, slots) => createElement('div', { 'data-count': String(count) }, createElement(slots.Main)), ) const client = connect({ transport: clientTransport, views: remoteViews<{ Host: typeof HostUi; Child: typeof ChildUi }>({ Host: HostView, Child: ChildView, }), }) const Root = (): ReturnType => { useSyncExternalStore(client.subscribe, client.snapshot, client.snapshot) return client.node() } const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) try { await act(async () => { root.render(createElement(Root)) }) await act(async () => { serverTransport.send({ _tag: 'Snapshot', tree: [ node({ id: '0', name: 'Host', props: [{ _tag: 'Data', name: 'count', value: 0 }] }), node({ id: '0.Main.0', name: 'Child', parentId: '0', slot: 'Main' }), ], }) }) expect(container.querySelector('[data-count]')?.getAttribute('data-count')).toBe('0') const childIdBefore = container.querySelector('[data-child-id]')?.getAttribute('data-child-id') expect(childIdBefore).toBe('mount-1') await act(async () => { serverTransport.send({ _tag: 'Patches', patches: [ { _tag: 'Upsert', node: node({ id: '0', name: 'Host', props: [{ _tag: 'Data', name: 'count', value: 7 }], }), }, ], }) }) expect(container.querySelector('[data-count]')?.getAttribute('data-count')).toBe('7') expect(container.querySelector('[data-child-id]')?.getAttribute('data-child-id')).toBe( childIdBefore, ) expect(mounts.n).toBe(1) } finally { await act(async () => { root.unmount() }) client.dispose() } })