import { describe, expect, it } from 'vitest'; import * as core from '@/index'; import { Fragment, getComponentKey, h, mount, mountCallback, nextTick, portal, render, replaceWDom, getComponentSubInfo, updateCallback, } from '@/index'; import type { CompKey, WDom } from '@/types'; const isConcurrent = 'deferRender' in core; // Baseline contracts for DC-17; these also run on the unchanged concurrent core. describe('keyed placement proof baseline', () => { it('preserves children access order relative to original key-map reads', async () => { const trace: string[] = []; let refresh = () => {}; let phase = 'old'; const Parent = mount(renew => { refresh = renew; return () => { const source = phase; let children = [0, 1].map(key => h( 'b', { get key() { trace.push(`${source}:${key}`); return key; }, }, key ) ); const loop: WDom = { type: 'l', get children() { trace.push('children'); return children; }, set children(value) { children = value || []; }, }; return
{loop}
; }; }); const host = document.createElement('div'); const dispose = render(, host); const original = [...host.querySelectorAll('b')]; trace.length = 0; phase = 'next'; refresh(); await nextTick(); const lastOriginalKey = trace.indexOf('old:1'); expect(lastOriginalKey).toBeGreaterThan(-1); // Base reads the next children array after building the original key Map. // Concurrent captures its children before building that Map. expect(trace[lastOriginalKey + 1] === 'children').toBe(!isConcurrent); expect(host.textContent).toBe('01'); expect([...host.querySelectorAll('b')]).toEqual(original); dispose(); }); it('preserves same-order falsy, NaN, object and Symbol keys and local state', async () => { const keys: unknown[] = [0, false, '', NaN, {}, Symbol('row')]; let version = 0; let refresh = () => {}; const updates: (() => void)[] = []; const removed: number[] = []; const Row = mount<{ key?: unknown; id: number; version: number }>( (renew, props) => { const id = props.id; let local = 0; updates[id] = () => { local++; renew(); }; mountCallback(() => () => removed.push(id)); return () => ; } ); const App = mount(renew => { refresh = renew; return () => (
{keys.map((key, id) => ( ))}
); }); const host = document.createElement('div'); const dispose = render(, host); const inputs = [...host.querySelectorAll('input')]; for (version = 1; version <= 3; version++) { refresh(); await nextTick(); expect([...host.querySelectorAll('input')]).toEqual(inputs); expect(inputs.map(input => input.value)).toEqual( keys.map((_, id) => `${version}:${id}:0`) ); expect(removed).toEqual([]); } updates[0](); updates[5](); await nextTick(); expect(inputs[0].value).toBe('3:0:1'); expect(inputs[5].value).toBe('3:5:1'); dispose(); expect(removed.sort()).toEqual([0, 1, 2, 3, 4, 5]); }); it('consumes only the first duplicate even when order and length stay equal', async () => { let refresh = () => {}; let version = 0; let created = 0; const removed: number[] = []; const Row = mount<{ key?: unknown; label: string }>((_renew, props) => { const id = ++created; mountCallback(() => () => removed.push(id)); return () => {props.label}; }); const App = mount(renew => { refresh = renew; return () => (
{[0, 1].map(id => ( ))}
); }); const host = document.createElement('div'); const dispose = render(, host); const first = host.querySelector('b'); for (version = 1; version <= 2; version++) { refresh(); await nextTick(); expect(host.querySelector('b')).toBe(first); expect( [...host.querySelectorAll('b')].map(node => node.textContent) ).toEqual([`${version}:0`, `${version}:1`]); expect(removed).toEqual(version === 1 ? [2] : [2, 3]); } dispose(); expect(removed.sort()).toEqual([1, 2, 3, 4]); }); it('handles mixed keys, empty children, tag replacement and changed keys', async () => { let refresh = () => {}; let phase = 0; const App = mount(renew => { refresh = renew; return () => (
{phase === 0 ? [a, null, u, b] : phase === 1 ? [A, null, U, B] : phase === 2 ? [null, N, V, C] : []}
end
); }); const host = document.createElement('div'); const dispose = render(, host); const first = host.querySelector('b'); const unkeyed = host.querySelector('i'); for (phase = 1; phase <= 3; phase++) { refresh(); await nextTick(); expect(host.textContent).toBe(['', 'AUBend', 'NVCend', 'end'][phase]); if (phase < 3) expect(host.querySelector('i')).toBe(unkeyed); if (phase === 1) expect(host.querySelector('b')).toBe(first); } dispose(); }); it('preserves each core matching time when a pre-callback mutates a later key', async () => { let refresh = () => {}; let laterProps = { key: 'late', id: 'late' }; let updating = false; let instances = 0; const removed: number[] = []; const trace: string[] = []; const Row = mount<{ key?: unknown; id: string }>((_renew, props) => { const instance = ++instances; updateCallback(() => { trace.push(`pre:${props.id}`); if (props.id === 'first') laterProps.key = 'changed'; }); mountCallback(() => () => removed.push(instance)); return () => { if (updating) trace.push(`render:${props.id}:${instance}`); return {props.id}; }; }); const App = mount(renew => { refresh = renew; return () => { const first = ; const later = ; laterProps = later.props as typeof laterProps; return
{[first, later]}
; }; }); const host = document.createElement('div'); const dispose = render(, host); const previous = [...host.querySelectorAll('b')]; updating = true; refresh(); await nextTick(); // Concurrent prepares its key matches before running these callbacks; // base reads each later key only after the previous updater has run. expect(trace).toEqual( isConcurrent ? ['pre:first', 'render:first:1', 'pre:late', 'render:late:2'] : ['pre:first', 'render:first:1', 'render:late:3'] ); expect(host.querySelector('b')).toBe(previous[0]); expect(host.querySelectorAll('b')[1] === previous[1]).toBe(isConcurrent); expect(removed).toEqual(isConcurrent ? [] : [2]); dispose(); expect(removed.sort()).toEqual(isConcurrent ? [1, 2] : [1, 2, 3]); }); it('preserves keyed component identity across fragment output arity changes', async () => { let refresh = () => {}; let count = 1; const mounted: string[] = []; const Part = mount<{ key?: unknown; label: string }>((_renew, props) => { mountCallback(() => { mounted.push(props.label); }); return () => {props.label}; }); const Row = mount<{ key?: unknown; id: number }>((_renew, props) => () => ( <> {props.id} {Array.from({ length: count }, (_, i) => ( ))} )); const App = mount(renew => { refresh = renew; return () => (
{[1, 2].map(id => ( ))}
end
); }); const host = document.createElement('div'); const dispose = render(, host); const anchors = [...host.querySelectorAll('b')]; mounted.length = 0; count = 2; refresh(); await nextTick(); expect(host.textContent).toBe('11:01:122:02:1end'); expect(mounted).toEqual(['1:1', '2:1']); count = 0; refresh(); await nextTick(); expect(host.textContent).toBe('12end'); expect([...host.querySelectorAll('b')]).toEqual(anchors); dispose(); }); it('keeps portal hosts and external siblings while keyed contents change', async () => { let refresh = () => {}; let version = 0; const host = document.createElement('div'); const external = document.createElement('aside'); const after = document.createElement('footer'); after.textContent = 'external'; host.appendChild(after); const App = mount(renew => { refresh = renew; return () => ( <> {[ {version}, portal({version}, external), {version + 1}, ]} ); }); const dispose = render(, host, after); const initial = [...host.querySelectorAll('b')]; for (version = 1; version <= 2; version++) { refresh(); await nextTick(); expect([...host.querySelectorAll('b')]).toEqual(initial); expect(host.textContent).toBe(`${version}${version + 1}external`); expect(external.textContent).toBe(String(version)); expect(host.lastChild).toBe(after); } dispose(); expect(host.textContent).toBe('external'); }); it('does not reinsert focused same-order inputs and permits later self renew', async () => { let refresh = () => {}; let local = 0; const updates: (() => void)[] = []; const Row = mount<{ key?: unknown; id: number }>((renew, props) => { updates[props.id] = renew; return () => ; }); const App = mount(renew => { refresh = renew; return () => (
{[0, 1].map(id => ( ))}
); }); const host = document.createElement('div'); document.body.appendChild(host); const dispose = render(, host); try { const input = host.querySelectorAll('input')[1]; input.focus(); input.setSelectionRange(1, 2); refresh(); await nextTick(); expect(document.activeElement).toBe(input); expect(input.selectionStart).toBe(1); local++; updates[1](); await nextTick(); expect(input.value).toBe('1:1'); expect(document.activeElement).toBe(input); } finally { dispose(); host.remove(); } }); it('keeps left-to-right post callbacks and a self renew queued from them', async () => { const trace: string[] = []; let refresh = () => {}; let requeue = false; const Row = mount<{ key?: unknown; id: number }>((renew, props) => { let count = 0; updateCallback(() => { trace.push(`pre:${props.id}`); return () => { trace.push(`post:${props.id}:${count}`); if (props.id === 0 && requeue) { requeue = false; count++; renew(); } }; }); return () => {`${props.id}:${count}`}; }); const App = mount(renew => { refresh = renew; return () => (
{[0, 1].map(id => ( ))}
); }); const host = document.createElement('div'); const dispose = render(, host); requeue = true; refresh(); await nextTick(); expect(trace).toEqual([ 'pre:0', 'pre:1', 'post:0:0', 'post:1:0', 'pre:0', 'post:0:1', ]); expect(host.textContent).toBe('0:11:0'); dispose(); }); it('recovers a queued sibling and later same-order updates after a diff throws', async () => { let fail = false; let refresh = () => {}; let siblingRefresh = () => {}; let version = 0; const Row = mount<{ key?: unknown; id: number }>((_renew, props) => () => ( {props.id} )); const Fail = mount(() => () => { if (fail) throw Error('placement proof diff failure'); return safe; }); const App = mount(renew => { refresh = renew; return () => (
{[0, 1].map(id => ( ))}
); }); const Sibling = mount(renew => { siblingRefresh = renew; return () => {version}; }); const host = document.createElement('div'); const dispose = render(
, host ); const errors: unknown[] = []; const onError = (error: unknown) => errors.push(error); process.on('uncaughtException', onError); try { fail = true; refresh(); version++; siblingRefresh(); await new Promise(resolve => setTimeout(resolve)); expect(errors).toEqual([Error('placement proof diff failure')]); expect(host.querySelector('em')?.textContent).toBe('1'); // Failed parent trees are not automatically rolled back by either core. // Verify recovery on the still-live sibling, without inventing that contract. fail = false; version++; siblingRefresh(); await nextTick(); expect(host.querySelector('em')?.textContent).toBe('2'); } finally { process.off('uncaughtException', onError); dispose(); } }); it('allows synchronous wrapper replacement from a post callback', async () => { let refresh = () => {}; let nestedKey: CompKey | null = null; let count = 0; let replace = false; const Nested = mount<{ label?: unknown }>((_renew, props) => { nestedKey = getComponentKey(); return () => {`${props.label}:${count}`}; }); const Row = mount<{ key?: unknown; id: number }>((_renew, props) => { updateCallback(() => () => { if (props.id === 0 && replace) { replace = false; count++; const previous = getComponentSubInfo(nestedKey!, 'vd')!.value!; replaceWDom( Nested, previous.compProps!, previous.compChild!, previous ); } }); return () => {props.id}; }); const App = mount(renew => { refresh = renew; return () => (
{[0, 1].map(id => ( ))}
); }); const host = document.createElement('div'); const dispose = render(, host); replace = true; refresh(); await nextTick(); expect(host.textContent).toBe('01n:1'); refresh(); await nextTick(); expect(host.textContent).toBe('01n:1'); dispose(); }); });