From e6d7af33fa6c48aa4d7707bcbd833f2846323b8a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 00:23:02 -0400 Subject: [PATCH] test(ui): deepen keyed reconciliation (complex moves) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toward React equivalency for the reconciler's hardest move patterns: full reverse (order flips, every node moved not recreated — identity preserved), simultaneous insert+remove, collapse to one survivor, and grow back. Each produces the correct order and child count. Verified via the native reconciler harness (the same surface that found the v3.4.8 duplicate-key leak). All pass; no version bump. Co-Authored-By: Claude Opus 4.8 (1M context) --- ui/reconciler_keyed_moves_test.go | 88 +++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 ui/reconciler_keyed_moves_test.go diff --git a/ui/reconciler_keyed_moves_test.go b/ui/reconciler_keyed_moves_test.go new file mode 100644 index 00000000..6c9b2356 --- /dev/null +++ b/ui/reconciler_keyed_moves_test.go @@ -0,0 +1,88 @@ +//go:build !js || !wasm + +package ui_test + +import ( + "testing" + + "github.com/monstercameron/GoWebComponents/internal/platform/mockdom" + "github.com/monstercameron/GoWebComponents/internal/runtime" +) + +func keyedListWithData(parseKeys ...string) *runtime.Element { + parseKids := make([]any, len(parseKeys)) + for parseI, parseK := range parseKeys { + parseKids[parseI] = runtime.CreateElement("li", map[string]any{"key": parseK, "data-k": parseK}, parseK) + } + return runtime.CreateElement("ul", map[string]any{}, parseKids...) +} + +// Deepens keyed-reconciliation coverage with the harder move patterns React's +// reconciler tests exercise: full reverse (identity preserved), simultaneous +// insert+remove, and collapse — each producing the correct order and child count. +func TestKeyedComplexMoves(t *testing.T) { + adapter := mockdom.NewMockDOMAdapter() + rt := runtime.NewRuntime(runtime.Config{DOMAdapter: adapter, Reset: true}) + root := adapter.CreateElement("div") + + order := func() []string { + parseUL := adapter.GetChildren(root)[0] + var parseOut []string + for _, parseLI := range adapter.GetChildren(parseUL) { + if parseN, parseOk := parseLI.(*mockdom.MockDOMNode); parseOk { + parseOut = append(parseOut, parseN.Attrs["data-k"]) + } + } + return parseOut + } + idOfKey := func(parseKey string) int { + parseUL := adapter.GetChildren(root)[0] + for _, parseLI := range adapter.GetChildren(parseUL) { + if parseN, parseOk := parseLI.(*mockdom.MockDOMNode); parseOk && parseN.Attrs["data-k"] == parseKey { + return parseN.ID + } + } + return -1 + } + equal := func(parseGot, parseWant []string) bool { + if len(parseGot) != len(parseWant) { + return false + } + for parseI := range parseGot { + if parseGot[parseI] != parseWant[parseI] { + return false + } + } + return true + } + + rt.RenderInto(root, keyedListWithData("a", "b", "c", "d", "e")) + parseIDA, parseIDE := idOfKey("a"), idOfKey("e") + + // Full reverse: order flips, every node is moved (not recreated). + rt.RenderInto(root, keyedListWithData("e", "d", "c", "b", "a")) + if !equal(order(), []string{"e", "d", "c", "b", "a"}) { + t.Fatalf("reverse order = %v", order()) + } + if idOfKey("a") != parseIDA || idOfKey("e") != parseIDE { + t.Errorf("reverse recreated nodes (a: %d->%d, e: %d->%d)", parseIDA, idOfKey("a"), parseIDE, idOfKey("e")) + } + + // Simultaneous insert (x, y) and remove (b, d). + rt.RenderInto(root, keyedListWithData("e", "x", "c", "y", "a")) + if !equal(order(), []string{"e", "x", "c", "y", "a"}) { + t.Errorf("insert+remove order = %v, want [e x c y a]", order()) + } + + // Collapse to a single survivor. + rt.RenderInto(root, keyedListWithData("c")) + if !equal(order(), []string{"c"}) { + t.Errorf("collapse order = %v, want [c]", order()) + } + + // Grow back from one. + rt.RenderInto(root, keyedListWithData("c", "z", "w")) + if !equal(order(), []string{"c", "z", "w"}) { + t.Errorf("grow order = %v, want [c z w]", order()) + } +}