diff --git a/src/host/config.ts b/src/host/config.ts index a8e33d5..7f8c339 100644 --- a/src/host/config.ts +++ b/src/host/config.ts @@ -2,7 +2,8 @@ import type { UNode, UElement, URoot, ComponentFn, UComponent } from "../core/sc import { isURoot, isUPrimitive } from "../core/schema.js"; import { Context } from "../core/context.js"; import type { Fiber } from "./fiber.js"; -import { reconcileProps, commitEffects } from "./reconcile.js"; +import { reconcileProps, reconcileChildren, commitMutations } from "./reconcile.js"; +import type { CommitContext } from "./reconcile.js"; export interface HostConfig { name: string; @@ -116,6 +117,130 @@ export function createRoot( return fiber; } + function createFiberForInsert(node: UNode, parentFiber: Fiber): Fiber { + if (isUPrimitive(node)) { + const text = node === null ? "" : String(node); + const t = host.createTextInstance(text, ctx, parentFiber.instance); + host.emit?.("instance.create", `text_${Date.now()}`, { kind: "text", value: text }); + return { + instance: t, + tag: "#text", + props: { text }, + key: undefined, + children: [], + parent: parentFiber, + effect: null, + signalDisposers: [], + prevProps: null, + }; + } + + const el = node as UElement; + + if (typeof el.type === "function") { + const component = el.type as ComponentFn; + const out = component({ ...el.props, children: el.children }); + host.emit?.("component.invoke", `comp_${Date.now()}`, { type: (component as unknown as UComponent).displayName ?? "anonymous" }); + return createFiberForInsert(out, parentFiber); + } + + const tag = el.type as TTag; + const inst = host.createInstance(tag, el.props as Record, ctx, parentFiber.instance); + host.emit?.("instance.create", `${tag}_${Date.now()}`, { kind: "element", tag, props: el.props }); + + const fiber: Fiber = { + instance: inst, + tag, + props: el.props as Record, + key: el.key, + children: [], + parent: parentFiber, + effect: null, + signalDisposers: [], + prevProps: null, + }; + + for (const child of el.children) { + const childFiber = createFiberForInsert(child, fiber); + if (childFiber) { + host.appendChild(inst, childFiber.instance, ctx); + fiber.children.push(childFiber); + } + } + + return fiber; + } + + function resolveUNode(node: UNode): UNode[] { + if (node == null || node === false) return []; + if (isUPrimitive(node)) return [node]; + if (isURoot(node)) { + const result: UNode[] = []; + for (const child of (node as URoot).children) { + result.push(...resolveUNode(child)); + } + return result; + } + const el = node as UElement; + if (typeof el.type === "function") { + const component = el.type as ComponentFn; + const out = component({ ...el.props, children: el.children }); + return resolveUNode(out); + } + return [node]; + } + + function reconcileNode(fiber: Fiber, nextNode: UNode): void { + if (isUPrimitive(nextNode)) { + if (fiber.tag === "#text") { + reconcileProps(fiber, nextNode, host as HostConfig, ctx as unknown); + } + return; + } + + if (isURoot(nextNode)) { + const rootChildren = (nextNode as URoot).children; + const classification = reconcileChildren( + fiber.children, + rootChildren, + ); + for (const m of classification.matched) { + reconcileProps(m.oldFiber, m.newChild, host as HostConfig, ctx as unknown); + } + const commitCtx: CommitContext = { + host: host as HostConfig, + ctx: ctx as unknown, + createFiber: (node, parent) => createFiberForInsert(node, parent), + }; + commitMutations(fiber, classification, commitCtx); + return; + } + + const el = nextNode as UElement; + + if (typeof el.type === "function") { + const component = el.type as ComponentFn; + const out = component({ ...el.props, children: el.children }); + reconcileNode(fiber, out); + return; + } + + if (fiber.tag !== el.type) return; + + reconcileProps(fiber, el, host as HostConfig, ctx as unknown); + + const classification = reconcileChildren(fiber.children, el.children); + for (const m of classification.matched) { + reconcileProps(m.oldFiber, m.newChild, host as HostConfig, ctx as unknown); + } + const commitCtx: CommitContext = { + host: host as HostConfig, + ctx: ctx as unknown, + createFiber: (node, parent) => createFiberForInsert(node, parent), + }; + commitMutations(fiber, classification, commitCtx); + } + return { host, ctx, @@ -125,13 +250,23 @@ export function createRoot( render(node: UNode) { if (this.rootFiber) { const payloadChildren = isURoot(node) ? (node as URoot).children : [node]; - for (let i = 0; i < payloadChildren.length; i++) { - const childFiber = this.rootFiber.children[i]; - if (childFiber) { - reconcileProps(childFiber, payloadChildren[i]!, host as HostConfig, ctx as unknown); - } + const resolvedChildren: UNode[] = []; + for (const child of payloadChildren) { + resolvedChildren.push(...resolveUNode(child)); } - commitEffects(this.rootFiber, host as HostConfig, ctx as unknown); + const classification = reconcileChildren( + this.rootFiber.children, + resolvedChildren, + ); + for (const m of classification.matched) { + reconcileNode(m.oldFiber, m.newChild); + } + const commitCtx: CommitContext = { + host: host as HostConfig, + ctx: ctx as unknown, + createFiber: (node, parent) => createFiberForInsert(node, parent), + }; + commitMutations(this.rootFiber, classification, commitCtx); host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length }); return; } diff --git a/src/host/reconcile.ts b/src/host/reconcile.ts index 57f781d..152a0f9 100644 --- a/src/host/reconcile.ts +++ b/src/host/reconcile.ts @@ -4,6 +4,12 @@ import type { HostConfig } from "./config.js"; import type { UNode, UElement } from "../core/schema.js"; import { isUPrimitive, isURoot, isUElement } from "../core/schema.js"; +export interface CommitContext { + host: HostConfig; + ctx: unknown; + createFiber: (node: UNode, parentFiber: Fiber) => Fiber; +} + interface PendingUpdate { fiber: Fiber; nextNode: UNode; @@ -320,4 +326,125 @@ export function reconcileChildren( } return { matched, added, removed, moves }; +} + +export function commitMutations( + parentFiber: Fiber, + classification: ChildClassification, + commitCtx: CommitContext, +): void { + const { host, ctx, createFiber } = commitCtx; + const parentInst = parentFiber.instance; + + const movedSet = new Set>(classification.moves.values()); + + // Map matched index (position in classification.matched[]) for quick lookup + const matchedArrayIndex = new Map, number>(); + for (let i = 0; i < classification.matched.length; i++) { + matchedArrayIndex.set(classification.matched[i]!, i); + } + + // Build maps keyed by new-children position + const matchedByNewIndex = new Map>(); + for (const m of classification.matched) { + matchedByNewIndex.set(m.index, m); + } + const addedByIndex = new Map(); + for (const { newChild, index } of classification.added) { + addedByIndex.set(index, newChild); + } + + // Collect all positions in new-children order + const allIndices = new Set(); + for (const m of classification.matched) allIndices.add(m.index); + for (const a of classification.added) allIndices.add(a.index); + const sortedIndices = [...allIndices].sort((a, b) => a - b); + + // Phase 1: Removes — reverse order (children before parents, bottom-up) + for (let i = classification.removed.length - 1; i >= 0; i--) { + const fiber = classification.removed[i]!; + host.removeChild?.(parentInst as never, fiber.instance as never, ctx as never); + } + + // Build new children array and determine placement actions + const newChildren: Fiber[] = []; + type Placement = { fiber: Fiber; beforeFiber: Fiber | null; kind: "insert" | "move" }; + const placements: Placement[] = []; + + // Create fibers for added children first (so they're available for before-lookup) + const addedFibers = new Map>(); + for (const { index } of classification.added) { + const newChild = addedByIndex.get(index)!; + const fiber = createFiber(newChild, parentFiber); + addedFibers.set(index, fiber); + } + + // Build the final children array + for (const idx of sortedIndices) { + const addedChild = addedByIndex.get(idx); + if (addedChild !== undefined) { + const fiber = addedFibers.get(idx)!; + newChildren.push(fiber); + // Find before: next fiber in newChildren that is "staying" (not moved, not just inserted) + const before = findNextStayingFiber(sortedIndices, matchedByNewIndex, addedByIndex, movedSet, idx); + placements.push({ fiber, beforeFiber: before, kind: "insert" }); + continue; + } + + const match = matchedByNewIndex.get(idx); + if (match !== undefined) { + const fiber = match.oldFiber; + newChildren.push(fiber); + const mIdx = matchedArrayIndex.get(match)!; + if (classification.moves.has(mIdx)) { + const before = findNextStayingFiber(sortedIndices, matchedByNewIndex, addedByIndex, movedSet, idx); + placements.push({ fiber, beforeFiber: before, kind: "move" }); + } + } + } + + // Phase 2: Inserts + Moves — left-to-right + for (const { fiber, beforeFiber } of placements) { + if (host.insertBefore && beforeFiber) { + host.insertBefore( + parentInst as never, + fiber.instance as never, + beforeFiber.instance as never, + ctx as never, + ); + } else { + host.appendChild(parentInst as never, fiber.instance as never, ctx as never); + } + } + + // Update fiber tree + parentFiber.children = newChildren; + for (const fiber of classification.removed) { + fiber.parent = null; + } + + // Phase 3: Updates — top-down (parent before child) via commitEffects + commitEffects(parentFiber, host as HostConfig, ctx); +} + +function findNextStayingFiber( + sortedIndices: number[], + matchedByNewIndex: Map>, + addedByIndex: Map, + movedSet: Set>, + currentIdx: number, +): Fiber | null { + const posInSorted = sortedIndices.indexOf(currentIdx); + for (let p = posInSorted + 1; p < sortedIndices.length; p++) { + const nextIdx = sortedIndices[p]!; + const isAdded = addedByIndex.has(nextIdx); + const match = matchedByNewIndex.get(nextIdx); + if (!isAdded && match !== undefined) { + const isMoved = movedSet.has(match.oldFiber); + if (!isMoved) { + return match.oldFiber; + } + } + } + return null; } \ No newline at end of file diff --git a/src/mod.ts b/src/mod.ts index e2c3f30..6f1550d 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -21,8 +21,8 @@ export type { HostConfig, Root } from "./host/config.js"; export type { Fiber, Effect } from "./host/fiber.js"; -export { scheduleUpdate, flushUpdates, reconcileProps, commitEffects, wireSignalToFiber, resetUpdateQueue, reconcileChildren, longestIncreasingSubsequence } from "./host/reconcile.js"; -export type { MatchedChild, ChildClassification } from "./host/reconcile.js"; +export { scheduleUpdate, flushUpdates, reconcileProps, commitEffects, commitMutations, wireSignalToFiber, resetUpdateQueue, reconcileChildren, longestIncreasingSubsequence } from "./host/reconcile.js"; +export type { MatchedChild, ChildClassification, CommitContext } from "./host/reconcile.js"; export { TransformRegistry, childCtx, matchesSchema, ctx as transformCtx } from "./transform/registry.js"; export type { TransformContext, TransformFn, TransformRule } from "./transform/registry.js"; \ No newline at end of file diff --git a/test/commit-mutations.test.ts b/test/commit-mutations.test.ts new file mode 100644 index 0000000..a4d6b0f --- /dev/null +++ b/test/commit-mutations.test.ts @@ -0,0 +1,382 @@ +import { describe, it, expect, vi } from "vitest"; +import { h } from "../src/core/h.js"; +import { createRoot as createHostRoot } from "../src/host/config.js"; +import type { HostConfig } from "../src/host/config.js"; +import { commitMutations, reconcileChildren } from "../src/host/reconcile.js"; +import type { CommitContext, ChildClassification } from "../src/host/reconcile.js"; +import type { Fiber } from "../src/host/fiber.js"; + +function makeHost(): { + host: HostConfig>; + operations: string[]; + instances: { tag: string; props: Record }[]; +} { + const operations: string[] = []; + const instances: { tag: string; props: Record }[] = []; + + const host: HostConfig> = { + name: "test", + createRootContext: () => ({}), + createInstance: (tag, props) => { + const id = `${tag}_${instances.length}`; + instances.push({ tag, props }); + return id; + }, + createTextInstance: (text) => { + return `text:${text}`; + }, + appendChild: (parent, child) => { + operations.push(`appendChild(${parent}, ${child})`); + }, + insertBefore: (parent, child, before) => { + operations.push(`insertBefore(${parent}, ${child}, ${before})`); + }, + removeChild: (parent, child) => { + operations.push(`removeChild(${parent}, ${child})`); + }, + prepareUpdate: (_instance, _tag, prevProps, nextProps) => { + const changed = Object.keys(nextProps).some( + (k) => prevProps[k] !== nextProps[k], + ) || Object.keys(prevProps).some( + (k) => !(k in nextProps), + ); + return changed ? { prevProps, nextProps } : null; + }, + commitUpdate: (instance, _payload, _tag, _prevProps, nextProps) => { + operations.push(`commitUpdate(${instance})`); + }, + }; + return { host, operations, instances }; +} + +describe("commitMutations", () => { + it("adding a child calls host.appendChild", () => { + const { host, operations } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a" }, "A"))); + + operations.length = 0; + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"))); + + expect(operations.some(op => op.includes("appendChild") || op.includes("insertBefore"))).toBe(true); + }); + + it("removing a child calls host.removeChild", () => { + const { host, operations } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"))); + + operations.length = 0; + root.render(h("ul", null, h("li", { key: "a" }, "A"))); + + expect(operations.some(op => op.includes("removeChild"))).toBe(true); + }); + + it("reordering children calls host.insertBefore for moved children", () => { + const { host, operations } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"), h("li", { key: "c" }, "C"))); + + operations.length = 0; + root.render(h("ul", null, h("li", { key: "c" }, "C"), h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"))); + + expect(operations.some(op => op.includes("insertBefore"))).toBe(true); + }); + + it("mixed add+remove+update operations apply in correct order", () => { + const { host, operations } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a", color: "red" }, "A"), h("li", { key: "b" }, "B"))); + + operations.length = 0; + root.render(h("ul", null, h("li", { key: "a", color: "blue" }, "A"), h("li", { key: "c" }, "C"))); + + const removeIdx = operations.findIndex(op => op.includes("removeChild")); + const insertIdx = operations.findIndex(op => op.includes("appendChild") || op.includes("insertBefore")); + const updateIdx = operations.findIndex(op => op.includes("commitUpdate")); + + if (removeIdx !== -1 && insertIdx !== -1) { + expect(removeIdx).toBeLessThan(insertIdx); + } + if (insertIdx !== -1 && updateIdx !== -1) { + expect(insertIdx).toBeLessThan(updateIdx); + } + }); + + it("insertBefore falls back to appendChild if before is null", () => { + const hostWithoutInsertBefore: HostConfig> = { + name: "test", + createRootContext: () => ({}), + createInstance: (tag) => `${tag}_0`, + createTextInstance: (text) => `text:${text}`, + appendChild: (_parent, _child) => {}, + removeChild: (_parent, _child) => {}, + prepareUpdate: () => null, + }; + const root = createHostRoot(hostWithoutInsertBefore, {}); + root.render(h("ul", null, h("li", { key: "a" }, "A"))); + + expect(() => { + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"))); + }).not.toThrow(); + }); + + it("fiber tree updated: new children added, removed children pruned", () => { + const { host } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"))); + + const ulFiber = root.rootFiber!.children[0]!; + expect(ulFiber.children.length).toBe(2); + expect(ulFiber.children[0]!.key).toBe("a"); + expect(ulFiber.children[1]!.key).toBe("b"); + + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "c" }, "C"))); + + expect(ulFiber.children.length).toBe(2); + const keys = ulFiber.children.map(c => c.key); + expect(keys).toContain("a"); + expect(keys).toContain("c"); + expect(keys).not.toContain("b"); + }); + + it("fiber tree updated: moved children reordered", () => { + const { host } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"), h("li", { key: "c" }, "C"))); + + root.render(h("ul", null, h("li", { key: "c" }, "C"), h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"))); + + const ulFiber = root.rootFiber!.children[0]!; + const keys = ulFiber.children.map(c => c.key); + expect(keys).toEqual(["c", "a", "b"]); + }); + + it("removes are committed before inserts and moves", () => { + const { host, operations } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"))); + + operations.length = 0; + root.render(h("ul", null, h("li", { key: "c" }, "C"), h("li", { key: "a" }, "A"))); + + const removeIdx = operations.findIndex(op => op.includes("removeChild")); + const insertIdx = operations.findIndex(op => op.includes("appendChild") || op.includes("insertBefore")); + + if (removeIdx !== -1 && insertIdx !== -1) { + expect(removeIdx).toBeLessThan(insertIdx); + } + }); + + it("updates are committed after inserts and moves", () => { + const { host, operations } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a", color: "red" }, "A"))); + + operations.length = 0; + root.render(h("ul", null, h("li", { key: "a", color: "blue" }, "A"), h("li", { key: "b" }, "B"))); + + const insertIdx = operations.findIndex(op => op.includes("appendChild") || op.includes("insertBefore")); + const updateIdx = operations.findIndex(op => op.includes("commitUpdate")); + + if (insertIdx !== -1 && updateIdx !== -1) { + expect(insertIdx).toBeLessThan(updateIdx); + } + }); + + it("removed fibers have parent set to null", () => { + const { host } = makeHost(); + const root = createHostRoot(host, {}); + root.render(h("ul", null, h("li", { key: "a" }, "A"), h("li", { key: "b" }, "B"))); + + const ulFiber = root.rootFiber!.children[0]!; + const bFiber = ulFiber.children[1]!; + + root.render(h("ul", null, h("li", { key: "a" }, "A"))); + + expect(bFiber.parent).toBeNull(); + }); +}); + +describe("commitMutations direct", () => { + function makeFiber(key: string | undefined, tag: string, instance?: string): Fiber { + return { + instance: instance ?? `inst-${tag}-${key ?? "nokey"}`, + tag, + props: {}, + key, + children: [], + parent: null, + effect: null, + signalDisposers: [], + prevProps: null, + }; + } + + function makeClassification( + overrides: Partial> = {}, + ): ChildClassification { + return { + matched: [], + added: [], + removed: [], + moves: new Map(), + ...overrides, + }; + } + + it("appendChild for added child at end position", () => { + const ops: string[] = []; + const parentFiber = makeFiber(undefined, "div", "parent-inst"); + + const addedFiber = makeFiber("b", "span", "span-inst"); + + const host: HostConfig = { + name: "test", + createRootContext: () => ({}), + createInstance: () => "inst", + createTextInstance: () => "text", + appendChild: (p, c) => ops.push(`appendChild(${p},${c})`), + removeChild: (p, c) => ops.push(`removeChild(${p},${c})`), + }; + + const existingFiber = makeFiber("a", "div", "a-inst"); + existingFiber.parent = parentFiber; + parentFiber.children = [existingFiber]; + + const classification = makeClassification({ + matched: [{ oldFiber: existingFiber, newChild: { type: "div", props: {}, children: [], key: "a" }, index: 0 }], + added: [{ newChild: { type: "span", props: {}, children: [], key: "b" }, index: 1 }], + }); + + const commitCtx: CommitContext = { + host: host as HostConfig, + ctx: {}, + createFiber: () => addedFiber, + }; + + commitMutations(parentFiber, classification, commitCtx); + + expect(ops).toContain("appendChild(parent-inst,span-inst)"); + }); + + it("removeChild called for removed child", () => { + const ops: string[] = []; + const parentFiber = makeFiber(undefined, "div", "parent-inst"); + + const removedFiber = makeFiber("b", "span", "b-inst"); + removedFiber.parent = parentFiber; + + const host: HostConfig = { + name: "test", + createRootContext: () => ({}), + createInstance: () => "inst", + createTextInstance: () => "text", + appendChild: () => {}, + removeChild: (p, c) => ops.push(`removeChild(${p},${c})`), + }; + + const classification = makeClassification({ + removed: [removedFiber], + }); + + const commitCtx: CommitContext = { + host: host as HostConfig, + ctx: {}, + createFiber: () => makeFiber("x", "x", "x"), + }; + + commitMutations(parentFiber, classification, commitCtx); + + expect(ops).toContain("removeChild(parent-inst,b-inst)"); + }); + + it("insertBefore called for moved child with staying sibling after it", () => { + const ops: string[] = []; + const parentFiber = makeFiber(undefined, "div", "parent-inst"); + + const aFiber = makeFiber("a", "div", "a-inst"); + aFiber.parent = parentFiber; + const bFiber = makeFiber("b", "span", "b-inst"); + bFiber.parent = parentFiber; + const cFiber = makeFiber("c", "p", "c-inst"); + cFiber.parent = parentFiber; + parentFiber.children = [aFiber, bFiber, cFiber]; + + const host: HostConfig = { + name: "test", + createRootContext: () => ({}), + createInstance: () => "inst", + createTextInstance: () => "text", + appendChild: (p, c) => ops.push(`appendChild(${p},${c})`), + insertBefore: (p, c, b) => ops.push(`insertBefore(${p},${c},${b})`), + removeChild: (p, c) => ops.push(`removeChild(${p},${c})`), + }; + + // Reorder: [b, a, c] → moves b before a (a is staying) + const classification: ChildClassification = { + matched: [ + { oldFiber: bFiber, newChild: { type: "span", props: {}, children: [], key: "b" }, index: 0 }, + { oldFiber: aFiber, newChild: { type: "div", props: {}, children: [], key: "a" }, index: 1 }, + { oldFiber: cFiber, newChild: { type: "p", props: {}, children: [], key: "c" }, index: 2 }, + ], + added: [], + removed: [], + moves: new Map([[0, bFiber]]), + }; + + const commitCtx: CommitContext = { + host: host as HostConfig, + ctx: {}, + createFiber: () => makeFiber("x", "x", "x"), + }; + + commitMutations(parentFiber, classification, commitCtx); + + expect(ops.some(op => op.includes("insertBefore"))).toBe(true); + }); + + it("updates committed top-down (parent before child)", () => { + const ops: string[] = []; + const parentFiber = makeFiber(undefined, "div", "parent-inst"); + parentFiber.effect = { type: "update", payload: {} }; + parentFiber.prevProps = {}; + parentFiber.props = { color: "blue" }; + + const childFiber = makeFiber("a", "span", "child-inst"); + childFiber.parent = parentFiber; + childFiber.effect = { type: "update", payload: {} }; + childFiber.prevProps = {}; + childFiber.props = { size: "big" }; + parentFiber.children = [childFiber]; + + const host: HostConfig = { + name: "test", + createRootContext: () => ({}), + createInstance: () => "inst", + createTextInstance: () => "text", + appendChild: () => {}, + commitUpdate: (inst) => ops.push(`commitUpdate(${inst})`), + }; + + const classification = makeClassification({ + matched: [{ + oldFiber: childFiber, + newChild: { type: "span", props: { size: "big" }, children: [], key: "a" }, + index: 0, + }], + }); + + const commitCtx: CommitContext = { + host: host as HostConfig, + ctx: {}, + createFiber: () => makeFiber("x", "x", "x"), + }; + + commitMutations(parentFiber, classification, commitCtx); + + const parentIdx = ops.indexOf("commitUpdate(parent-inst)"); + const childIdx = ops.indexOf("commitUpdate(child-inst)"); + expect(parentIdx).toBeLessThan(childIdx); + }); +}); \ No newline at end of file diff --git a/test/render-rerenderable.test.ts b/test/render-rerenderable.test.ts index 4503a2d..8888525 100644 --- a/test/render-rerenderable.test.ts +++ b/test/render-rerenderable.test.ts @@ -133,15 +133,14 @@ describe("Root.render() re-renderable", () => { expect(emCall!.nextProps.size).toBe("small"); }); - it("excess old children remain when new children are fewer", () => { + it("excess old children are removed when new children are fewer", () => { const { host, instances } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", null, h("span", null), h("em", null))); - const countAfterFirst = instances.length; root.render(h("div", null, h("span", null))); - expect(instances.length).toBe(countAfterFirst); - expect(root.rootFiber!.children[0]!.children.length).toBe(2); + expect(root.rootFiber!.children[0]!.children.length).toBe(1); + expect(root.rootFiber!.children[0]!.children[0]!.tag).toBe("span"); }); it("text node props are reconciled on re-render", () => {