import { describe, expect, it } from 'vitest'; import { h, mount, mountCallback, nextTick, render, updateCallback, } from '@/index'; import type { WDom } from '@/types'; describe('cached VDOM identity', () => { it('runs component update callbacks on cache hits and leaves reused props intact', async () => { let bump = () => {}; let renders = 0; let updates = 0; let unmounts = 0; const props = Object.freeze({ title: 'cached' }); let cached = h('span', props, '0'); const App = mount(renew => { bump = renew; mountCallback(() => () => { unmounts++; }); updateCallback(() => { updates++; }); return () => { renders++; return cached; }; }); const host = document.createElement('div'); const dispose = render(, host); const span = host.querySelector('span'); for (let index = 0; index < 3; index++) { bump(); await nextTick(); } expect(renders).toBe(4); expect(updates).toBe(3); expect(host.querySelector('span')).toBe(span); expect(host.textContent).toBe('0'); cached = h('span', props, '1'); bump(); await nextTick(); expect(renders).toBe(5); expect(updates).toBe(4); expect(host.querySelector('span')).toBe(span); expect(host.textContent).toBe('1'); expect(props).toEqual({ title: 'cached' }); expect(unmounts).toBe(0); dispose(); expect(unmounts).toBe(1); }); it('keeps cached keyed subtree identity and child renew after moves and deletion', async () => { let rows = [1, 2, 3]; let bump = () => {}; const views = new Map(); const unmounted: number[] = []; const mounted: number[] = []; const Counter = mount<{ id: number }>((renew, props) => { let count = 0; mountCallback(() => { mounted.push(props.id); return () => unmounted.push(props.id); }); return () => ( ); }); const view = (id: number) => { if (!views.has(id)) { views.set( id,
); } return views.get(id) as WDom; }; const List = mount(renew => { bump = renew; return () =>
{rows.map(view)}
; }); const host = document.createElement('div'); const dispose = render(, host); const original = [...host.querySelectorAll('section')]; const update = async (next: number[]) => { rows = next; bump(); await nextTick(); }; await update([1, 2, 3]); await update([3, 1, 2]); expect([...host.querySelectorAll('section')]).toEqual([ original[2], original[0], original[1], ]); original[0].querySelector('button')?.click(); await nextTick(); expect(original[0].textContent).toBe('1:1'); expect(host.querySelector('[data-id="1"]')).toBe(original[0]); expect(mounted).toEqual([1, 2, 3]); expect(unmounted).toEqual([]); await update([3, 1]); expect([...host.querySelectorAll('section')]).toEqual([ original[2], original[0], ]); expect(unmounted).toEqual([2]); original[0].querySelector('button')?.click(); await nextTick(); expect(original[0].textContent).toBe('1:2'); dispose(); expect(unmounted).toEqual([2, 3, 1]); }); it('still deletes an identical node whose type becomes empty', async () => { const cached = (removed) as WDom; let bump = () => {}; const App = mount(renew => { bump = renew; return () => cached; }); const host = document.createElement('div'); const dispose = render(, host); expect(host.textContent).toBe('removed'); cached.type = null; bump(); await nextTick(); expect(host.innerHTML).toBe(''); dispose(); }); });