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