Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve tests

+1594 -1
+1
.gitignore
··· 2 2 .DS_Store 3 3 dist 4 4 coverage 5 + reference
+1 -1
AGENTS.md
··· 1 1 - I’m using bun to manage packages. 2 2 - Don’t create a summary document. 3 - - Running all the tests with `bun run test` is cheap, so do it all the time. Don’t do too much before running tests. 3 + - Running all the tests with `bun run test` is cheap, so do it all the time. Don’t do too much before running tests. You can also run browser tests with `bun run test:browser`. 4 4 - Try to maintain 100% test coverage. Use `bun run test --coverage`. 5 5 - I’m using `jj` so you can use that to look at your diff, but please don’t commit unless I ask you to. 6 6 - Make sure you leave things in a good state. No diagnostics warnings. No type errors.
+519
test/morphlex-idiomorph.test.ts
··· 1 + import { describe, it, expect, beforeEach, afterEach } from "vitest" 2 + import { morph, morphInner } from "../src/morphlex" 3 + 4 + describe("Idiomorph-style tests", () => { 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 + function parseHTML(html: string): HTMLElement { 19 + const tmp = document.createElement("div") 20 + tmp.innerHTML = html.trim() 21 + return tmp.firstChild as HTMLElement 22 + } 23 + 24 + describe("basic morphing with different content types", () => { 25 + it("should morph with single node", () => { 26 + const initial = parseHTML("<button>Foo</button>") 27 + container.appendChild(initial) 28 + 29 + const final = document.createElement("button") 30 + final.textContent = "Bar" 31 + 32 + morph(initial, final) 33 + 34 + expect(initial.textContent).toBe("Bar") 35 + }) 36 + 37 + it("should morph with string", () => { 38 + const initial = parseHTML("<button>Foo</button>") 39 + container.appendChild(initial) 40 + 41 + morph(initial, "<button>Bar</button>") 42 + 43 + expect(initial.textContent).toBe("Bar") 44 + }) 45 + }) 46 + 47 + describe("morphInner functionality", () => { 48 + it("should morph innerHTML with string", () => { 49 + const div = parseHTML("<div><span>Old</span></div>") 50 + container.appendChild(div) 51 + 52 + morphInner(div, "<div><span>New</span></div>") 53 + 54 + expect(div.innerHTML).toBe("<span>New</span>") 55 + }) 56 + 57 + it("should morph innerHTML with element", () => { 58 + const div = parseHTML("<div><span>Old</span></div>") 59 + container.appendChild(div) 60 + 61 + const newDiv = document.createElement("div") 62 + const newSpan = document.createElement("span") 63 + newSpan.textContent = "New" 64 + newDiv.appendChild(newSpan) 65 + 66 + morphInner(div, newDiv) 67 + 68 + expect(div.innerHTML).toBe("<span>New</span>") 69 + }) 70 + 71 + it("should clear children when morphing to empty", () => { 72 + const div = parseHTML("<div><span>Old</span></div>") 73 + container.appendChild(div) 74 + 75 + morphInner(div, "<div></div>") 76 + 77 + expect(div.innerHTML).toBe("") 78 + }) 79 + 80 + it("should add multiple children", () => { 81 + const div = parseHTML("<div></div>") 82 + container.appendChild(div) 83 + 84 + morphInner(div, "<div><i>A</i><b>B</b></div>") 85 + 86 + expect(div.innerHTML).toBe("<i>A</i><b>B</b>") 87 + }) 88 + }) 89 + 90 + describe("special elements", () => { 91 + it("should handle numeric IDs", () => { 92 + const initial = parseHTML('<div id="123">Old</div>') 93 + const final = parseHTML('<div id="123">New</div>') 94 + 95 + morph(initial, final) 96 + 97 + expect(initial.textContent).toBe("New") 98 + }) 99 + }) 100 + 101 + describe("input value handling", () => { 102 + it("should morph input value correctly", () => { 103 + const initial = parseHTML('<input type="text" value="foo">') 104 + container.appendChild(initial) 105 + 106 + const final = parseHTML('<input type="text" value="bar">') 107 + 108 + morph(initial, final) 109 + 110 + expect((initial as HTMLInputElement).value).toBe("bar") 111 + }) 112 + 113 + it("should morph textarea value property", () => { 114 + const initial = parseHTML("<textarea>foo</textarea>") 115 + container.appendChild(initial) 116 + 117 + const final = parseHTML("<textarea>bar</textarea>") 118 + 119 + morph(initial, final) 120 + 121 + expect((initial as HTMLTextAreaElement).value).toBe("bar") 122 + }) 123 + 124 + it("should handle textarea value changes", () => { 125 + const initial = parseHTML("<textarea>foo</textarea>") 126 + container.appendChild(initial) 127 + ;(initial as HTMLTextAreaElement).value = "user input" 128 + 129 + const final = parseHTML("<textarea>bar</textarea>") 130 + 131 + morph(initial, final) 132 + 133 + expect((initial as HTMLTextAreaElement).value).toBe("bar") 134 + }) 135 + }) 136 + 137 + describe("checkbox handling", () => { 138 + it("should remove checked attribute", () => { 139 + const parent = parseHTML('<div><input type="checkbox" checked></div>') 140 + container.appendChild(parent) 141 + 142 + const input = parent.querySelector("input") as HTMLInputElement 143 + 144 + morph(input, '<input type="checkbox">') 145 + 146 + expect(input.checked).toBe(false) 147 + }) 148 + 149 + it("should add checked attribute", () => { 150 + const parent = parseHTML('<div><input type="checkbox"></div>') 151 + container.appendChild(parent) 152 + 153 + const input = parent.querySelector("input") as HTMLInputElement 154 + 155 + morph(input, '<input type="checkbox" checked>') 156 + 157 + expect(input.checked).toBe(true) 158 + }) 159 + 160 + it("should set checked property to true", () => { 161 + const parent = parseHTML('<div><input type="checkbox" checked></div>') 162 + container.appendChild(parent) 163 + 164 + const input = parent.querySelector("input") as HTMLInputElement 165 + input.checked = false 166 + 167 + morph(input, '<input type="checkbox" checked>') 168 + 169 + expect(input.checked).toBe(true) 170 + }) 171 + 172 + it("should set checked property to false", () => { 173 + const parent = parseHTML('<div><input type="checkbox"></div>') 174 + container.appendChild(parent) 175 + 176 + const input = parent.querySelector("input") as HTMLInputElement 177 + input.checked = true 178 + 179 + morph(input, '<input type="checkbox">') 180 + 181 + expect(input.checked).toBe(false) 182 + }) 183 + }) 184 + 185 + describe("select element handling", () => { 186 + it("should remove selected option", () => { 187 + const parent = parseHTML(` 188 + <div> 189 + <select> 190 + <option>0</option> 191 + <option selected>1</option> 192 + </select> 193 + </div> 194 + `) 195 + container.appendChild(parent) 196 + 197 + const select = parent.querySelector("select") as HTMLSelectElement 198 + const options = parent.querySelectorAll("option") 199 + 200 + expect(select.selectedIndex).toBe(1) 201 + expect(options[1].selected).toBe(true) 202 + 203 + morphInner( 204 + parent, 205 + `<div> 206 + <select> 207 + <option>0</option> 208 + <option>1</option> 209 + </select> 210 + </div>`, 211 + ) 212 + 213 + expect(select.selectedIndex).toBe(0) 214 + expect(options[0].selected).toBe(true) 215 + expect(options[1].selected).toBe(false) 216 + }) 217 + 218 + it("should add selected option", () => { 219 + const parent = parseHTML(` 220 + <div> 221 + <select> 222 + <option>0</option> 223 + <option>1</option> 224 + </select> 225 + </div> 226 + `) 227 + container.appendChild(parent) 228 + 229 + const select = parent.querySelector("select") as HTMLSelectElement 230 + const options = parent.querySelectorAll("option") 231 + 232 + expect(select.selectedIndex).toBe(0) 233 + 234 + morphInner( 235 + parent, 236 + `<div> 237 + <select> 238 + <option>0</option> 239 + <option selected>1</option> 240 + </select> 241 + </div>`, 242 + ) 243 + 244 + expect(select.selectedIndex).toBe(1) 245 + expect(options[0].selected).toBe(false) 246 + expect(options[1].selected).toBe(true) 247 + }) 248 + }) 249 + 250 + describe("complex scenarios", () => { 251 + it("should not build ID in new content parent into persistent id set", () => { 252 + const initial = parseHTML('<div id="a"><div id="b">B</div></div>') 253 + container.appendChild(initial) 254 + 255 + const finalSrc = parseHTML('<div id="b">B Updated</div>') 256 + 257 + morph(initial, finalSrc) 258 + 259 + expect(initial.textContent).toBe("B Updated") 260 + }) 261 + 262 + it("should handle soft match abortion on two future soft matches", () => { 263 + const initial = parseHTML(` 264 + <div> 265 + <span>A</span> 266 + <span>B</span> 267 + <span>C</span> 268 + </div> 269 + `) 270 + container.appendChild(initial) 271 + 272 + const final = parseHTML(` 273 + <div> 274 + <span>X</span> 275 + <span>B</span> 276 + <span>C</span> 277 + </div> 278 + `) 279 + 280 + morph(initial, final) 281 + 282 + expect(initial.children[0].textContent).toBe("X") 283 + expect(initial.children[1].textContent).toBe("B") 284 + expect(initial.children[2].textContent).toBe("C") 285 + }) 286 + }) 287 + 288 + describe("edge cases", () => { 289 + it("should preserve elements during complex morphing", () => { 290 + const parent = parseHTML(` 291 + <div> 292 + <div id="outer"> 293 + <div id="inner"> 294 + <span id="a">A</span> 295 + <span id="b">B</span> 296 + <span id="c">C</span> 297 + </div> 298 + </div> 299 + </div> 300 + `) 301 + container.appendChild(parent) 302 + 303 + const aEl = parent.querySelector("#a") 304 + const bEl = parent.querySelector("#b") 305 + const cEl = parent.querySelector("#c") 306 + 307 + const final = parseHTML(` 308 + <div> 309 + <div id="outer"> 310 + <div id="inner"> 311 + <span id="c">C Modified</span> 312 + <span id="a">A Modified</span> 313 + <span id="b">B Modified</span> 314 + </div> 315 + </div> 316 + </div> 317 + `) 318 + 319 + morph(parent, final) 320 + 321 + // Elements should be preserved 322 + expect(parent.querySelector("#a")).toBe(aEl) 323 + expect(parent.querySelector("#b")).toBe(bEl) 324 + expect(parent.querySelector("#c")).toBe(cEl) 325 + 326 + // Content should be updated 327 + expect(aEl?.textContent).toBe("A Modified") 328 + expect(bEl?.textContent).toBe("B Modified") 329 + expect(cEl?.textContent).toBe("C Modified") 330 + }) 331 + 332 + it("should handle deeply nested structure changes", () => { 333 + const parent = parseHTML(` 334 + <div> 335 + <section id="sec1"> 336 + <article id="art1"> 337 + <p id="p1">Paragraph 1</p> 338 + <p id="p2">Paragraph 2</p> 339 + </article> 340 + </section> 341 + </div> 342 + `) 343 + container.appendChild(parent) 344 + 345 + const final = parseHTML(` 346 + <div> 347 + <section id="sec1"> 348 + <article id="art1"> 349 + <p id="p2">Paragraph 2 Updated</p> 350 + <p id="p1">Paragraph 1 Updated</p> 351 + </article> 352 + </section> 353 + </div> 354 + `) 355 + 356 + morph(parent, final) 357 + 358 + expect(parent.querySelector("#p1")?.textContent).toBe("Paragraph 1 Updated") 359 + expect(parent.querySelector("#p2")?.textContent).toBe("Paragraph 2 Updated") 360 + }) 361 + 362 + it("should handle attribute changes on nested elements", () => { 363 + const parent = parseHTML(` 364 + <div> 365 + <button id="btn1" class="old">Click</button> 366 + </div> 367 + `) 368 + container.appendChild(parent) 369 + 370 + const final = parseHTML(` 371 + <div> 372 + <button id="btn1" class="new" disabled>Click</button> 373 + </div> 374 + `) 375 + 376 + morph(parent, final) 377 + 378 + const button = parent.querySelector("#btn1") as HTMLButtonElement 379 + expect(button.className).toBe("new") 380 + expect(button.disabled).toBe(true) 381 + }) 382 + 383 + it("should handle mixed content morphing", () => { 384 + const parent = parseHTML(` 385 + <div> 386 + Text node 387 + <span>Span</span> 388 + More text 389 + </div> 390 + `) 391 + container.appendChild(parent) 392 + 393 + const final = parseHTML(` 394 + <div> 395 + Updated text 396 + <span>Updated span</span> 397 + Final text 398 + </div> 399 + `) 400 + 401 + morph(parent, final) 402 + 403 + expect(parent.textContent?.replace(/\s+/g, " ").trim()).toBe("Updated text Updated span Final text") 404 + }) 405 + }) 406 + 407 + describe("id preservation", () => { 408 + it("should preserve elements with matching IDs across different positions", () => { 409 + const parent = parseHTML(` 410 + <ul> 411 + <li id="item-1">Item 1</li> 412 + <li id="item-2">Item 2</li> 413 + <li id="item-3">Item 3</li> 414 + </ul> 415 + `) 416 + container.appendChild(parent) 417 + 418 + const item1 = parent.querySelector("#item-1") 419 + const item2 = parent.querySelector("#item-2") 420 + const item3 = parent.querySelector("#item-3") 421 + 422 + const final = parseHTML(` 423 + <ul> 424 + <li id="item-3">Item 3</li> 425 + <li id="item-1">Item 1</li> 426 + <li id="item-2">Item 2</li> 427 + </ul> 428 + `) 429 + 430 + morph(parent, final) 431 + 432 + expect(parent.querySelector("#item-1")).toBe(item1) 433 + expect(parent.querySelector("#item-2")).toBe(item2) 434 + expect(parent.querySelector("#item-3")).toBe(item3) 435 + }) 436 + 437 + it("should handle ID changes correctly", () => { 438 + const parent = parseHTML(` 439 + <div> 440 + <span id="old-id">Content</span> 441 + </div> 442 + `) 443 + container.appendChild(parent) 444 + 445 + const final = parseHTML(` 446 + <div> 447 + <span id="new-id">Content</span> 448 + </div> 449 + `) 450 + 451 + morph(parent, final) 452 + 453 + expect(parent.querySelector("#old-id")).toBeNull() 454 + expect(parent.querySelector("#new-id")).toBeTruthy() 455 + expect(parent.querySelector("#new-id")?.textContent).toBe("Content") 456 + }) 457 + }) 458 + 459 + describe("performance scenarios", () => { 460 + it("should handle large lists efficiently", () => { 461 + let fromHTML = "<ul>" 462 + for (let i = 0; i < 50; i++) { 463 + fromHTML += `<li id="item-${i}">Item ${i}</li>` 464 + } 465 + fromHTML += "</ul>" 466 + 467 + let toHTML = "<ul>" 468 + for (let i = 0; i < 50; i++) { 469 + toHTML += `<li id="item-${i}">Item ${i} Updated</li>` 470 + } 471 + toHTML += "</ul>" 472 + 473 + const from = parseHTML(fromHTML) 474 + const to = parseHTML(toHTML) 475 + container.appendChild(from) 476 + 477 + const originalElements = Array.from(from.children).map((el) => el) 478 + 479 + morph(from, to) 480 + 481 + // All elements should be preserved 482 + expect(from.children.length).toBe(50) 483 + for (let i = 0; i < 50; i++) { 484 + expect(from.children[i]).toBe(originalElements[i]) 485 + expect(from.children[i].textContent).toBe(`Item ${i} Updated`) 486 + } 487 + }) 488 + 489 + it("should handle reordering large lists", () => { 490 + let fromHTML = "<ul>" 491 + for (let i = 0; i < 20; i++) { 492 + fromHTML += `<li id="item-${i}">Item ${i}</li>` 493 + } 494 + fromHTML += "</ul>" 495 + 496 + let toHTML = "<ul>" 497 + for (let i = 19; i >= 0; i--) { 498 + toHTML += `<li id="item-${i}">Item ${i}</li>` 499 + } 500 + toHTML += "</ul>" 501 + 502 + const from = parseHTML(fromHTML) 503 + const to = parseHTML(toHTML) 504 + container.appendChild(from) 505 + 506 + const elementMap = new Map() 507 + for (let i = 0; i < 20; i++) { 508 + elementMap.set(`item-${i}`, from.querySelector(`#item-${i}`)) 509 + } 510 + 511 + morph(from, to) 512 + 513 + // All elements should be preserved 514 + for (let i = 0; i < 20; i++) { 515 + expect(from.querySelector(`#item-${i}`)).toBe(elementMap.get(`item-${i}`)) 516 + } 517 + }) 518 + }) 519 + })
+571
test/morphlex-morphdom.test.ts
··· 1 + import { describe, it, expect, beforeEach, afterEach } from "vitest" 2 + import { morph } from "../src/morphlex" 3 + 4 + describe("Morphdom-style fixture tests", () => { 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 + function parseHTML(html: string): HTMLElement { 19 + const tmp = document.createElement("div") 20 + tmp.innerHTML = html.trim() 21 + return tmp.firstChild as HTMLElement 22 + } 23 + 24 + describe("simple morphing", () => { 25 + it("should add new element before existing element", () => { 26 + const from = parseHTML("<div><b>bold</b></div>") 27 + const to = parseHTML("<div><i>italics</i><b>bold</b></div>") 28 + 29 + morph(from, to) 30 + 31 + expect(from.innerHTML).toBe("<i>italics</i><b>bold</b>") 32 + }) 33 + 34 + it("should handle equal elements", () => { 35 + const from = parseHTML("<div>test</div>") 36 + const to = parseHTML("<div>test</div>") 37 + 38 + morph(from, to) 39 + 40 + expect(from.innerHTML).toBe("test") 41 + }) 42 + 43 + it("should shorten list of children", () => { 44 + const from = parseHTML("<div><span>1</span><span>2</span><span>3</span></div>") 45 + const to = parseHTML("<div><span>1</span></div>") 46 + 47 + morph(from, to) 48 + 49 + expect(from.children.length).toBe(1) 50 + expect(from.innerHTML).toBe("<span>1</span>") 51 + }) 52 + 53 + it("should lengthen list of children", () => { 54 + const from = parseHTML("<div><span>1</span></div>") 55 + const to = parseHTML("<div><span>1</span><span>2</span><span>3</span></div>") 56 + 57 + morph(from, to) 58 + 59 + expect(from.children.length).toBe(3) 60 + expect(from.innerHTML).toBe("<span>1</span><span>2</span><span>3</span>") 61 + }) 62 + 63 + it("should reverse children", () => { 64 + const from = parseHTML("<div><span>a</span><span>b</span><span>c</span></div>") 65 + const to = parseHTML("<div><span>c</span><span>b</span><span>a</span></div>") 66 + 67 + morph(from, to) 68 + 69 + expect(from.innerHTML).toBe("<span>c</span><span>b</span><span>a</span>") 70 + }) 71 + }) 72 + 73 + describe("attribute handling", () => { 74 + it("should handle empty string attribute values", () => { 75 + const from = parseHTML('<div class="foo"></div>') 76 + const to = parseHTML('<div class=""></div>') 77 + 78 + morph(from, to) 79 + 80 + expect(from.getAttribute("class")).toBe("") 81 + }) 82 + }) 83 + 84 + describe("input elements", () => { 85 + it("should morph input element", () => { 86 + const from = parseHTML('<input type="text" value="Hello">') 87 + const to = parseHTML('<input type="text" value="World">') 88 + 89 + morph(from, to) 90 + 91 + expect((from as HTMLInputElement).value).toBe("World") 92 + }) 93 + 94 + it("should add disabled attribute to input", () => { 95 + const from = parseHTML('<input type="text" value="Hello World">') 96 + const to = parseHTML('<input type="text" value="Hello World" disabled>') 97 + 98 + morph(from, to) 99 + 100 + expect((from as HTMLInputElement).disabled).toBe(true) 101 + }) 102 + 103 + it("should remove disabled attribute from input", () => { 104 + const from = parseHTML('<input type="text" value="Hello World" disabled>') 105 + const to = parseHTML('<input type="text" value="Hello World">') 106 + 107 + morph(from, to) 108 + 109 + expect((from as HTMLInputElement).disabled).toBe(false) 110 + }) 111 + }) 112 + 113 + describe("select elements", () => { 114 + it("should handle select element with selected option", () => { 115 + const from = parseHTML(` 116 + <select> 117 + <option value="1">One</option> 118 + <option value="2" selected>Two</option> 119 + <option value="3">Three</option> 120 + </select> 121 + `) 122 + const to = parseHTML(` 123 + <select> 124 + <option value="1" selected>One</option> 125 + <option value="2">Two</option> 126 + <option value="3">Three</option> 127 + </select> 128 + `) 129 + 130 + morph(from, to) 131 + 132 + const select = from as HTMLSelectElement 133 + expect(select.value).toBe("1") 134 + expect(select.options[0].selected).toBe(true) 135 + expect(select.options[1].selected).toBe(false) 136 + }) 137 + 138 + it("should handle select element with default selection", () => { 139 + const from = parseHTML(` 140 + <select> 141 + <option value="1">One</option> 142 + <option value="2">Two</option> 143 + <option value="3">Three</option> 144 + </select> 145 + `) 146 + const to = parseHTML(` 147 + <select> 148 + <option value="1">One</option> 149 + <option value="2" selected>Two</option> 150 + <option value="3">Three</option> 151 + </select> 152 + `) 153 + 154 + morph(from, to) 155 + 156 + const select = from as HTMLSelectElement 157 + expect(select.value).toBe("2") 158 + expect(select.options[1].selected).toBe(true) 159 + }) 160 + }) 161 + 162 + describe("id-based morphing", () => { 163 + it("should handle nested elements with IDs", () => { 164 + const from = parseHTML(` 165 + <div> 166 + <div id="a">A</div> 167 + <div id="b">B</div> 168 + </div> 169 + `) 170 + const to = parseHTML(` 171 + <div> 172 + <div id="b">B Updated</div> 173 + <div id="a">A Updated</div> 174 + </div> 175 + `) 176 + 177 + const aEl = from.querySelector("#a") 178 + const bEl = from.querySelector("#b") 179 + 180 + morph(from, to) 181 + 182 + // Elements with IDs should be preserved 183 + expect(from.querySelector("#a")).toBe(aEl) 184 + expect(from.querySelector("#b")).toBe(bEl) 185 + expect(from.querySelector("#a")?.textContent).toBe("A Updated") 186 + expect(from.querySelector("#b")?.textContent).toBe("B Updated") 187 + }) 188 + 189 + it("should handle reversing elements with IDs", () => { 190 + const from = parseHTML(` 191 + <div> 192 + <div id="a">a</div> 193 + <div id="b">b</div> 194 + <div id="c">c</div> 195 + </div> 196 + `) 197 + const to = parseHTML(` 198 + <div> 199 + <div id="c">c</div> 200 + <div id="b">b</div> 201 + <div id="a">a</div> 202 + </div> 203 + `) 204 + 205 + const aEl = from.querySelector("#a") 206 + const bEl = from.querySelector("#b") 207 + const cEl = from.querySelector("#c") 208 + 209 + morph(from, to) 210 + 211 + expect(from.querySelector("#a")).toBe(aEl) 212 + expect(from.querySelector("#b")).toBe(bEl) 213 + expect(from.querySelector("#c")).toBe(cEl) 214 + }) 215 + 216 + it("should handle prepending element with ID", () => { 217 + const from = parseHTML(` 218 + <div> 219 + <div id="a">a</div> 220 + <div id="b">b</div> 221 + </div> 222 + `) 223 + const to = parseHTML(` 224 + <div> 225 + <div id="c">c</div> 226 + <div id="a">a</div> 227 + <div id="b">b</div> 228 + </div> 229 + `) 230 + 231 + const aEl = from.querySelector("#a") 232 + const bEl = from.querySelector("#b") 233 + 234 + morph(from, to) 235 + 236 + expect(from.querySelector("#a")).toBe(aEl) 237 + expect(from.querySelector("#b")).toBe(bEl) 238 + expect(from.children.length).toBe(3) 239 + expect(from.children[0].id).toBe("c") 240 + }) 241 + 242 + it("should handle changing tag name with ID preservation", () => { 243 + const from = parseHTML(` 244 + <div> 245 + <div id="a">A</div> 246 + </div> 247 + `) 248 + const to = parseHTML(` 249 + <div> 250 + <span id="a">A</span> 251 + </div> 252 + `) 253 + 254 + morph(from, to) 255 + 256 + expect(from.querySelector("#a")?.tagName).toBe("SPAN") 257 + }) 258 + }) 259 + 260 + describe("tag name changes", () => { 261 + it("should change tag name", () => { 262 + const from = parseHTML("<div><b>Hello</b></div>") 263 + const to = parseHTML("<div><i>Hello</i></div>") 264 + 265 + morph(from, to) 266 + 267 + expect(from.innerHTML).toBe("<i>Hello</i>") 268 + }) 269 + 270 + it("should change tag name with IDs", () => { 271 + const from = parseHTML(` 272 + <div> 273 + <div id="a">A</div> 274 + <div id="b">B</div> 275 + </div> 276 + `) 277 + const to = parseHTML(` 278 + <div> 279 + <span id="a">A</span> 280 + <span id="b">B</span> 281 + </div> 282 + `) 283 + 284 + morph(from, to) 285 + 286 + expect(from.querySelector("#a")?.tagName).toBe("SPAN") 287 + expect(from.querySelector("#b")?.tagName).toBe("SPAN") 288 + }) 289 + }) 290 + 291 + describe("SVG elements", () => { 292 + it("should handle SVG elements", () => { 293 + const from = parseHTML(` 294 + <svg> 295 + <circle cx="50" cy="50" r="40"></circle> 296 + </svg> 297 + `) 298 + const to = parseHTML(` 299 + <svg> 300 + <circle cx="50" cy="50" r="40"></circle> 301 + <rect x="10" y="10" width="30" height="30"></rect> 302 + </svg> 303 + `) 304 + 305 + morph(from, to) 306 + 307 + expect(from.children.length).toBe(2) 308 + expect(from.children[0].tagName.toLowerCase()).toBe("circle") 309 + expect(from.children[1].tagName.toLowerCase()).toBe("rect") 310 + }) 311 + 312 + it("should append new SVG elements", () => { 313 + const from = parseHTML('<svg><circle cx="10" cy="10" r="5"></circle></svg>') 314 + const to = parseHTML(` 315 + <svg> 316 + <circle cx="10" cy="10" r="5"></circle> 317 + <circle cx="20" cy="20" r="5"></circle> 318 + </svg> 319 + `) 320 + 321 + morph(from, to) 322 + 323 + expect(from.children.length).toBe(2) 324 + }) 325 + }) 326 + 327 + describe("data table tests", () => { 328 + it("should handle complex data table morphing", () => { 329 + const from = parseHTML(` 330 + <table> 331 + <tbody> 332 + <tr><td>A</td><td>B</td></tr> 333 + <tr><td>C</td><td>D</td></tr> 334 + </tbody> 335 + </table> 336 + `) 337 + const to = parseHTML(` 338 + <table> 339 + <tbody> 340 + <tr><td>A</td><td>B</td><td>E</td></tr> 341 + <tr><td>C</td><td>D</td><td>F</td></tr> 342 + </tbody> 343 + </table> 344 + `) 345 + 346 + morph(from, to) 347 + 348 + const rows = from.querySelectorAll("tr") 349 + expect(rows.length).toBe(2) 350 + expect(rows[0].children.length).toBe(3) 351 + expect(rows[0].children[2].textContent).toBe("E") 352 + expect(rows[1].children[2].textContent).toBe("F") 353 + }) 354 + 355 + it("should handle data table with row modifications", () => { 356 + const from = parseHTML(` 357 + <table> 358 + <tbody> 359 + <tr><td>1</td></tr> 360 + <tr><td>2</td></tr> 361 + <tr><td>3</td></tr> 362 + </tbody> 363 + </table> 364 + `) 365 + const to = parseHTML(` 366 + <table> 367 + <tbody> 368 + <tr><td>1</td></tr> 369 + <tr><td>2 Updated</td></tr> 370 + <tr><td>3</td></tr> 371 + <tr><td>4</td></tr> 372 + </tbody> 373 + </table> 374 + `) 375 + 376 + morph(from, to) 377 + 378 + const rows = from.querySelectorAll("tr") 379 + expect(rows.length).toBe(4) 380 + expect(rows[1].textContent).toBe("2 Updated") 381 + expect(rows[3].textContent).toBe("4") 382 + }) 383 + }) 384 + 385 + describe("nested id scenarios", () => { 386 + it("should handle deeply nested IDs - scenario 2", () => { 387 + const from = parseHTML(` 388 + <div> 389 + <div id="outer"> 390 + <div id="a">A</div> 391 + <div id="b">B</div> 392 + </div> 393 + </div> 394 + `) 395 + const to = parseHTML(` 396 + <div> 397 + <div id="outer"> 398 + <div id="b">B</div> 399 + <div id="a">A</div> 400 + </div> 401 + </div> 402 + `) 403 + 404 + const aEl = from.querySelector("#a") 405 + const bEl = from.querySelector("#b") 406 + 407 + morph(from, to) 408 + 409 + expect(from.querySelector("#a")).toBe(aEl) 410 + expect(from.querySelector("#b")).toBe(bEl) 411 + }) 412 + 413 + it("should handle deeply nested IDs - scenario 3", () => { 414 + const from = parseHTML(` 415 + <div> 416 + <div id="outer"> 417 + <div> 418 + <div id="a">A</div> 419 + <div id="b">B</div> 420 + </div> 421 + </div> 422 + </div> 423 + `) 424 + const to = parseHTML(` 425 + <div> 426 + <div id="outer"> 427 + <div> 428 + <div id="b">B</div> 429 + <div id="a">A</div> 430 + </div> 431 + </div> 432 + </div> 433 + `) 434 + 435 + const aEl = from.querySelector("#a") 436 + const bEl = from.querySelector("#b") 437 + 438 + morph(from, to) 439 + 440 + expect(from.querySelector("#a")).toBe(aEl) 441 + expect(from.querySelector("#b")).toBe(bEl) 442 + }) 443 + 444 + it("should handle deeply nested IDs - scenario 4", () => { 445 + const from = parseHTML(` 446 + <div> 447 + <div id="outer"> 448 + <div id="inner"> 449 + <div id="a">A</div> 450 + <div id="b">B</div> 451 + </div> 452 + </div> 453 + </div> 454 + `) 455 + const to = parseHTML(` 456 + <div> 457 + <div id="outer"> 458 + <div id="inner"> 459 + <div id="b">B</div> 460 + <div id="a">A</div> 461 + </div> 462 + </div> 463 + </div> 464 + `) 465 + 466 + const aEl = from.querySelector("#a") 467 + const bEl = from.querySelector("#b") 468 + 469 + morph(from, to) 470 + 471 + expect(from.querySelector("#a")).toBe(aEl) 472 + expect(from.querySelector("#b")).toBe(bEl) 473 + }) 474 + 475 + it("should handle deeply nested IDs - scenario 5", () => { 476 + const from = parseHTML(` 477 + <div> 478 + <div id="a"> 479 + <div id="b">B</div> 480 + </div> 481 + </div> 482 + `) 483 + const to = parseHTML(` 484 + <div> 485 + <div id="a">A</div> 486 + <div id="b">B</div> 487 + </div> 488 + `) 489 + 490 + morph(from, to) 491 + 492 + expect(from.querySelector("#a")?.textContent?.trim()).toBe("A") 493 + expect(from.querySelector("#b")?.textContent).toBe("B") 494 + }) 495 + 496 + it("should handle deeply nested IDs - scenario 6", () => { 497 + const from = parseHTML(` 498 + <div> 499 + <div id="a">A</div> 500 + <div id="b">B</div> 501 + </div> 502 + `) 503 + const to = parseHTML(` 504 + <div> 505 + <div id="a"> 506 + <div id="b">B</div> 507 + </div> 508 + </div> 509 + `) 510 + 511 + morph(from, to) 512 + 513 + expect(from.querySelector("#a #b")?.textContent).toBe("B") 514 + }) 515 + 516 + it("should handle deeply nested IDs - scenario 7", () => { 517 + const from = parseHTML(` 518 + <div> 519 + <div id="a"> 520 + <div id="b"> 521 + <div id="c">C</div> 522 + </div> 523 + </div> 524 + </div> 525 + `) 526 + const to = parseHTML(` 527 + <div> 528 + <div id="a">A</div> 529 + <div id="b">B</div> 530 + <div id="c">C</div> 531 + </div> 532 + `) 533 + 534 + morph(from, to) 535 + 536 + expect(from.children.length).toBe(3) 537 + expect(from.querySelector("#a")?.textContent?.trim()).toBe("A") 538 + expect(from.querySelector("#b")?.textContent?.trim()).toBe("B") 539 + expect(from.querySelector("#c")?.textContent).toBe("C") 540 + }) 541 + }) 542 + 543 + describe("large document morphing", () => { 544 + it("should handle large DOM trees efficiently", () => { 545 + let fromHTML = "<div>" 546 + let toHTML = "<div>" 547 + 548 + for (let i = 0; i < 100; i++) { 549 + fromHTML += `<div id="item-${i}">Item ${i}</div>` 550 + toHTML += `<div id="item-${i}">Item ${i} Updated</div>` 551 + } 552 + 553 + fromHTML += "</div>" 554 + toHTML += "</div>" 555 + 556 + const from = parseHTML(fromHTML) 557 + const to = parseHTML(toHTML) 558 + 559 + const originalElements = Array.from(from.children).map(el => el) 560 + 561 + morph(from, to) 562 + 563 + // All elements should be preserved 564 + expect(from.children.length).toBe(100) 565 + for (let i = 0; i < 100; i++) { 566 + expect(from.children[i]).toBe(originalElements[i]) 567 + expect(from.children[i].textContent).toBe(`Item ${i} Updated`) 568 + } 569 + }) 570 + }) 571 + })
+502
test/morphlex.browser.test.ts
··· 332 332 expect(p4).toBeTruthy() 333 333 expect(p4?.textContent).toBe("New Paragraph 4") 334 334 }) 335 + 336 + it("should handle table elements properly", () => { 337 + const table = document.createElement("table") 338 + table.innerHTML = ` 339 + <thead> 340 + <tr><th>Name</th><th>Age</th></tr> 341 + </thead> 342 + <tbody> 343 + <tr id="row1"><td>Alice</td><td>30</td></tr> 344 + <tr id="row2"><td>Bob</td><td>25</td></tr> 345 + </tbody> 346 + ` 347 + container.appendChild(table) 348 + 349 + const row1 = table.querySelector("#row1") 350 + const row2 = table.querySelector("#row2") 351 + 352 + const referenceTable = document.createElement("table") 353 + referenceTable.className = "updated" 354 + referenceTable.innerHTML = ` 355 + <thead> 356 + <tr><th>Name</th><th>Age</th><th>City</th></tr> 357 + </thead> 358 + <tbody> 359 + <tr id="row1"><td>Alice</td><td>31</td><td>NYC</td></tr> 360 + <tr id="row2"><td>Bob</td><td>25</td><td>LA</td></tr> 361 + <tr id="row3"><td>Charlie</td><td>35</td><td>SF</td></tr> 362 + </tbody> 363 + ` 364 + 365 + morph(table, referenceTable) 366 + 367 + expect(table.className).toBe("updated") 368 + expect(table.querySelector("#row1")).toBe(row1) 369 + expect(table.querySelector("#row2")).toBe(row2) 370 + expect(table.querySelectorAll("tbody tr").length).toBe(3) 371 + expect(table.querySelectorAll("thead th").length).toBe(3) 372 + }) 373 + 374 + it("should handle iframe elements", () => { 375 + const div = document.createElement("div") 376 + div.innerHTML = '<iframe id="frame1" src="about:blank"></iframe>' 377 + container.appendChild(div) 378 + 379 + const frame1 = div.querySelector("#frame1") 380 + 381 + const referenceDiv = document.createElement("div") 382 + referenceDiv.innerHTML = '<iframe id="frame1" src="about:blank" title="Updated"></iframe>' 383 + 384 + morph(div, referenceDiv) 385 + 386 + const updatedFrame = div.querySelector("#frame1") as HTMLIFrameElement 387 + expect(updatedFrame).toBe(frame1) 388 + expect(updatedFrame.title).toBe("Updated") 389 + }) 390 + 391 + it("should handle canvas elements", () => { 392 + const div = document.createElement("div") 393 + const canvas = document.createElement("canvas") 394 + canvas.id = "canvas1" 395 + canvas.width = 100 396 + canvas.height = 100 397 + div.appendChild(canvas) 398 + container.appendChild(div) 399 + 400 + const ctx = canvas.getContext("2d") 401 + if (ctx) { 402 + ctx.fillStyle = "red" 403 + ctx.fillRect(0, 0, 50, 50) 404 + } 405 + 406 + const referenceDiv = document.createElement("div") 407 + const referenceCanvas = document.createElement("canvas") 408 + referenceCanvas.id = "canvas1" 409 + referenceCanvas.width = 200 410 + referenceCanvas.height = 200 411 + referenceDiv.appendChild(referenceCanvas) 412 + 413 + morph(div, referenceDiv) 414 + 415 + const updatedCanvas = div.querySelector("#canvas1") as HTMLCanvasElement 416 + expect(updatedCanvas).toBe(canvas) 417 + expect(updatedCanvas.width).toBe(200) 418 + expect(updatedCanvas.height).toBe(200) 419 + }) 420 + 421 + it("should handle video and audio elements", () => { 422 + const div = document.createElement("div") 423 + div.innerHTML = ` 424 + <video id="vid1" width="320" height="240" controls> 425 + <source src="movie.mp4" type="video/mp4"> 426 + </video> 427 + <audio id="aud1" controls> 428 + <source src="audio.mp3" type="audio/mpeg"> 429 + </audio> 430 + ` 431 + container.appendChild(div) 432 + 433 + const video = div.querySelector("#vid1") 434 + const audio = div.querySelector("#aud1") 435 + 436 + const referenceDiv = document.createElement("div") 437 + referenceDiv.innerHTML = ` 438 + <video id="vid1" width="640" height="480" controls autoplay> 439 + <source src="movie.mp4" type="video/mp4"> 440 + </video> 441 + <audio id="aud1" controls loop> 442 + <source src="audio.mp3" type="audio/mpeg"> 443 + </audio> 444 + ` 445 + 446 + morph(div, referenceDiv) 447 + 448 + const updatedVideo = div.querySelector("#vid1") as HTMLVideoElement 449 + const updatedAudio = div.querySelector("#aud1") as HTMLAudioElement 450 + 451 + expect(updatedVideo).toBe(video) 452 + expect(updatedAudio).toBe(audio) 453 + expect(updatedVideo.getAttribute("width")).toBe("640") 454 + expect(updatedVideo.hasAttribute("autoplay")).toBe(true) 455 + expect(updatedAudio.hasAttribute("loop")).toBe(true) 456 + }) 457 + 458 + it("should handle data attributes", () => { 459 + const div = document.createElement("div") 460 + div.setAttribute("data-user-id", "123") 461 + div.setAttribute("data-role", "admin") 462 + div.textContent = "User panel" 463 + container.appendChild(div) 464 + 465 + const referenceDiv = document.createElement("div") 466 + referenceDiv.setAttribute("data-user-id", "456") 467 + referenceDiv.setAttribute("data-role", "user") 468 + referenceDiv.setAttribute("data-active", "true") 469 + referenceDiv.textContent = "User panel" 470 + 471 + morph(div, referenceDiv) 472 + 473 + expect(div.getAttribute("data-user-id")).toBe("456") 474 + expect(div.getAttribute("data-role")).toBe("user") 475 + expect(div.getAttribute("data-active")).toBe("true") 476 + expect(div.dataset.userId).toBe("456") 477 + expect(div.dataset.role).toBe("user") 478 + expect(div.dataset.active).toBe("true") 479 + }) 480 + 481 + it("should handle aria attributes", () => { 482 + const button = document.createElement("button") 483 + button.setAttribute("aria-label", "Close") 484 + button.setAttribute("aria-expanded", "false") 485 + button.textContent = "X" 486 + container.appendChild(button) 487 + 488 + const referenceButton = document.createElement("button") 489 + referenceButton.setAttribute("aria-label", "Open") 490 + referenceButton.setAttribute("aria-expanded", "true") 491 + referenceButton.setAttribute("aria-controls", "menu") 492 + referenceButton.textContent = "☰" 493 + 494 + morph(button, referenceButton) 495 + 496 + expect(button.getAttribute("aria-label")).toBe("Open") 497 + expect(button.getAttribute("aria-expanded")).toBe("true") 498 + expect(button.getAttribute("aria-controls")).toBe("menu") 499 + expect(button.textContent).toBe("☰") 500 + }) 501 + 502 + it("should handle style object changes", () => { 503 + const div = document.createElement("div") 504 + div.style.color = "red" 505 + div.style.fontSize = "16px" 506 + div.style.padding = "10px" 507 + container.appendChild(div) 508 + 509 + const referenceDiv = document.createElement("div") 510 + referenceDiv.style.color = "blue" 511 + referenceDiv.style.fontSize = "20px" 512 + referenceDiv.style.margin = "5px" 513 + 514 + morph(div, referenceDiv) 515 + 516 + expect(div.style.color).toBe("blue") 517 + expect(div.style.fontSize).toBe("20px") 518 + expect(div.style.margin).toBe("5px") 519 + }) 520 + 521 + it("should handle class list manipulation", () => { 522 + const div = document.createElement("div") 523 + div.className = "class1 class2 class3" 524 + container.appendChild(div) 525 + 526 + expect(div.classList.contains("class1")).toBe(true) 527 + expect(div.classList.contains("class2")).toBe(true) 528 + 529 + const referenceDiv = document.createElement("div") 530 + referenceDiv.className = "class2 class4 class5" 531 + 532 + morph(div, referenceDiv) 533 + 534 + expect(div.classList.contains("class1")).toBe(false) 535 + expect(div.classList.contains("class2")).toBe(true) 536 + expect(div.classList.contains("class3")).toBe(false) 537 + expect(div.classList.contains("class4")).toBe(true) 538 + expect(div.classList.contains("class5")).toBe(true) 539 + }) 540 + 541 + it("should handle boolean attributes correctly", () => { 542 + const button = document.createElement("button") 543 + button.disabled = true 544 + button.textContent = "Submit" 545 + container.appendChild(button) 546 + 547 + const referenceButton = document.createElement("button") 548 + referenceButton.textContent = "Submit" 549 + // disabled is not set, so it should be removed 550 + 551 + morph(button, referenceButton) 552 + 553 + expect(button.disabled).toBe(false) 554 + expect(button.hasAttribute("disabled")).toBe(false) 555 + 556 + // Now add it back 557 + const referenceButton2 = document.createElement("button") 558 + referenceButton2.disabled = true 559 + referenceButton2.textContent = "Submit" 560 + 561 + morph(button, referenceButton2) 562 + 563 + expect(button.disabled).toBe(true) 564 + }) 565 + 566 + it("should handle readonly and required attributes on inputs", () => { 567 + const input = document.createElement("input") 568 + input.type = "text" 569 + input.required = true 570 + container.appendChild(input) 571 + 572 + const referenceInput = document.createElement("input") 573 + referenceInput.type = "text" 574 + referenceInput.readOnly = true 575 + 576 + morph(input, referenceInput) 577 + 578 + expect(input.required).toBe(false) 579 + expect(input.readOnly).toBe(true) 580 + }) 581 + 582 + it("should handle multiple select options", () => { 583 + const select = document.createElement("select") 584 + select.multiple = true 585 + select.innerHTML = ` 586 + <option value="1" selected>Option 1</option> 587 + <option value="2" selected>Option 2</option> 588 + <option value="3">Option 3</option> 589 + ` 590 + container.appendChild(select) 591 + 592 + expect(select.selectedOptions.length).toBe(2) 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 + expect(select.selectedOptions.length).toBe(2) 605 + expect(select.selectedOptions[0].value).toBe("2") 606 + expect(select.selectedOptions[1].value).toBe("3") 607 + }) 608 + 609 + it("should handle script tags safely", () => { 610 + const div = document.createElement("div") 611 + div.innerHTML = '<div id="content">Content</div>' 612 + container.appendChild(div) 613 + 614 + const referenceDiv = document.createElement("div") 615 + referenceDiv.innerHTML = '<div id="content">Updated</div><script>console.log("test")</script>' 616 + 617 + morph(div, referenceDiv) 618 + 619 + expect(div.querySelector("#content")?.textContent).toBe("Updated") 620 + expect(div.querySelector("script")).toBeTruthy() 621 + }) 622 + 623 + it("should handle deep nesting with many levels", () => { 624 + const createNested = (depth: number, id: string): string => { 625 + if (depth === 0) return `<span id="${id}">Leaf ${id}</span>` 626 + return `<div id="level-${depth}"><div>${createNested(depth - 1, id)}</div></div>` 627 + } 628 + 629 + const div = document.createElement("div") 630 + div.innerHTML = createNested(10, "original") 631 + container.appendChild(div) 632 + 633 + const leaf = div.querySelector("#original") 634 + 635 + const referenceDiv = document.createElement("div") 636 + referenceDiv.innerHTML = createNested(10, "original") 637 + referenceDiv.querySelector("#original")!.textContent = "Leaf updated" 638 + 639 + morph(div, referenceDiv) 640 + 641 + expect(div.querySelector("#original")).toBe(leaf) 642 + expect(div.querySelector("#original")?.textContent).toBe("Leaf updated") 643 + }) 644 + 645 + it("should handle text nodes with special characters", () => { 646 + const div = document.createElement("div") 647 + div.textContent = 'Hello <World> & "Friends"' 648 + container.appendChild(div) 649 + 650 + const referenceDiv = document.createElement("div") 651 + referenceDiv.textContent = "Goodbye <Universe> & 'Enemies'" 652 + 653 + morph(div, referenceDiv) 654 + 655 + expect(div.textContent).toBe("Goodbye <Universe> & 'Enemies'") 656 + }) 657 + 658 + it("should handle whitespace preservation", () => { 659 + const pre = document.createElement("pre") 660 + pre.textContent = "Line 1\n Line 2\n Line 3" 661 + container.appendChild(pre) 662 + 663 + const referencePre = document.createElement("pre") 664 + referencePre.textContent = "Line 1\n Line 2\n Line 3\nLine 4" 665 + 666 + morph(pre, referencePre) 667 + 668 + expect(pre.textContent).toBe("Line 1\n Line 2\n Line 3\nLine 4") 669 + }) 670 + 671 + it("should handle radio button groups", () => { 672 + const form = document.createElement("form") 673 + form.innerHTML = ` 674 + <input type="radio" name="choice" value="a" id="radio-a" checked> 675 + <input type="radio" name="choice" value="b" id="radio-b"> 676 + <input type="radio" name="choice" value="c" id="radio-c"> 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 + <input type="radio" name="choice" value="c" id="radio-c"> 688 + ` 689 + 690 + morph(form, referenceForm) 691 + 692 + const radioB = form.querySelector("#radio-b") as HTMLInputElement 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 + }) 335 837 }) 336 838 })