Precise DOM morphing
morphing typescript dom
at main 601 lines 15 kB view raw
1/* 2 * These tests were inspired by morphdom. 3 * Here's their license: 4 * 5 * The MIT License (MIT) 6 * 7 * Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com) 8 * 9 * Permission is hereby granted, free of charge, to any person obtaining a copy 10 * of this software and associated documentation files (the "Software"), to deal 11 * in the Software without restriction, including without limitation the rights 12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 * copies of the Software, and to permit persons to whom the Software is 14 * furnished to do so, subject to the following conditions: 15 * 16 * The above copyright notice and this permission notice shall be included in 17 * all copies or substantial portions of the Software. 18 * 19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 * THE SOFTWARE. 26 */ 27 28import { describe, it, expect, beforeEach, afterEach } from "vitest" 29import { morph } from "../src/morphlex" 30 31describe("Morphdom-style fixture tests", () => { 32 let container: HTMLElement 33 34 beforeEach(() => { 35 container = document.createElement("div") 36 document.body.appendChild(container) 37 }) 38 39 afterEach(() => { 40 if (container && container.parentNode) { 41 container.parentNode.removeChild(container) 42 } 43 }) 44 45 function parseHTML(html: string): HTMLElement { 46 const tmp = document.createElement("div") 47 tmp.innerHTML = html.trim() 48 return tmp.firstChild as HTMLElement 49 } 50 51 describe("simple morphing", () => { 52 it("should add new element before existing element", () => { 53 const from = parseHTML("<div><b>bold</b></div>") 54 const to = parseHTML("<div><i>italics</i><b>bold</b></div>") 55 56 morph(from, to) 57 58 expect(from.innerHTML).toBe("<i>italics</i><b>bold</b>") 59 }) 60 61 it("should handle equal elements", () => { 62 const from = parseHTML("<div>test</div>") 63 const to = parseHTML("<div>test</div>") 64 65 morph(from, to) 66 67 expect(from.innerHTML).toBe("test") 68 }) 69 70 it("should shorten list of children", () => { 71 const from = parseHTML("<div><span>1</span><span>2</span><span>3</span></div>") 72 const to = parseHTML("<div><span>1</span></div>") 73 74 morph(from, to) 75 76 expect(from.children.length).toBe(1) 77 expect(from.innerHTML).toBe("<span>1</span>") 78 }) 79 80 it("should lengthen list of children", () => { 81 const from = parseHTML("<div><span>1</span></div>") 82 const to = parseHTML("<div><span>1</span><span>2</span><span>3</span></div>") 83 84 morph(from, to) 85 86 expect(from.children.length).toBe(3) 87 expect(from.innerHTML).toBe("<span>1</span><span>2</span><span>3</span>") 88 }) 89 90 it("should reverse children", () => { 91 const from = parseHTML("<div><span>a</span><span>b</span><span>c</span></div>") 92 const to = parseHTML("<div><span>c</span><span>b</span><span>a</span></div>") 93 94 morph(from, to) 95 96 expect(from.innerHTML).toBe("<span>c</span><span>b</span><span>a</span>") 97 }) 98 }) 99 100 describe("attribute handling", () => { 101 it("should handle empty string attribute values", () => { 102 const from = parseHTML('<div class="foo"></div>') 103 const to = parseHTML('<div class=""></div>') 104 105 morph(from, to) 106 107 expect(from.getAttribute("class")).toBe("") 108 }) 109 }) 110 111 describe("input elements", () => { 112 it("should morph input element", () => { 113 const from = parseHTML('<input type="text" value="Hello">') 114 const to = parseHTML('<input type="text" value="World">') 115 116 morph(from, to) 117 118 // Input values are updated by default when not modified 119 expect((from as HTMLInputElement).value).toBe("World") 120 }) 121 122 it("should add disabled attribute to input", () => { 123 const from = parseHTML('<input type="text" value="Hello World">') 124 const to = parseHTML('<input type="text" value="Hello World" disabled>') 125 126 morph(from, to) 127 128 expect((from as HTMLInputElement).disabled).toBe(true) 129 }) 130 131 it("should remove disabled attribute from input", () => { 132 const from = parseHTML('<input type="text" value="Hello World" disabled>') 133 const to = parseHTML('<input type="text" value="Hello World">') 134 135 morph(from, to) 136 137 expect((from as HTMLInputElement).disabled).toBe(false) 138 }) 139 }) 140 141 describe("select elements", () => { 142 it("should handle select element with selected option", () => { 143 const from = parseHTML(` 144 <select> 145 <option value="1">One</option> 146 <option value="2" selected>Two</option> 147 <option value="3">Three</option> 148 </select> 149 `) 150 const to = parseHTML(` 151 <select> 152 <option value="1" selected>One</option> 153 <option value="2">Two</option> 154 <option value="3">Three</option> 155 </select> 156 `) 157 158 morph(from, to) 159 160 // Selected attribute is removed but not added - select defaults to first option 161 const select = from as HTMLSelectElement 162 expect(select.value).toBe("1") 163 expect(select.options[0].selected).toBe(true) 164 expect(select.options[1].selected).toBe(false) 165 }) 166 167 it("should handle select element with default selection", () => { 168 const from = parseHTML(` 169 <select> 170 <option value="1">One</option> 171 <option value="2">Two</option> 172 <option value="3">Three</option> 173 </select> 174 `) 175 const to = parseHTML(` 176 <select> 177 <option value="1">One</option> 178 <option value="2" selected>Two</option> 179 <option value="3">Three</option> 180 </select> 181 `) 182 183 morph(from, to) 184 185 // Selected options are updated by default when not modified 186 const select = from as HTMLSelectElement 187 expect(select.value).toBe("2") 188 expect(select.options[1].selected).toBe(true) 189 }) 190 }) 191 192 describe("id-based morphing", () => { 193 it("should handle nested elements with IDs", () => { 194 const from = parseHTML(` 195 <div> 196 <div id="a">A</div> 197 <div id="b">B</div> 198 </div> 199 `) 200 const to = parseHTML(` 201 <div> 202 <div id="b">B Updated</div> 203 <div id="a">A Updated</div> 204 </div> 205 `) 206 207 const aEl = from.querySelector("#a") 208 const bEl = from.querySelector("#b") 209 210 morph(from, to) 211 212 // Elements with IDs should be preserved 213 expect(from.querySelector("#a")).toBe(aEl) 214 expect(from.querySelector("#b")).toBe(bEl) 215 expect(from.querySelector("#a")?.textContent).toBe("A Updated") 216 expect(from.querySelector("#b")?.textContent).toBe("B Updated") 217 }) 218 219 it("should handle reversing elements with IDs", () => { 220 const from = parseHTML(` 221 <div> 222 <div id="a">a</div> 223 <div id="b">b</div> 224 <div id="c">c</div> 225 </div> 226 `) 227 const to = parseHTML(` 228 <div> 229 <div id="c">c</div> 230 <div id="b">b</div> 231 <div id="a">a</div> 232 </div> 233 `) 234 235 const aEl = from.querySelector("#a") 236 const bEl = from.querySelector("#b") 237 const cEl = from.querySelector("#c") 238 239 morph(from, to) 240 241 expect(from.querySelector("#a")).toBe(aEl) 242 expect(from.querySelector("#b")).toBe(bEl) 243 expect(from.querySelector("#c")).toBe(cEl) 244 }) 245 246 it("should handle prepending element with ID", () => { 247 const from = parseHTML(` 248 <div> 249 <div id="a">a</div> 250 <div id="b">b</div> 251 </div> 252 `) 253 const to = parseHTML(` 254 <div> 255 <div id="c">c</div> 256 <div id="a">a</div> 257 <div id="b">b</div> 258 </div> 259 `) 260 261 const aEl = from.querySelector("#a") 262 const bEl = from.querySelector("#b") 263 264 morph(from, to) 265 266 expect(from.querySelector("#a")).toBe(aEl) 267 expect(from.querySelector("#b")).toBe(bEl) 268 expect(from.children.length).toBe(3) 269 expect(from.children[0].id).toBe("c") 270 }) 271 272 it("should handle changing tag name with ID preservation", () => { 273 const from = parseHTML(` 274 <div> 275 <div id="a">A</div> 276 </div> 277 `) 278 const to = parseHTML(` 279 <div> 280 <span id="a">A</span> 281 </div> 282 `) 283 284 morph(from, to) 285 286 expect(from.querySelector("#a")?.tagName).toBe("SPAN") 287 }) 288 }) 289 290 describe("tag name changes", () => { 291 it("should change tag name", () => { 292 const from = parseHTML("<div><b>Hello</b></div>") 293 const to = parseHTML("<div><i>Hello</i></div>") 294 295 morph(from, to) 296 297 expect(from.innerHTML).toBe("<i>Hello</i>") 298 }) 299 300 it("should change tag name with IDs", () => { 301 const from = parseHTML(` 302 <div> 303 <div id="a">A</div> 304 <div id="b">B</div> 305 </div> 306 `) 307 const to = parseHTML(` 308 <div> 309 <span id="a">A</span> 310 <span id="b">B</span> 311 </div> 312 `) 313 314 morph(from, to) 315 316 expect(from.querySelector("#a")?.tagName).toBe("SPAN") 317 expect(from.querySelector("#b")?.tagName).toBe("SPAN") 318 }) 319 }) 320 321 describe("SVG elements", () => { 322 it("should handle SVG elements", () => { 323 const from = parseHTML(` 324 <svg> 325 <circle cx="50" cy="50" r="40"></circle> 326 </svg> 327 `) 328 const to = parseHTML(` 329 <svg> 330 <circle cx="50" cy="50" r="40"></circle> 331 <rect x="10" y="10" width="30" height="30"></rect> 332 </svg> 333 `) 334 335 morph(from, to) 336 337 expect(from.children.length).toBe(2) 338 expect(from.children[0].tagName.toLowerCase()).toBe("circle") 339 expect(from.children[1].tagName.toLowerCase()).toBe("rect") 340 }) 341 342 it("should append new SVG elements", () => { 343 const from = parseHTML('<svg><circle cx="10" cy="10" r="5"></circle></svg>') 344 const to = parseHTML(` 345 <svg> 346 <circle cx="10" cy="10" r="5"></circle> 347 <circle cx="20" cy="20" r="5"></circle> 348 </svg> 349 `) 350 351 morph(from, to) 352 353 expect(from.children.length).toBe(2) 354 }) 355 }) 356 357 describe("data table tests", () => { 358 it("should handle complex data table morphing", () => { 359 const from = parseHTML(` 360 <table> 361 <tbody> 362 <tr><td>A</td><td>B</td></tr> 363 <tr><td>C</td><td>D</td></tr> 364 </tbody> 365 </table> 366 `) 367 const to = parseHTML(` 368 <table> 369 <tbody> 370 <tr><td>A</td><td>B</td><td>E</td></tr> 371 <tr><td>C</td><td>D</td><td>F</td></tr> 372 </tbody> 373 </table> 374 `) 375 376 morph(from, to) 377 378 const rows = from.querySelectorAll("tr") 379 expect(rows.length).toBe(2) 380 expect(rows[0].children.length).toBe(3) 381 expect(rows[0].children[2].textContent).toBe("E") 382 expect(rows[1].children[2].textContent).toBe("F") 383 }) 384 385 it("should handle data table with row modifications", () => { 386 const from = parseHTML(` 387 <table> 388 <tbody> 389 <tr><td>1</td></tr> 390 <tr><td>2</td></tr> 391 <tr><td>3</td></tr> 392 </tbody> 393 </table> 394 `) 395 const to = parseHTML(` 396 <table> 397 <tbody> 398 <tr><td>1</td></tr> 399 <tr><td>2 Updated</td></tr> 400 <tr><td>3</td></tr> 401 <tr><td>4</td></tr> 402 </tbody> 403 </table> 404 `) 405 406 morph(from, to) 407 408 const rows = from.querySelectorAll("tr") 409 expect(rows.length).toBe(4) 410 expect(rows[1].textContent).toBe("2 Updated") 411 expect(rows[3].textContent).toBe("4") 412 }) 413 }) 414 415 describe("nested id scenarios", () => { 416 it("should handle deeply nested IDs - scenario 2", () => { 417 const from = parseHTML(` 418 <div> 419 <div id="outer"> 420 <div id="a">A</div> 421 <div id="b">B</div> 422 </div> 423 </div> 424 `) 425 const to = parseHTML(` 426 <div> 427 <div id="outer"> 428 <div id="b">B</div> 429 <div id="a">A</div> 430 </div> 431 </div> 432 `) 433 434 const aEl = from.querySelector("#a") 435 const bEl = from.querySelector("#b") 436 437 morph(from, to) 438 439 expect(from.querySelector("#a")).toBe(aEl) 440 expect(from.querySelector("#b")).toBe(bEl) 441 }) 442 443 it("should handle deeply nested IDs - scenario 3", () => { 444 const from = parseHTML(` 445 <div> 446 <div id="outer"> 447 <div> 448 <div id="a">A</div> 449 <div id="b">B</div> 450 </div> 451 </div> 452 </div> 453 `) 454 const to = parseHTML(` 455 <div> 456 <div id="outer"> 457 <div> 458 <div id="b">B</div> 459 <div id="a">A</div> 460 </div> 461 </div> 462 </div> 463 `) 464 465 const aEl = from.querySelector("#a") 466 const bEl = from.querySelector("#b") 467 468 morph(from, to) 469 470 expect(from.querySelector("#a")).toBe(aEl) 471 expect(from.querySelector("#b")).toBe(bEl) 472 }) 473 474 it("should handle deeply nested IDs - scenario 4", () => { 475 const from = parseHTML(` 476 <div> 477 <div id="outer"> 478 <div id="inner"> 479 <div id="a">A</div> 480 <div id="b">B</div> 481 </div> 482 </div> 483 </div> 484 `) 485 const to = parseHTML(` 486 <div> 487 <div id="outer"> 488 <div id="inner"> 489 <div id="b">B</div> 490 <div id="a">A</div> 491 </div> 492 </div> 493 </div> 494 `) 495 496 const aEl = from.querySelector("#a") 497 const bEl = from.querySelector("#b") 498 499 morph(from, to) 500 501 expect(from.querySelector("#a")).toBe(aEl) 502 expect(from.querySelector("#b")).toBe(bEl) 503 }) 504 505 it("should handle deeply nested IDs - scenario 5", () => { 506 const from = parseHTML(` 507 <div> 508 <div id="a"> 509 <div id="b">B</div> 510 </div> 511 </div> 512 `) 513 const to = parseHTML(` 514 <div> 515 <div id="a">A</div> 516 <div id="b">B</div> 517 </div> 518 `) 519 520 morph(from, to) 521 522 expect(from.querySelector("#a")?.textContent?.trim()).toBe("A") 523 expect(from.querySelector("#b")?.textContent).toBe("B") 524 }) 525 526 it("should handle deeply nested IDs - scenario 6", () => { 527 const from = parseHTML(` 528 <div> 529 <div id="a">A</div> 530 <div id="b">B</div> 531 </div> 532 `) 533 const to = parseHTML(` 534 <div> 535 <div id="a"> 536 <div id="b">B</div> 537 </div> 538 </div> 539 `) 540 541 morph(from, to) 542 543 expect(from.querySelector("#a #b")?.textContent).toBe("B") 544 }) 545 546 it("should handle deeply nested IDs - scenario 7", () => { 547 const from = parseHTML(` 548 <div> 549 <div id="a"> 550 <div id="b"> 551 <div id="c">C</div> 552 </div> 553 </div> 554 </div> 555 `) 556 const to = parseHTML(` 557 <div> 558 <div id="a">A</div> 559 <div id="b">B</div> 560 <div id="c">C</div> 561 </div> 562 `) 563 564 morph(from, to) 565 566 expect(from.children.length).toBe(3) 567 expect(from.querySelector("#a")?.textContent?.trim()).toBe("A") 568 expect(from.querySelector("#b")?.textContent?.trim()).toBe("B") 569 expect(from.querySelector("#c")?.textContent).toBe("C") 570 }) 571 }) 572 573 describe("large document morphing", () => { 574 it("should handle large DOM trees efficiently", () => { 575 let fromHTML = "<div>" 576 let toHTML = "<div>" 577 578 for (let i = 0; i < 100; i++) { 579 fromHTML += `<div id="item-${i}">Item ${i}</div>` 580 toHTML += `<div id="item-${i}">Item ${i} Updated</div>` 581 } 582 583 fromHTML += "</div>" 584 toHTML += "</div>" 585 586 const from = parseHTML(fromHTML) 587 const to = parseHTML(toHTML) 588 589 const originalElements = Array.from(from.children).map((el) => el) 590 591 morph(from, to) 592 593 // All elements should be preserved 594 expect(from.children.length).toBe(100) 595 for (let i = 0; i < 100; i++) { 596 expect(from.children[i]).toBe(originalElements[i]) 597 expect(from.children[i].textContent).toBe(`Item ${i} Updated`) 598 } 599 }) 600 }) 601})