Precise DOM morphing
morphing
typescript
dom
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})