Precise DOM morphing
morphing typescript dom
at main 564 lines 16 kB view raw
1import { describe, it, expect, beforeEach, afterEach } from "vitest" 2import { morph, morphInner } from "../src/morphlex" 3 4describe("Morphlex Vitest Suite", () => { 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("morph() - Basic functionality", () => { 19 it("should update text content", () => { 20 const original = document.createElement("div") 21 original.textContent = "Hello" 22 23 const reference = document.createElement("div") 24 reference.textContent = "World" 25 26 morph(original, reference) 27 28 expect(original.textContent).toBe("World") 29 }) 30 31 it("should accept HTML string as reference", () => { 32 const original = document.createElement("div") 33 original.textContent = "Old" 34 35 morph(original, "<div>New</div>") 36 37 expect(original.textContent).toBe("New") 38 }) 39 40 it("should preserve element when morphing matching tags", () => { 41 const original = document.createElement("div") 42 original.id = "test" 43 const elementRef = original 44 45 const reference = document.createElement("div") 46 reference.textContent = "Updated" 47 48 morph(original, reference) 49 50 expect(original).toBe(elementRef) 51 expect(original.textContent).toBe("Updated") 52 }) 53 54 it("should replace element when morphing different tags", () => { 55 const original = document.createElement("div") 56 const parent = document.createElement("section") 57 parent.appendChild(original) 58 59 const reference = document.createElement("span") 60 reference.textContent = "Updated" 61 62 morph(original, reference) 63 64 expect(parent.querySelector("span")).toBeTruthy() 65 expect(parent.querySelector("div")).toBeFalsy() 66 }) 67 }) 68 69 describe("morph() - Attribute handling", () => { 70 it("should add attributes", () => { 71 const original = document.createElement("button") 72 73 const reference = document.createElement("button") 74 reference.setAttribute("class", "btn-primary") 75 reference.setAttribute("disabled", "") 76 77 morph(original, reference) 78 79 expect(original.className).toBe("btn-primary") 80 expect(original.hasAttribute("disabled")).toBe(true) 81 }) 82 83 it("should remove attributes", () => { 84 const original = document.createElement("div") 85 original.setAttribute("data-test", "value") 86 87 const reference = document.createElement("div") 88 89 morph(original, reference) 90 91 expect(original.hasAttribute("data-test")).toBe(false) 92 }) 93 94 it("should update attributes", () => { 95 const original = document.createElement("div") 96 original.setAttribute("data-value", "old") 97 98 const reference = document.createElement("div") 99 reference.setAttribute("data-value", "new") 100 101 morph(original, reference) 102 103 expect(original.getAttribute("data-value")).toBe("new") 104 }) 105 106 it("should update class attribute", () => { 107 const original = document.createElement("div") 108 original.className = "old-class" 109 110 const reference = document.createElement("div") 111 reference.className = "new-class" 112 113 morph(original, reference) 114 115 expect(original.className).toBe("new-class") 116 }) 117 }) 118 119 describe("morph() - Child elements", () => { 120 it("should add child elements", () => { 121 const original = document.createElement("ul") 122 123 const reference = document.createElement("ul") 124 const li1 = document.createElement("li") 125 li1.textContent = "Item 1" 126 const li2 = document.createElement("li") 127 li2.textContent = "Item 2" 128 reference.appendChild(li1) 129 reference.appendChild(li2) 130 131 morph(original, reference) 132 133 expect(original.children.length).toBe(2) 134 expect(original.children[0].textContent).toBe("Item 1") 135 }) 136 137 it("should remove excess child elements", () => { 138 const original = document.createElement("ul") 139 original.innerHTML = "<li>A</li><li>B</li><li>C</li>" 140 141 const reference = document.createElement("ul") 142 reference.innerHTML = "<li>A</li>" 143 144 morph(original, reference) 145 146 expect(original.children.length).toBe(1) 147 }) 148 149 it("should morph existing child elements", () => { 150 const original = document.createElement("div") 151 const child = document.createElement("span") 152 child.textContent = "old" 153 original.appendChild(child) 154 155 const reference = document.createElement("div") 156 const refChild = document.createElement("span") 157 refChild.textContent = "new" 158 reference.appendChild(refChild) 159 160 morph(original, reference) 161 162 expect(original.children[0].textContent).toBe("new") 163 }) 164 165 it("should handle text nodes", () => { 166 const original = document.createElement("div") 167 original.appendChild(document.createTextNode("Hello")) 168 169 const reference = document.createElement("div") 170 reference.appendChild(document.createTextNode("World")) 171 172 morph(original, reference) 173 174 expect(original.textContent).toBe("World") 175 }) 176 177 it("should handle mixed text and element nodes", () => { 178 const original = document.createElement("div") 179 original.appendChild(document.createTextNode("Start ")) 180 const span = document.createElement("span") 181 span.textContent = "middle" 182 original.appendChild(span) 183 original.appendChild(document.createTextNode(" end")) 184 185 const reference = document.createElement("div") 186 reference.appendChild(document.createTextNode("Start ")) 187 const refSpan = document.createElement("span") 188 refSpan.textContent = "updated" 189 reference.appendChild(refSpan) 190 reference.appendChild(document.createTextNode(" end")) 191 192 morph(original, reference) 193 194 expect(original.textContent).toBe("Start updated end") 195 }) 196 }) 197 198 describe("morph() - Element identity and IDs", () => { 199 it("should preserve element identity when using IDs", () => { 200 const original = document.createElement("div") 201 original.innerHTML = '<p id="p1">Para 1</p><p id="p2">Para 2</p>' 202 203 const reference = document.createElement("div") 204 reference.innerHTML = '<p id="p2">Para 2</p><p id="p1">Para 1</p>' 205 206 const p1Original = original.querySelector("#p1") 207 208 morph(original, reference) 209 210 const p1After = original.querySelector("#p1") 211 212 expect(p1After).toBe(p1Original) 213 }) 214 215 it("should reorder elements with IDs correctly", () => { 216 const original = document.createElement("div") 217 original.innerHTML = '<span id="a">A</span><span id="b">B</span><span id="c">C</span>' 218 219 const reference = document.createElement("div") 220 reference.innerHTML = '<span id="c">C</span><span id="a">A</span><span id="b">B</span>' 221 222 const originalA = original.querySelector("#a") 223 224 morph(original, reference) 225 226 const newA = original.querySelector("#a") 227 228 expect(newA).toBe(originalA) 229 expect(original.children[1]).toBe(newA) 230 }) 231 }) 232 233 describe("morph() - Callbacks", () => { 234 it("should call beforeNodeVisited and afterNodeVisited", () => { 235 const original = document.createElement("div") 236 original.textContent = "Before" 237 238 const reference = document.createElement("div") 239 reference.textContent = "After" 240 241 let beforeCalled = false 242 let afterCalled = false 243 244 morph(original, reference, { 245 beforeNodeVisited: () => { 246 beforeCalled = true 247 return true 248 }, 249 afterNodeVisited: () => { 250 afterCalled = true 251 }, 252 }) 253 254 expect(beforeCalled).toBe(true) 255 expect(afterCalled).toBe(true) 256 }) 257 258 it("should cancel morphing if beforeNodeVisited returns false", () => { 259 const original = document.createElement("div") 260 original.textContent = "Original" 261 262 const reference = document.createElement("div") 263 reference.textContent = "Reference" 264 265 morph(original, reference, { 266 beforeNodeVisited: () => false, 267 }) 268 269 expect(original.textContent).toBe("Original") 270 }) 271 272 it("should call beforeNodeAdded and afterNodeAdded", () => { 273 const original = document.createElement("div") 274 275 const reference = document.createElement("div") 276 const newChild = document.createElement("p") 277 newChild.textContent = "New" 278 reference.appendChild(newChild) 279 280 let beforeAddCalled = false 281 let afterAddCalled = false 282 283 morph(original, reference, { 284 beforeNodeAdded: () => { 285 beforeAddCalled = true 286 return true 287 }, 288 afterNodeAdded: () => { 289 afterAddCalled = true 290 }, 291 }) 292 293 expect(beforeAddCalled).toBe(true) 294 expect(afterAddCalled).toBe(true) 295 }) 296 297 it("should call beforeNodeRemoved and afterNodeRemoved", () => { 298 const original = document.createElement("div") 299 const child = document.createElement("p") 300 child.textContent = "To remove" 301 original.appendChild(child) 302 303 const reference = document.createElement("div") 304 305 let beforeRemoveCalled = false 306 let afterRemoveCalled = false 307 308 morph(original, reference, { 309 beforeNodeRemoved: () => { 310 beforeRemoveCalled = true 311 return true 312 }, 313 afterNodeRemoved: () => { 314 afterRemoveCalled = true 315 }, 316 }) 317 318 expect(beforeRemoveCalled).toBe(true) 319 expect(afterRemoveCalled).toBe(true) 320 }) 321 322 it("should call attribute update callbacks", () => { 323 const original = document.createElement("div") 324 325 const reference = document.createElement("div") 326 reference.setAttribute("data-test", "value") 327 328 let callbackCalled = false 329 330 morph(original, reference, { 331 afterAttributeUpdated: (element, attrName) => { 332 if (attrName === "data-test") { 333 callbackCalled = true 334 } 335 }, 336 }) 337 338 expect(callbackCalled).toBe(true) 339 }) 340 }) 341 342 describe("morph() - Form elements", () => { 343 it("should update textarea value", () => { 344 const original = document.createElement("textarea") as HTMLTextAreaElement 345 original.textContent = "old text" 346 347 const reference = document.createElement("textarea") as HTMLTextAreaElement 348 reference.textContent = "new text" 349 350 morph(original, reference) 351 352 expect(original.textContent).toBe("new text") 353 }) 354 }) 355 356 describe("morphInner() - Basic functionality", () => { 357 it("should morph inner content only", () => { 358 const original = document.createElement("div") 359 original.id = "container" 360 original.innerHTML = "<p>Old</p>" 361 362 const reference = document.createElement("div") 363 reference.innerHTML = "<p>New</p>" 364 365 morphInner(original, reference) 366 367 expect(original.id).toBe("container") 368 expect(original.innerHTML).toBe("<p>New</p>") 369 }) 370 371 it("should accept string reference for morphInner", () => { 372 const original = document.createElement("div") 373 original.innerHTML = "<span>Old</span>" 374 375 const reference = document.createElement("div") 376 reference.innerHTML = "<span>New</span>" 377 378 morphInner(original, reference) 379 380 expect(original.innerHTML).toBe("<span>New</span>") 381 }) 382 383 it("should preserve outer element attributes with morphInner", () => { 384 const original = document.createElement("div") 385 original.setAttribute("class", "container") 386 original.setAttribute("data-id", "123") 387 original.innerHTML = "<p>Old</p>" 388 389 const reference = document.createElement("div") 390 reference.setAttribute("class", "different") 391 reference.innerHTML = "<p>New</p>" 392 393 morphInner(original, reference) 394 395 expect(original.getAttribute("class")).toBe("container") 396 expect(original.getAttribute("data-id")).toBe("123") 397 expect(original.innerHTML).toBe("<p>New</p>") 398 }) 399 400 it("should update multiple children with morphInner", () => { 401 const original = document.createElement("ul") 402 original.innerHTML = "<li>Item 1</li><li>Item 2</li>" 403 404 const reference = document.createElement("ul") 405 reference.innerHTML = "<li>Item A</li><li>Item B</li><li>Item C</li>" 406 407 morphInner(original, reference) 408 409 expect(original.children.length).toBe(3) 410 expect(original.children[0].textContent).toBe("Item A") 411 expect(original.children[2].textContent).toBe("Item C") 412 }) 413 414 it("should empty contents with morphInner when reference has no children", () => { 415 const original = document.createElement("div") 416 original.innerHTML = "<span>Content</span><p>More</p>" 417 418 const reference = document.createElement("div") 419 420 morphInner(original, reference) 421 422 expect(original.children.length).toBe(0) 423 }) 424 }) 425 426 describe("Edge cases and complex scenarios", () => { 427 it("should handle empty elements", () => { 428 const original = document.createElement("div") 429 const reference = document.createElement("div") 430 431 expect(() => morph(original, reference)).not.toThrow() 432 expect(original.children.length).toBe(0) 433 }) 434 435 it("should handle deeply nested structures", () => { 436 const original = document.createElement("div") 437 original.innerHTML = "<div><div><div><span>Deep</span></div></div></div>" 438 439 const reference = document.createElement("div") 440 reference.innerHTML = "<div><div><div><span>Updated</span></div></div></div>" 441 442 morph(original, reference) 443 444 expect(original.querySelector("span")?.textContent).toBe("Updated") 445 }) 446 447 it("should handle special characters in text", () => { 448 const original = document.createElement("div") 449 original.textContent = "Hello & goodbye" 450 451 const reference = document.createElement("div") 452 reference.textContent = 'Special <> characters "test"' 453 454 morph(original, reference) 455 456 expect(original.textContent).toBe('Special <> characters "test"') 457 }) 458 459 it("should handle multiple class names", () => { 460 const original = document.createElement("div") 461 original.classList.add("class1", "class2", "class3") 462 463 const reference = document.createElement("div") 464 reference.classList.add("class2", "class3", "class4") 465 466 morph(original, reference) 467 468 expect(original.classList.contains("class2")).toBe(true) 469 expect(original.classList.contains("class3")).toBe(true) 470 expect(original.classList.contains("class4")).toBe(true) 471 expect(original.classList.contains("class1")).toBe(false) 472 }) 473 474 it("should handle element replacement", () => { 475 const original = document.createElement("div") 476 const span = document.createElement("span") 477 span.textContent = "Span" 478 original.appendChild(span) 479 480 const reference = document.createElement("div") 481 const p = document.createElement("p") 482 p.textContent = "Paragraph" 483 reference.appendChild(p) 484 485 morph(original, reference) 486 487 expect(original.children[0].nodeName).toBe("P") 488 expect(original.children[0].textContent).toBe("Paragraph") 489 }) 490 491 it("should handle list updates with ID preservation", () => { 492 const original = document.createElement("ul") 493 original.innerHTML = '<li id="item-1">Item 1</li><li id="item-2">Item 2</li><li id="item-3">Item 3</li>' 494 495 const item2Ref = original.querySelector("#item-2") 496 497 const reference = document.createElement("ul") 498 reference.innerHTML = 499 '<li id="item-1">Item 1</li><li id="item-3">Item 3</li><li id="item-2">Item 2</li><li id="item-4">Item 4</li>' 500 501 morph(original, reference) 502 503 expect(original.querySelector("#item-2")).toBe(item2Ref) 504 expect(original.children.length).toBe(4) 505 }) 506 507 it("should handle complex page-like structure", () => { 508 const original = document.createElement("main") 509 original.innerHTML = ` 510 <header id="header"> 511 <h1>Title</h1> 512 </header> 513 <article> 514 <p>Old paragraph</p> 515 </article> 516 ` 517 518 const reference = document.createElement("main") 519 reference.innerHTML = ` 520 <header id="header"> 521 <h1>New Title</h1> 522 </header> 523 <article> 524 <p>New paragraph</p> 525 <p>Another paragraph</p> 526 </article> 527 ` 528 529 const headerRef = original.querySelector("#header") 530 531 morph(original, reference) 532 533 expect(original.querySelector("#header")).toBe(headerRef) 534 expect(original.querySelector("h1")?.textContent).toBe("New Title") 535 expect(original.querySelectorAll("article p").length).toBe(2) 536 }) 537 538 it("should handle nested element morphing with updates", () => { 539 const original = document.createElement("div") 540 original.innerHTML = "<p>Old <span>content</span></p>" 541 542 const reference = document.createElement("div") 543 reference.innerHTML = "<p>New <span>text</span></p>" 544 545 morph(original, reference) 546 547 expect(original.innerHTML).toBe("<p>New <span>text</span></p>") 548 }) 549 550 it("should preserve element reference through morph", () => { 551 const original = document.createElement("div") 552 original.textContent = "Original" 553 554 const reference = document.createElement("div") 555 reference.textContent = "Updated" 556 557 const originalRef = original 558 morph(original, reference) 559 560 expect(original).toBe(originalRef) 561 expect(original.textContent).toBe("Updated") 562 }) 563 }) 564})