Precise DOM morphing
morphing typescript dom
at main 838 lines 27 kB view raw
1import { describe, it, expect, beforeEach, afterEach } from "vitest" 2import { morph, morphInner } from "../src/morphlex" 3 4describe("Morphlex Browser Tests", () => { 5 let container: HTMLElement 6 7 beforeEach(() => { 8 container = document.createElement("div") 9 container.id = "test-container" 10 document.body.appendChild(container) 11 }) 12 13 afterEach(() => { 14 if (container && container.parentNode) { 15 container.parentNode.removeChild(container) 16 } 17 }) 18 19 describe("Browser-specific DOM interactions", () => { 20 it("should handle real browser events after morphing", async () => { 21 const original = document.createElement("button") 22 original.textContent = "Click me" 23 let clicked = false 24 25 original.addEventListener("click", () => { 26 clicked = true 27 }) 28 29 container.appendChild(original) 30 31 // Morph with new text but preserve the element 32 const reference = document.createElement("button") 33 reference.textContent = "Updated button" 34 35 morph(original, reference) 36 37 // Verify the button text changed 38 expect(original.textContent).toBe("Updated button") 39 40 // Click the button in the real browser 41 original.click() 42 43 // Event listener should still work 44 expect(clicked).toBe(true) 45 }) 46 47 it("should handle CSS transitions in real browser", async () => { 48 const original = document.createElement("div") 49 original.style.cssText = "width: 100px; transition: width 0.1s;" 50 container.appendChild(original) 51 52 // Force browser to compute styles 53 const computedStyle = getComputedStyle(original) 54 expect(computedStyle.width).toBe("100px") 55 56 // Morph with new styles 57 const reference = document.createElement("div") 58 reference.style.cssText = "width: 200px; transition: width 0.1s;" 59 60 morph(original, reference) 61 62 // Verify styles were updated 63 expect(original.style.width).toBe("200px") 64 }) 65 66 it("should handle focus state correctly", () => { 67 const original = document.createElement("input") 68 original.type = "text" 69 original.value = "initial" 70 container.appendChild(original) 71 72 // Focus the input 73 original.focus() 74 expect(document.activeElement).toBe(original) 75 76 // Morph with new attributes 77 const reference = document.createElement("input") 78 reference.type = "text" 79 reference.value = "updated" 80 reference.placeholder = "Enter text" 81 82 morph(original, reference) 83 84 // Focus should be preserved on the same element 85 expect(document.activeElement).toBe(original) 86 // Value is NOT updated - morphlex no longer updates input values 87 expect(original.value).toBe("initial") 88 expect(original.placeholder).toBe("Enter text") 89 }) 90 91 it("should handle complex nested structures", () => { 92 container.innerHTML = ` 93 <div class="parent"> 94 <h1>Title</h1> 95 <ul> 96 <li>Item 1</li> 97 <li>Item 2</li> 98 <li>Item 3</li> 99 </ul> 100 </div> 101 ` 102 103 const original = container.firstElementChild as HTMLElement 104 const originalH1 = original.querySelector("h1") 105 106 const referenceHTML = ` 107 <div class="parent modified"> 108 <h1>Updated Title</h1> 109 <ul> 110 <li>Item 1 - Modified</li> 111 <li>Item 2</li> 112 <li>New Item 3</li> 113 <li>Item 4</li> 114 </ul> 115 </div> 116 ` 117 118 morph(original, referenceHTML) 119 120 // Check the structure is updated 121 expect(original.className).toBe("parent modified") 122 expect(originalH1?.textContent).toBe("Updated Title") 123 124 const newItems = Array.from(original.querySelectorAll("li")) 125 expect(newItems.length).toBe(4) 126 expect(newItems[0].textContent).toBe("Item 1 - Modified") 127 expect(newItems[3].textContent).toBe("Item 4") 128 }) 129 130 it("should handle SVG elements in real browser", () => { 131 const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg") 132 svg.setAttribute("width", "100") 133 svg.setAttribute("height", "100") 134 135 const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle") 136 circle.setAttribute("cx", "50") 137 circle.setAttribute("cy", "50") 138 circle.setAttribute("r", "40") 139 circle.setAttribute("fill", "red") 140 141 svg.appendChild(circle) 142 container.appendChild(svg) 143 144 const referenceSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg") 145 referenceSVG.setAttribute("width", "200") 146 referenceSVG.setAttribute("height", "200") 147 148 const referenceCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle") 149 referenceCircle.setAttribute("cx", "100") 150 referenceCircle.setAttribute("cy", "100") 151 referenceCircle.setAttribute("r", "80") 152 referenceCircle.setAttribute("fill", "blue") 153 154 referenceSVG.appendChild(referenceCircle) 155 156 morph(svg, referenceSVG) 157 158 expect(svg.getAttribute("width")).toBe("200") 159 expect(svg.getAttribute("height")).toBe("200") 160 161 const morphedCircle = svg.querySelector("circle") 162 expect(morphedCircle?.getAttribute("cx")).toBe("100") 163 expect(morphedCircle?.getAttribute("cy")).toBe("100") 164 expect(morphedCircle?.getAttribute("r")).toBe("80") 165 expect(morphedCircle?.getAttribute("fill")).toBe("blue") 166 }) 167 168 it("should handle form inputs and maintain state", () => { 169 const form = document.createElement("form") 170 form.innerHTML = ` 171 <input type="text" name="username" value="john"> 172 <input type="checkbox" name="remember" checked> 173 <select name="country"> 174 <option value="us">United States</option> 175 <option value="uk" selected>United Kingdom</option> 176 </select> 177 ` 178 container.appendChild(form) 179 180 const textInput = form.querySelector('input[name="username"]') as HTMLInputElement 181 const checkbox = form.querySelector('input[name="remember"]') as HTMLInputElement 182 const select = form.querySelector('select[name="country"]') as HTMLSelectElement 183 184 // Modify the values in the browser 185 textInput.value = "jane" 186 checkbox.checked = false 187 select.value = "us" 188 189 // Create reference with different structure but same form fields 190 const referenceForm = document.createElement("form") 191 referenceForm.className = "updated-form" 192 referenceForm.innerHTML = ` 193 <div class="form-group"> 194 <input type="text" name="username" value="john" placeholder="Username"> 195 </div> 196 <div class="form-group"> 197 <input type="checkbox" name="remember" checked> 198 <label>Remember me</label> 199 </div> 200 <div class="form-group"> 201 <select name="country" class="country-select"> 202 <option value="us">United States</option> 203 <option value="uk" selected>United Kingdom</option> 204 <option value="ca">Canada</option> 205 </select> 206 </div> 207 ` 208 209 morph(form, referenceForm) 210 211 // Form should have new structure 212 expect(form.className).toBe("updated-form") 213 expect(form.querySelectorAll(".form-group").length).toBe(3) 214 215 // The form elements should be the same instances (preserved) 216 const newTextInput = form.querySelector('input[name="username"]') as HTMLInputElement 217 const newCheckbox = form.querySelector('input[name="remember"]') as HTMLInputElement 218 const newSelect = form.querySelector('select[name="country"]') as HTMLSelectElement 219 220 // Values are NOT updated - morphlex no longer updates input values, checked states, or selected options 221 // The input elements are reused, so they keep their existing values 222 expect(newTextInput.value).toBe("john") 223 expect(newCheckbox.checked).toBe(true) 224 expect(newSelect.value).toBe("uk") 225 226 // New attributes should be applied 227 expect(newTextInput.placeholder).toBe("Username") 228 expect(newSelect.className).toBe("country-select") 229 }) 230 231 it("should handle morphInner with browser content", () => { 232 const testContainer = document.createElement("div") 233 testContainer.innerHTML = ` 234 <p>Old paragraph</p> 235 <button>Old button</button> 236 ` 237 document.body.appendChild(testContainer) 238 239 const referenceContainer = document.createElement("div") 240 referenceContainer.innerHTML = ` 241 <h2>New heading</h2> 242 <p>New paragraph</p> 243 <button>New button</button> 244 <span>New span</span> 245 ` 246 247 morphInner(testContainer, referenceContainer) 248 249 expect(testContainer.children.length).toBe(4) 250 expect(testContainer.querySelector("h2")?.textContent).toBe("New heading") 251 expect(testContainer.querySelector("p")?.textContent).toBe("New paragraph") 252 expect(testContainer.querySelector("button")?.textContent).toBe("New button") 253 expect(testContainer.querySelector("span")?.textContent).toBe("New span") 254 255 testContainer.remove() 256 }) 257 258 it("should handle custom elements if supported", () => { 259 // Skip if custom elements are not supported 260 if (!window.customElements) { 261 return 262 } 263 264 // Define a simple custom element 265 class TestElement extends HTMLElement { 266 connectedCallback() { 267 this.innerHTML = "<span>Custom content</span>" 268 } 269 } 270 271 // Register it if not already registered 272 if (!customElements.get("test-element")) { 273 customElements.define("test-element", TestElement) 274 } 275 276 const original = document.createElement("div") 277 original.innerHTML = `<test-element id="custom"></test-element>` 278 container.appendChild(original) 279 280 // Wait for custom element to be upgraded 281 const customEl = original.querySelector("#custom") 282 expect(customEl).toBeTruthy() 283 284 const reference = document.createElement("div") 285 reference.innerHTML = `<test-element id="custom" data-updated="true"></test-element>` 286 287 morph(original, reference) 288 289 const morphedCustom = original.querySelector("#custom") as HTMLElement 290 expect(morphedCustom).toBeTruthy() 291 expect(morphedCustom.getAttribute("data-updated")).toBe("true") 292 }) 293 294 it("should handle real browser viewport and scroll position", () => { 295 // Create a scrollable container 296 const scrollContainer = document.createElement("div") 297 scrollContainer.style.cssText = "height: 200px; overflow-y: scroll; position: relative;" 298 scrollContainer.innerHTML = ` 299 <div style="height: 500px;"> 300 <p id="p1">Paragraph 1</p> 301 <p id="p2" style="margin-top: 200px;">Paragraph 2</p> 302 <p id="p3" style="margin-top: 200px;">Paragraph 3</p> 303 </div> 304 ` 305 container.appendChild(scrollContainer) 306 307 // Scroll to middle 308 scrollContainer.scrollTop = 100 309 const initialScrollTop = scrollContainer.scrollTop 310 311 // Morph with new content 312 const referenceContainer = document.createElement("div") 313 referenceContainer.style.cssText = "height: 200px; overflow-y: scroll; position: relative;" 314 referenceContainer.innerHTML = ` 315 <div style="height: 500px;"> 316 <p id="p1" class="updated">Updated Paragraph 1</p> 317 <p id="p2" style="margin-top: 200px;">Updated Paragraph 2</p> 318 <p id="p3" style="margin-top: 200px;">Updated Paragraph 3</p> 319 <p id="p4" style="margin-top: 200px;">New Paragraph 4</p> 320 </div> 321 ` 322 323 morph(scrollContainer, referenceContainer) 324 325 // Scroll position should be preserved 326 expect(scrollContainer.scrollTop).toBe(initialScrollTop) 327 328 // Content should be updated 329 const p1 = scrollContainer.querySelector("#p1") 330 expect(p1?.className).toBe("updated") 331 expect(p1?.textContent).toBe("Updated Paragraph 1") 332 333 const p4 = scrollContainer.querySelector("#p4") 334 expect(p4).toBeTruthy() 335 expect(p4?.textContent).toBe("New Paragraph 4") 336 }) 337 338 it("should handle table elements properly", () => { 339 const table = document.createElement("table") 340 table.innerHTML = ` 341 <thead> 342 <tr><th>Name</th><th>Age</th></tr> 343 </thead> 344 <tbody> 345 <tr id="row1"><td>Alice</td><td>30</td></tr> 346 <tr id="row2"><td>Bob</td><td>25</td></tr> 347 </tbody> 348 ` 349 container.appendChild(table) 350 351 const row1 = table.querySelector("#row1") 352 const row2 = table.querySelector("#row2") 353 354 const referenceTable = document.createElement("table") 355 referenceTable.className = "updated" 356 referenceTable.innerHTML = ` 357 <thead> 358 <tr><th>Name</th><th>Age</th><th>City</th></tr> 359 </thead> 360 <tbody> 361 <tr id="row1"><td>Alice</td><td>31</td><td>NYC</td></tr> 362 <tr id="row2"><td>Bob</td><td>25</td><td>LA</td></tr> 363 <tr id="row3"><td>Charlie</td><td>35</td><td>SF</td></tr> 364 </tbody> 365 ` 366 367 morph(table, referenceTable) 368 369 expect(table.className).toBe("updated") 370 expect(table.querySelector("#row1")).toBe(row1) 371 expect(table.querySelector("#row2")).toBe(row2) 372 expect(table.querySelectorAll("tbody tr").length).toBe(3) 373 expect(table.querySelectorAll("thead th").length).toBe(3) 374 }) 375 376 it("should handle iframe elements", () => { 377 const div = document.createElement("div") 378 div.innerHTML = '<iframe id="frame1" src="about:blank"></iframe>' 379 container.appendChild(div) 380 381 const frame1 = div.querySelector("#frame1") 382 383 const referenceDiv = document.createElement("div") 384 referenceDiv.innerHTML = '<iframe id="frame1" src="about:blank" title="Updated"></iframe>' 385 386 morph(div, referenceDiv) 387 388 const updatedFrame = div.querySelector("#frame1") as HTMLIFrameElement 389 expect(updatedFrame).toBe(frame1) 390 expect(updatedFrame.title).toBe("Updated") 391 }) 392 393 it("should handle canvas elements", () => { 394 const div = document.createElement("div") 395 const canvas = document.createElement("canvas") 396 canvas.id = "canvas1" 397 canvas.width = 100 398 canvas.height = 100 399 div.appendChild(canvas) 400 container.appendChild(div) 401 402 const ctx = canvas.getContext("2d") 403 if (ctx) { 404 ctx.fillStyle = "red" 405 ctx.fillRect(0, 0, 50, 50) 406 } 407 408 const referenceDiv = document.createElement("div") 409 const referenceCanvas = document.createElement("canvas") 410 referenceCanvas.id = "canvas1" 411 referenceCanvas.width = 200 412 referenceCanvas.height = 200 413 referenceDiv.appendChild(referenceCanvas) 414 415 morph(div, referenceDiv) 416 417 const updatedCanvas = div.querySelector("#canvas1") as HTMLCanvasElement 418 expect(updatedCanvas).toBe(canvas) 419 expect(updatedCanvas.width).toBe(200) 420 expect(updatedCanvas.height).toBe(200) 421 }) 422 423 it("should handle video and audio elements", () => { 424 const div = document.createElement("div") 425 div.innerHTML = ` 426 <video id="vid1" width="320" height="240" controls> 427 <source src="movie.mp4" type="video/mp4"> 428 </video> 429 <audio id="aud1" controls> 430 <source src="audio.mp3" type="audio/mpeg"> 431 </audio> 432 ` 433 container.appendChild(div) 434 435 const video = div.querySelector("#vid1") 436 const audio = div.querySelector("#aud1") 437 438 const referenceDiv = document.createElement("div") 439 referenceDiv.innerHTML = ` 440 <video id="vid1" width="640" height="480" controls autoplay> 441 <source src="movie.mp4" type="video/mp4"> 442 </video> 443 <audio id="aud1" controls loop> 444 <source src="audio.mp3" type="audio/mpeg"> 445 </audio> 446 ` 447 448 morph(div, referenceDiv) 449 450 const updatedVideo = div.querySelector("#vid1") as HTMLVideoElement 451 const updatedAudio = div.querySelector("#aud1") as HTMLAudioElement 452 453 expect(updatedVideo).toBe(video) 454 expect(updatedAudio).toBe(audio) 455 expect(updatedVideo.getAttribute("width")).toBe("640") 456 expect(updatedVideo.hasAttribute("autoplay")).toBe(true) 457 expect(updatedAudio.hasAttribute("loop")).toBe(true) 458 }) 459 460 it("should handle data attributes", () => { 461 const div = document.createElement("div") 462 div.setAttribute("data-user-id", "123") 463 div.setAttribute("data-role", "admin") 464 div.textContent = "User panel" 465 container.appendChild(div) 466 467 const referenceDiv = document.createElement("div") 468 referenceDiv.setAttribute("data-user-id", "456") 469 referenceDiv.setAttribute("data-role", "user") 470 referenceDiv.setAttribute("data-active", "true") 471 referenceDiv.textContent = "User panel" 472 473 morph(div, referenceDiv) 474 475 expect(div.getAttribute("data-user-id")).toBe("456") 476 expect(div.getAttribute("data-role")).toBe("user") 477 expect(div.getAttribute("data-active")).toBe("true") 478 expect(div.dataset.userId).toBe("456") 479 expect(div.dataset.role).toBe("user") 480 expect(div.dataset.active).toBe("true") 481 }) 482 483 it("should handle aria attributes", () => { 484 const button = document.createElement("button") 485 button.setAttribute("aria-label", "Close") 486 button.setAttribute("aria-expanded", "false") 487 button.textContent = "X" 488 container.appendChild(button) 489 490 const referenceButton = document.createElement("button") 491 referenceButton.setAttribute("aria-label", "Open") 492 referenceButton.setAttribute("aria-expanded", "true") 493 referenceButton.setAttribute("aria-controls", "menu") 494 referenceButton.textContent = "☰" 495 496 morph(button, referenceButton) 497 498 expect(button.getAttribute("aria-label")).toBe("Open") 499 expect(button.getAttribute("aria-expanded")).toBe("true") 500 expect(button.getAttribute("aria-controls")).toBe("menu") 501 expect(button.textContent).toBe("☰") 502 }) 503 504 it("should handle style object changes", () => { 505 const div = document.createElement("div") 506 div.style.color = "red" 507 div.style.fontSize = "16px" 508 div.style.padding = "10px" 509 container.appendChild(div) 510 511 const referenceDiv = document.createElement("div") 512 referenceDiv.style.color = "blue" 513 referenceDiv.style.fontSize = "20px" 514 referenceDiv.style.margin = "5px" 515 516 morph(div, referenceDiv) 517 518 expect(div.style.color).toBe("blue") 519 expect(div.style.fontSize).toBe("20px") 520 expect(div.style.margin).toBe("5px") 521 }) 522 523 it("should handle class list manipulation", () => { 524 const div = document.createElement("div") 525 div.className = "class1 class2 class3" 526 container.appendChild(div) 527 528 expect(div.classList.contains("class1")).toBe(true) 529 expect(div.classList.contains("class2")).toBe(true) 530 531 const referenceDiv = document.createElement("div") 532 referenceDiv.className = "class2 class4 class5" 533 534 morph(div, referenceDiv) 535 536 expect(div.classList.contains("class1")).toBe(false) 537 expect(div.classList.contains("class2")).toBe(true) 538 expect(div.classList.contains("class3")).toBe(false) 539 expect(div.classList.contains("class4")).toBe(true) 540 expect(div.classList.contains("class5")).toBe(true) 541 }) 542 543 it("should handle boolean attributes correctly", () => { 544 const button = document.createElement("button") 545 button.disabled = true 546 button.textContent = "Submit" 547 container.appendChild(button) 548 549 const referenceButton = document.createElement("button") 550 referenceButton.textContent = "Submit" 551 // disabled is not set, so it should be removed 552 553 morph(button, referenceButton) 554 555 expect(button.disabled).toBe(false) 556 expect(button.hasAttribute("disabled")).toBe(false) 557 558 // Now add it back 559 const referenceButton2 = document.createElement("button") 560 referenceButton2.disabled = true 561 referenceButton2.textContent = "Submit" 562 563 morph(button, referenceButton2) 564 565 expect(button.disabled).toBe(true) 566 }) 567 568 it("should handle readonly and required attributes on inputs", () => { 569 const input = document.createElement("input") 570 input.type = "text" 571 input.required = true 572 container.appendChild(input) 573 574 const referenceInput = document.createElement("input") 575 referenceInput.type = "text" 576 referenceInput.readOnly = true 577 578 morph(input, referenceInput) 579 580 expect(input.required).toBe(false) 581 expect(input.readOnly).toBe(true) 582 }) 583 584 it("should handle multiple select options", () => { 585 const select = document.createElement("select") 586 select.multiple = true 587 select.innerHTML = ` 588 <option value="1">Option 1</option> 589 <option value="2" selected>Option 2</option> 590 <option value="3">Option 3</option> 591 ` 592 container.appendChild(select) 593 594 const referenceSelect = document.createElement("select") 595 referenceSelect.multiple = true 596 referenceSelect.innerHTML = ` 597 <option value="1">Option 1</option> 598 <option value="2" selected>Option 2</option> 599 <option value="3" selected>Option 3</option> 600 ` 601 602 morph(select, referenceSelect) 603 604 // Selected attributes are updated by default when not modified 605 expect(select.selectedOptions.length).toBe(2) 606 expect(select.selectedOptions[0].value).toBe("2") 607 expect(select.selectedOptions[1].value).toBe("3") 608 }) 609 610 it("should handle script tags safely", () => { 611 const div = document.createElement("div") 612 div.innerHTML = '<div id="content">Content</div>' 613 container.appendChild(div) 614 615 const referenceDiv = document.createElement("div") 616 referenceDiv.innerHTML = '<div id="content">Updated</div><script>console.log("test")</script>' 617 618 morph(div, referenceDiv) 619 620 expect(div.querySelector("#content")?.textContent).toBe("Updated") 621 expect(div.querySelector("script")).toBeTruthy() 622 }) 623 624 it("should handle deep nesting with many levels", () => { 625 const createNested = (depth: number, id: string): string => { 626 if (depth === 0) return `<span id="${id}">Leaf ${id}</span>` 627 return `<div id="level-${depth}"><div>${createNested(depth - 1, id)}</div></div>` 628 } 629 630 const div = document.createElement("div") 631 div.innerHTML = createNested(10, "original") 632 container.appendChild(div) 633 634 const leaf = div.querySelector("#original") 635 636 const referenceDiv = document.createElement("div") 637 referenceDiv.innerHTML = createNested(10, "original") 638 referenceDiv.querySelector("#original")!.textContent = "Leaf updated" 639 640 morph(div, referenceDiv) 641 642 expect(div.querySelector("#original")).toBe(leaf) 643 expect(div.querySelector("#original")?.textContent).toBe("Leaf updated") 644 }) 645 646 it("should handle text nodes with special characters", () => { 647 const div = document.createElement("div") 648 div.textContent = 'Hello <World> & "Friends"' 649 container.appendChild(div) 650 651 const referenceDiv = document.createElement("div") 652 referenceDiv.textContent = "Goodbye <Universe> & 'Enemies'" 653 654 morph(div, referenceDiv) 655 656 expect(div.textContent).toBe("Goodbye <Universe> & 'Enemies'") 657 }) 658 659 it("should handle whitespace preservation", () => { 660 const pre = document.createElement("pre") 661 pre.textContent = "Line 1\n Line 2\n Line 3" 662 container.appendChild(pre) 663 664 const referencePre = document.createElement("pre") 665 referencePre.textContent = "Line 1\n Line 2\n Line 3\nLine 4" 666 667 morph(pre, referencePre) 668 669 expect(pre.textContent).toBe("Line 1\n Line 2\n Line 3\nLine 4") 670 }) 671 672 it("should handle radio button groups", () => { 673 const form = document.createElement("form") 674 form.innerHTML = ` 675 <input type="radio" name="choice" value="a" id="radio-a" checked> 676 <input type="radio" name="choice" value="b" id="radio-b"> 677 ` 678 container.appendChild(form) 679 680 const radioA = form.querySelector("#radio-a") as HTMLInputElement 681 expect(radioA.checked).toBe(true) 682 683 const referenceForm = document.createElement("form") 684 referenceForm.innerHTML = ` 685 <input type="radio" name="choice" value="a" id="radio-a"> 686 <input type="radio" name="choice" value="b" id="radio-b" checked> 687 ` 688 689 morph(form, referenceForm) 690 691 const radioB = form.querySelector("#radio-b") as HTMLInputElement 692 // Checked attributes are updated by default when not modified 693 expect(radioA.checked).toBe(false) 694 expect(radioB.checked).toBe(true) 695 }) 696 697 it("should handle contenteditable elements", () => { 698 const div = document.createElement("div") 699 div.contentEditable = "true" 700 div.textContent = "Editable content" 701 container.appendChild(div) 702 703 // User types something 704 div.textContent = "User modified content" 705 706 const referenceDiv = document.createElement("div") 707 referenceDiv.contentEditable = "true" 708 referenceDiv.textContent = "Server content" 709 710 morph(div, referenceDiv) 711 712 // Content should be updated from server 713 expect(div.textContent).toBe("Server content") 714 expect(div.contentEditable).toBe("true") 715 }) 716 717 it("should handle elements with shadow DOM", () => { 718 const host = document.createElement("div") 719 host.id = "shadow-host" 720 721 // Attach shadow root 722 const shadowRoot = host.attachShadow({ mode: "open" }) 723 shadowRoot.innerHTML = "<p>Shadow content</p>" 724 725 container.appendChild(host) 726 727 const referenceHost = document.createElement("div") 728 referenceHost.id = "shadow-host" 729 referenceHost.setAttribute("data-version", "2") 730 731 morph(host, referenceHost) 732 733 // Shadow root should be preserved 734 expect(host.shadowRoot).toBe(shadowRoot) 735 expect(host.shadowRoot?.innerHTML).toBe("<p>Shadow content</p>") 736 expect(host.getAttribute("data-version")).toBe("2") 737 }) 738 739 it("should handle large attribute sets", () => { 740 const div = document.createElement("div") 741 for (let i = 0; i < 50; i++) { 742 div.setAttribute(`data-attr-${i}`, `value-${i}`) 743 } 744 container.appendChild(div) 745 746 const referenceDiv = document.createElement("div") 747 for (let i = 0; i < 50; i++) { 748 referenceDiv.setAttribute(`data-attr-${i}`, `updated-${i}`) 749 } 750 referenceDiv.setAttribute("data-attr-50", "new-value") 751 752 morph(div, referenceDiv) 753 754 for (let i = 0; i < 50; i++) { 755 expect(div.getAttribute(`data-attr-${i}`)).toBe(`updated-${i}`) 756 } 757 expect(div.getAttribute("data-attr-50")).toBe("new-value") 758 }) 759 760 it("should handle progress and meter elements", () => { 761 const div = document.createElement("div") 762 div.innerHTML = ` 763 <progress id="prog" value="30" max="100"></progress> 764 <meter id="met" value="0.6" min="0" max="1"></meter> 765 ` 766 container.appendChild(div) 767 768 const progress = div.querySelector("#prog") as HTMLProgressElement 769 const meter = div.querySelector("#met") as HTMLMeterElement 770 771 const referenceDiv = document.createElement("div") 772 referenceDiv.innerHTML = ` 773 <progress id="prog" value="70" max="100"></progress> 774 <meter id="met" value="0.8" min="0" max="1" high="0.9" low="0.3"></meter> 775 ` 776 777 morph(div, referenceDiv) 778 779 expect(progress.value).toBe(70) 780 expect(meter.value).toBe(0.8) 781 expect(meter.high).toBe(0.9) 782 expect(meter.low).toBe(0.3) 783 }) 784 785 it("should handle details and summary elements", () => { 786 const details = document.createElement("details") 787 details.open = true 788 details.innerHTML = ` 789 <summary>Click to expand</summary> 790 <p>Hidden content</p> 791 ` 792 container.appendChild(details) 793 794 const referenceDetails = document.createElement("details") 795 referenceDetails.innerHTML = ` 796 <summary>Click to collapse</summary> 797 <p>Visible content</p> 798 ` 799 800 morph(details, referenceDetails) 801 802 expect(details.open).toBe(false) 803 expect(details.querySelector("summary")?.textContent).toBe("Click to collapse") 804 expect(details.querySelector("p")?.textContent).toBe("Visible content") 805 }) 806 807 it("should preserve element references across morphs", () => { 808 const button = document.createElement("button") 809 button.id = "my-btn" 810 button.textContent = "Click" 811 container.appendChild(button) 812 813 const buttonRef = button 814 let clickCount = 0 815 816 button.addEventListener("click", () => { 817 clickCount++ 818 }) 819 820 // Morph multiple times 821 for (let i = 1; i <= 5; i++) { 822 const reference = document.createElement("button") 823 reference.id = "my-btn" 824 reference.textContent = `Click ${i}` 825 reference.setAttribute("data-version", i.toString()) 826 827 morph(button, reference) 828 829 expect(button).toBe(buttonRef) 830 expect(button.textContent).toBe(`Click ${i}`) 831 expect(button.getAttribute("data-version")).toBe(i.toString()) 832 } 833 834 button.click() 835 expect(clickCount).toBe(1) 836 }) 837 }) 838})