···279279 }
280280281281 // Remove any excess nodes from the original
282282- while (from.childNodes.length > toChildNodes.length) {
283283- const lastChild = from.lastChild
284284- if (lastChild) {
285285- this.removeNode(lastChild)
286286- }
282282+ // We iterate backwards through excess nodes and attempt to remove each one
283283+ for (let i = from.childNodes.length - 1; i >= toChildNodes.length; i--) {
284284+ this.removeNode(from.childNodes[i]!)
287285 }
288286 }
289287
+95
test/morphlex-loops.test.ts
···253253 })
254254255255 describe("Edge case loops", () => {
256256+ it("should not infinite loop when beforeNodeRemoved returns false", () => {
257257+ const parent = document.createElement("div")
258258+259259+ // Create a custom element parent
260260+ const customElement = document.createElement("my-component")
261261+ const child1 = document.createElement("span")
262262+ child1.textContent = "child1"
263263+ const child2 = document.createElement("span")
264264+ child2.textContent = "child2"
265265+266266+ customElement.appendChild(child1)
267267+ customElement.appendChild(child2)
268268+ parent.appendChild(customElement)
269269+270270+ // Reference only has one child in the custom element
271271+ const reference = document.createElement("div")
272272+ const refCustomElement = document.createElement("my-component")
273273+ const refChild1 = document.createElement("span")
274274+ refChild1.textContent = "child1"
275275+ refCustomElement.appendChild(refChild1)
276276+ reference.appendChild(refCustomElement)
277277+278278+ const startTime = Date.now()
279279+280280+ // This should cause an infinite loop if not handled correctly
281281+ // because child2 can't be removed (beforeNodeRemoved returns false)
282282+ // but the algorithm keeps trying to remove it
283283+ morph(parent, reference, {
284284+ beforeNodeRemoved: (oldNode: Node) => {
285285+ let parent = oldNode.parentElement
286286+287287+ while (parent) {
288288+ if (parent.tagName && parent.tagName.includes("-")) return false
289289+ parent = parent.parentElement
290290+ }
291291+292292+ return true
293293+ },
294294+ })
295295+296296+ const endTime = Date.now()
297297+298298+ // Should complete quickly without infinite loop
299299+ expect(endTime - startTime).toBeLessThan(1000)
300300+ // child2 should still be there since it couldn't be removed
301301+ expect(customElement.children.length).toBe(2)
302302+ })
303303+304304+ it("should remove removable nodes even when some nodes cannot be removed", () => {
305305+ const parent = document.createElement("div")
306306+307307+ // Create a custom element parent
308308+ const customElement = document.createElement("my-component")
309309+ const child1 = document.createElement("span")
310310+ child1.textContent = "child1"
311311+ const child2 = document.createElement("span")
312312+ child2.textContent = "child2"
313313+ const child3 = document.createElement("span")
314314+ child3.textContent = "child3"
315315+316316+ customElement.appendChild(child1)
317317+ customElement.appendChild(child2)
318318+ parent.appendChild(customElement)
319319+ parent.appendChild(child3) // This one is outside the custom element
320320+321321+ // Reference only has child1 in custom element, no child3
322322+ const reference = document.createElement("div")
323323+ const refCustomElement = document.createElement("my-component")
324324+ const refChild1 = document.createElement("span")
325325+ refChild1.textContent = "child1"
326326+ refCustomElement.appendChild(refChild1)
327327+ reference.appendChild(refCustomElement)
328328+329329+ morph(parent, reference, {
330330+ beforeNodeRemoved: (oldNode: Node) => {
331331+ let parent = oldNode.parentElement
332332+333333+ while (parent) {
334334+ if (parent.tagName && parent.tagName.includes("-")) return false
335335+ parent = parent.parentElement
336336+ }
337337+338338+ return true
339339+ },
340340+ })
341341+342342+ // child2 should still be there (inside custom element, can't be removed)
343343+ expect(customElement.children.length).toBe(2)
344344+ expect(customElement.children[1].textContent).toBe("child2")
345345+346346+ // child3 should be removed (outside custom element)
347347+ expect(parent.children.length).toBe(1)
348348+ expect(parent.children[0]).toBe(customElement)
349349+ })
350350+256351 it("should not infinite loop when node equals insertionPoint", () => {
257352 const parent = document.createElement("div")
258353 const child = document.createElement("span")