// @vitest-environment happy-dom import { act, createElement } 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 { remoteViews, renderWireTree, type RemoteViews } from './client' class ItemUi extends ui('Item', { props: S.Struct({ label: S.String }) }) {} class ItemComp extends Composition.make('Item', { title: 'Item', ui: ItemUi })() {} class ItemSlot extends slot('Item')() {} class ListUi extends ui('List', { props: S.Struct({ mode: S.String }), slots: { Item: ItemSlot }, }) {} const node = (over: Partial & Pick): WireNode => ({ parentId: null, childIndex: 0, slot: null, key: null, props: [], ...over, }) const ItemView = Ui.make(ItemUi, ({ label }) => createElement('span', { 'data-item': label }, label), ) const tree: ReadonlyArray = [ node({ id: '0', name: 'List', props: [{ _tag: 'Data', name: 'mode', value: 'keyed' }] }), node({ id: '0.Item.0', name: 'Item', parentId: '0', slot: 'Item', key: 'a', props: [{ _tag: 'Data', name: 'label', value: 'a' }], }), node({ id: '0.Item.1', name: 'Item', parentId: '0', slot: 'Item', key: 'b', props: [{ _tag: 'Data', name: 'label', value: 'b' }], }), node({ id: '0.Item.2', name: 'Item', parentId: '0', slot: 'Item', key: 'c', props: [{ _tag: 'Data', name: 'label', value: 'c' }], }), ] type Contract = { List: typeof ListUi; Item: typeof ItemUi } const renderWith = async ( ListView: RemoteViews['List'], wire: ReadonlyArray, ): Promise => { const config = { invoke: () => Promise.resolve(), views: remoteViews({ List: ListView, Item: ItemView }), } const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) await act(async () => { root.render(renderWireTree(wire, config)) }) return container } test('a keyed slot renders ONE matching child per call site, not the whole list', async () => { const ListView = Ui.make(ListUi, (_props, slots) => createElement( 'div', null, createElement( 'section', { 'data-section': 'one' }, createElement(slots.Item, { slotKey: 'a' }), ), createElement( 'section', { 'data-section': 'two' }, createElement(slots.Item, { slotKey: 'b' }), ), ), ) const container = await renderWith(ListView, tree) expect(container.querySelectorAll('[data-item]').length).toBe(2) const one = container.querySelector('[data-section="one"]') const two = container.querySelector('[data-section="two"]') expect(one?.querySelectorAll('[data-item]').length).toBe(1) expect(one?.querySelector('[data-item]')?.getAttribute('data-item')).toBe('a') expect(two?.querySelector('[data-item]')?.getAttribute('data-item')).toBe('b') expect(container.querySelector('[data-item="c"]')).toBeNull() }) test('an un-keyed slot still renders ALL its children (singleton/back-compat path)', async () => { const ListView = Ui.make(ListUi, (_props, slots) => createElement('div', null, createElement(slots.Item)), ) const container = await renderWith(ListView, tree) expect(container.querySelectorAll('[data-item]').length).toBe(3) expect( [...container.querySelectorAll('[data-item]')].map((el) => el.getAttribute('data-item')), ).toEqual(['a', 'b', 'c']) }) test('a keyed call with no matching child renders nothing (no fallback to the full list)', async () => { const ListView = Ui.make(ListUi, (_props, slots) => createElement('div', null, createElement(slots.Item, { slotKey: 'missing' })), ) const container = await renderWith(ListView, tree) expect(container.querySelectorAll('[data-item]').length).toBe(0) })