Precise DOM morphing
morphing typescript dom
at main 398 lines 12 kB view raw
1import { describe, it, expect, beforeEach, afterEach } from "vitest" 2import { morph } from "../src/morphlex" 3 4describe("Morphlex - Infinite Loop Bug Detection", () => { 5 let container: HTMLElement 6 7 beforeEach(() => { 8 container = document.createElement("div") 9 document.body.appendChild(container) 10 }) 11 12 afterEach(() => { 13 if (container && container.parentNode) { 14 container.parentNode.removeChild(container) 15 } 16 }) 17 18 describe("Input value handling", () => { 19 it("should not infinite loop with modified input value", () => { 20 const parent = document.createElement("div") 21 22 const input = document.createElement("input") as HTMLInputElement 23 input.id = "input1" 24 input.defaultValue = "default" 25 input.value = "modified" 26 27 const div = document.createElement("div") 28 div.id = "div1" 29 30 parent.appendChild(input) 31 parent.appendChild(div) 32 33 const reference = document.createElement("div") 34 35 const refDiv = document.createElement("div") 36 refDiv.id = "div1" 37 38 const refInput = document.createElement("input") as HTMLInputElement 39 refInput.id = "input1" 40 41 reference.appendChild(refDiv) 42 reference.appendChild(refInput) 43 44 const startTime = Date.now() 45 morph(parent, reference) 46 const endTime = Date.now() 47 48 expect(endTime - startTime).toBeLessThan(1000) 49 }) 50 }) 51 52 describe("ID-based matching loops", () => { 53 it("should not infinite loop when matching by overlapping ID sets", () => { 54 const parent = document.createElement("div") 55 56 const outer1 = document.createElement("div") 57 outer1.id = "outer1" 58 const inner1 = document.createElement("span") 59 inner1.id = "inner1" 60 outer1.appendChild(inner1) 61 62 const outer2 = document.createElement("div") 63 outer2.id = "outer2" 64 const inner2 = document.createElement("span") 65 inner2.id = "inner2" 66 outer2.appendChild(inner2) 67 68 parent.appendChild(outer1) 69 parent.appendChild(outer2) 70 71 // Reference has them in reverse order 72 const reference = document.createElement("div") 73 74 const refOuter2 = document.createElement("div") 75 refOuter2.id = "outer2" 76 const refInner2 = document.createElement("span") 77 refInner2.id = "inner2" 78 refOuter2.appendChild(refInner2) 79 80 const refOuter1 = document.createElement("div") 81 refOuter1.id = "outer1" 82 const refInner1 = document.createElement("span") 83 refInner1.id = "inner1" 84 refOuter1.appendChild(refInner1) 85 86 reference.appendChild(refOuter2) 87 reference.appendChild(refOuter1) 88 89 const startTime = Date.now() 90 morph(parent, reference) 91 const endTime = Date.now() 92 93 expect(endTime - startTime).toBeLessThan(1000) 94 expect(parent.children[0].id).toBe("outer2") 95 expect(parent.children[1].id).toBe("outer1") 96 }) 97 98 it("should not infinite loop when currentNode becomes child during matching", () => { 99 const parent = document.createElement("div") 100 101 const child1 = document.createElement("div") 102 child1.id = "child1" 103 104 const child2 = document.createElement("div") 105 child2.id = "child2" 106 107 const child3 = document.createElement("div") 108 child3.id = "child3" 109 110 parent.appendChild(child1) 111 parent.appendChild(child2) 112 parent.appendChild(child3) 113 114 const reference = document.createElement("div") 115 116 const refChild2 = document.createElement("div") 117 refChild2.id = "child2" 118 119 const refChild3 = document.createElement("div") 120 refChild3.id = "child3" 121 122 const refChild1 = document.createElement("div") 123 refChild1.id = "child1" 124 125 reference.appendChild(refChild2) 126 reference.appendChild(refChild3) 127 reference.appendChild(refChild1) 128 129 const startTime = Date.now() 130 morph(parent, reference) 131 const endTime = Date.now() 132 133 expect(endTime - startTime).toBeLessThan(1000) 134 }) 135 }) 136 137 describe("Head element morphing loops", () => { 138 it("should not infinite loop when morphing head with many elements", () => { 139 const originalHead = document.createElement("head") 140 141 for (let i = 0; i < 10; i++) { 142 const meta = document.createElement("meta") 143 meta.setAttribute("name", `meta-${i}`) 144 meta.setAttribute("content", `value-${i}`) 145 originalHead.appendChild(meta) 146 } 147 148 const referenceHead = document.createElement("head") 149 150 for (let i = 0; i < 10; i++) { 151 const meta = document.createElement("meta") 152 meta.setAttribute("name", `meta-${i}`) 153 meta.setAttribute("content", `updated-${i}`) 154 referenceHead.appendChild(meta) 155 } 156 157 const startTime = Date.now() 158 morph(originalHead, referenceHead) 159 const endTime = Date.now() 160 161 expect(endTime - startTime).toBeLessThan(1000) 162 }) 163 164 it("should not infinite loop with identical outerHTML", () => { 165 const originalHead = document.createElement("head") 166 const script1 = document.createElement("script") 167 script1.textContent = "console.log('test')" 168 originalHead.appendChild(script1) 169 170 const script2 = document.createElement("script") 171 script2.textContent = "console.log('test')" 172 originalHead.appendChild(script2) 173 174 const referenceHead = document.createElement("head") 175 const refScript = document.createElement("script") 176 refScript.textContent = "console.log('test')" 177 referenceHead.appendChild(refScript) 178 179 const startTime = Date.now() 180 morph(originalHead, referenceHead) 181 const endTime = Date.now() 182 183 expect(endTime - startTime).toBeLessThan(1000) 184 }) 185 }) 186 187 describe("Recursive morphing loops", () => { 188 it("should not infinite loop with deeply nested structures", () => { 189 const parent = document.createElement("div") 190 let current = parent 191 192 for (let i = 0; i < 20; i++) { 193 const child = document.createElement("div") 194 child.id = `level-${i}` 195 current.appendChild(child) 196 current = child 197 } 198 199 const reference = document.createElement("div") 200 let refCurrent = reference 201 202 for (let i = 0; i < 20; i++) { 203 const child = document.createElement("div") 204 child.id = `level-${i}` 205 child.textContent = "updated" 206 refCurrent.appendChild(child) 207 refCurrent = child 208 } 209 210 const startTime = Date.now() 211 morph(parent, reference) 212 const endTime = Date.now() 213 214 expect(endTime - startTime).toBeLessThan(2000) 215 }) 216 217 it("should not infinite loop with circular-looking ID references", () => { 218 const parent = document.createElement("div") 219 220 const a = document.createElement("div") 221 a.id = "a" 222 const b = document.createElement("div") 223 b.id = "b" 224 const c = document.createElement("div") 225 c.id = "c" 226 227 parent.appendChild(a) 228 parent.appendChild(b) 229 parent.appendChild(c) 230 231 const reference = document.createElement("div") 232 233 const refB = document.createElement("div") 234 refB.id = "b" 235 const refC = document.createElement("div") 236 refC.id = "c" 237 const refA = document.createElement("div") 238 refA.id = "a" 239 240 reference.appendChild(refB) 241 reference.appendChild(refC) 242 reference.appendChild(refA) 243 244 const startTime = Date.now() 245 morph(parent, reference) 246 const endTime = Date.now() 247 248 expect(endTime - startTime).toBeLessThan(1000) 249 expect(parent.children[0].id).toBe("b") 250 expect(parent.children[1].id).toBe("c") 251 expect(parent.children[2].id).toBe("a") 252 }) 253 }) 254 255 describe("Edge case loops", () => { 256 it("should not infinite loop when beforeNodeRemoved returns false", () => { 257 const parent = document.createElement("div") 258 259 // Create a custom element parent 260 const customElement = document.createElement("my-component") 261 const child1 = document.createElement("span") 262 child1.textContent = "child1" 263 const child2 = document.createElement("span") 264 child2.textContent = "child2" 265 266 customElement.appendChild(child1) 267 customElement.appendChild(child2) 268 parent.appendChild(customElement) 269 270 // Reference only has one child in the custom element 271 const reference = document.createElement("div") 272 const refCustomElement = document.createElement("my-component") 273 const refChild1 = document.createElement("span") 274 refChild1.textContent = "child1" 275 refCustomElement.appendChild(refChild1) 276 reference.appendChild(refCustomElement) 277 278 const startTime = Date.now() 279 280 // This should cause an infinite loop if not handled correctly 281 // because child2 can't be removed (beforeNodeRemoved returns false) 282 // but the algorithm keeps trying to remove it 283 morph(parent, reference, { 284 beforeNodeRemoved: (oldNode: Node) => { 285 let parent = oldNode.parentElement 286 287 while (parent) { 288 if (parent.tagName && parent.tagName.includes("-")) return false 289 parent = parent.parentElement 290 } 291 292 return true 293 }, 294 }) 295 296 const endTime = Date.now() 297 298 // Should complete quickly without infinite loop 299 expect(endTime - startTime).toBeLessThan(1000) 300 // child2 should still be there since it couldn't be removed 301 expect(customElement.children.length).toBe(2) 302 }) 303 304 it("should remove removable nodes even when some nodes cannot be removed", () => { 305 const parent = document.createElement("div") 306 307 // Create a custom element parent 308 const customElement = document.createElement("my-component") 309 const child1 = document.createElement("span") 310 child1.textContent = "child1" 311 const child2 = document.createElement("span") 312 child2.textContent = "child2" 313 const child3 = document.createElement("span") 314 child3.textContent = "child3" 315 316 customElement.appendChild(child1) 317 customElement.appendChild(child2) 318 parent.appendChild(customElement) 319 parent.appendChild(child3) // This one is outside the custom element 320 321 // Reference only has child1 in custom element, no child3 322 const reference = document.createElement("div") 323 const refCustomElement = document.createElement("my-component") 324 const refChild1 = document.createElement("span") 325 refChild1.textContent = "child1" 326 refCustomElement.appendChild(refChild1) 327 reference.appendChild(refCustomElement) 328 329 morph(parent, reference, { 330 beforeNodeRemoved: (oldNode: Node) => { 331 let parent = oldNode.parentElement 332 333 while (parent) { 334 if (parent.tagName && parent.tagName.includes("-")) return false 335 parent = parent.parentElement 336 } 337 338 return true 339 }, 340 }) 341 342 // child2 should still be there (inside custom element, can't be removed) 343 expect(customElement.children.length).toBe(2) 344 expect(customElement.children[1].textContent).toBe("child2") 345 346 // child3 should be removed (outside custom element) 347 expect(parent.children.length).toBe(1) 348 expect(parent.children[0]).toBe(customElement) 349 }) 350 351 it("should not infinite loop when node equals insertionPoint", () => { 352 const parent = document.createElement("div") 353 const child = document.createElement("span") 354 child.textContent = "test" 355 parent.appendChild(child) 356 357 const reference = document.createElement("div") 358 const refChild = document.createElement("span") 359 refChild.textContent = "test updated" 360 reference.appendChild(refChild) 361 362 const startTime = Date.now() 363 morph(parent, reference) 364 const endTime = Date.now() 365 366 expect(endTime - startTime).toBeLessThan(1000) 367 }) 368 369 it("should not infinite loop with empty elements", () => { 370 const parent = document.createElement("div") 371 const reference = document.createElement("div") 372 373 const startTime = Date.now() 374 morph(parent, reference) 375 const endTime = Date.now() 376 377 expect(endTime - startTime).toBeLessThan(100) 378 }) 379 380 it("should not infinite loop when cleaning up excess nodes", () => { 381 const parent = document.createElement("div") 382 383 for (let i = 0; i < 100; i++) { 384 const child = document.createElement("div") 385 parent.appendChild(child) 386 } 387 388 const reference = document.createElement("div") 389 390 const startTime = Date.now() 391 morph(parent, reference) 392 const endTime = Date.now() 393 394 expect(endTime - startTime).toBeLessThan(1000) 395 expect(parent.children.length).toBe(0) 396 }) 397 }) 398})