Precise DOM morphing
morphing
typescript
dom
1import { describe, it, expect, beforeEach, afterEach } from "vitest"
2import { morph, morphInner } from "../src/morphlex"
3
4describe("Morphlex - Coverage 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 describe("String parsing error cases", () => {
19 it("should throw error when parseElementFromString receives non-element string", () => {
20 const div = document.createElement("div")
21 container.appendChild(div)
22
23 // Text node is not an element
24 expect(() => {
25 morphInner(div, "Just text")
26 }).toThrow("[Morphlex] The string was not a valid HTML element.")
27 })
28
29 it("should parse multiple elements as valid HTML (they go into body)", () => {
30 const div = document.createElement("div")
31 container.appendChild(div)
32
33 // Multiple root nodes actually work because DOMParser wraps them in body
34 // This test just verifies the parsing works
35 const reference = "<div>Content</div>"
36 morph(div, reference)
37 expect(div.textContent).toBe("Content")
38 })
39
40 it("should throw error when morphInner called with non-matching elements", () => {
41 const div = document.createElement("div")
42 const span = document.createElement("span")
43 container.appendChild(div)
44
45 expect(() => {
46 morphInner(div, span)
47 }).toThrow("[Morphlex] You can only do an inner morph with matching elements.")
48 })
49 })
50
51 describe("ariaBusy handling", () => {
52 it("should handle ariaBusy for non-element nodes", () => {
53 const parent = document.createElement("div")
54 const textNode = document.createTextNode("Original")
55 parent.appendChild(textNode)
56
57 const referenceParent = document.createElement("div")
58 const refTextNode = document.createTextNode("Updated")
59 referenceParent.appendChild(refTextNode)
60
61 morph(parent, referenceParent)
62
63 expect(parent.textContent).toBe("Updated")
64 })
65 })
66
67 describe("Property updates", () => {
68 it("should update input disabled property", () => {
69 const parent = document.createElement("div")
70 const input = document.createElement("input")
71 input.disabled = false
72 parent.appendChild(input)
73
74 const reference = document.createElement("div")
75 const refInput = document.createElement("input")
76 refInput.disabled = true
77 reference.appendChild(refInput)
78
79 morph(parent, reference)
80
81 expect(input.disabled).toBe(true)
82 })
83
84 it("should not update file input value", () => {
85 const parent = document.createElement("div")
86 const input = document.createElement("input")
87 input.type = "file"
88 parent.appendChild(input)
89
90 const reference = document.createElement("div")
91 const refInput = document.createElement("input")
92 refInput.type = "file"
93 reference.appendChild(refInput)
94
95 morph(parent, reference)
96
97 expect(input.type).toBe("file")
98 })
99 })
100
101 describe("Head element special handling", () => {
102 it("should handle nested head elements in child morphing", () => {
103 const parent = document.createElement("div")
104 const head = document.createElement("head")
105 const meta1 = document.createElement("meta")
106 meta1.setAttribute("name", "test")
107 meta1.setAttribute("content", "value")
108 head.appendChild(meta1)
109 parent.appendChild(head)
110
111 const reference = document.createElement("div")
112 const refHead = document.createElement("head")
113 const refMeta = document.createElement("meta")
114 refMeta.setAttribute("name", "test")
115 refMeta.setAttribute("content", "updated")
116 refHead.appendChild(refMeta)
117 reference.appendChild(refHead)
118
119 morph(parent, reference)
120
121 expect(parent.querySelector("head")).toBeTruthy()
122 })
123 })
124
125 describe("Child element morphing edge cases", () => {
126 it("should handle ID matching with overlapping ID sets", () => {
127 const parent = document.createElement("div")
128 const child1 = document.createElement("div")
129 child1.id = "child1"
130 const nested = document.createElement("span")
131 nested.id = "nested"
132 child1.appendChild(nested)
133
134 const child2 = document.createElement("div")
135 child2.id = "child2"
136
137 parent.appendChild(child1)
138 parent.appendChild(child2)
139
140 const reference = document.createElement("div")
141 const refChild1 = document.createElement("div")
142 refChild1.id = "child1"
143 const refNested = document.createElement("span")
144 refNested.id = "different"
145 refChild1.appendChild(refNested)
146
147 const refChild2 = document.createElement("div")
148 refChild2.id = "child2"
149
150 reference.appendChild(refChild1)
151 reference.appendChild(refChild2)
152
153 morph(parent, reference)
154
155 expect(parent.children[0].id).toBe("child1")
156 })
157
158 it("should insert new node when no match found and beforeNodeAdded returns true", () => {
159 const parent = document.createElement("div")
160 const existing = document.createElement("div")
161 existing.id = "existing"
162 parent.appendChild(existing)
163
164 const reference = document.createElement("div")
165 const refNew = document.createElement("div")
166 refNew.id = "new"
167 const refExisting = document.createElement("div")
168 refExisting.id = "existing"
169 reference.appendChild(refNew)
170 reference.appendChild(refExisting)
171
172 let addedNode: Node | null = null
173 morph(parent, reference, {
174 beforeNodeAdded: (node) => {
175 addedNode = node
176 return true
177 },
178 })
179
180 expect(addedNode).toBeTruthy()
181 expect(parent.children[0].id).toBe("new")
182 })
183
184 it("should not insert new node when beforeNodeAdded returns false", () => {
185 const parent = document.createElement("div")
186 const existing = document.createElement("div")
187 existing.id = "existing"
188 existing.textContent = "original"
189 parent.appendChild(existing)
190
191 const reference = document.createElement("div")
192 const refNew = document.createElement("span")
193 refNew.id = "new"
194 refNew.textContent = "new content"
195 const refExisting = document.createElement("div")
196 refExisting.id = "existing"
197 refExisting.textContent = "updated"
198 reference.appendChild(refNew)
199 reference.appendChild(refExisting)
200
201 let addCallbackCalled = false
202 morph(parent, reference, {
203 beforeNodeAdded: () => {
204 addCallbackCalled = true
205 return false
206 },
207 })
208
209 // beforeNodeAdded should have been called
210 expect(addCallbackCalled).toBe(true)
211 // The existing div will be morphed to match reference
212 expect(parent.children[0].tagName).toBe("DIV")
213 })
214
215 it("should call afterNodeVisited for child elements even when new node inserted", () => {
216 const parent = document.createElement("div")
217 const child = document.createElement("div")
218 child.id = "child"
219 parent.appendChild(child)
220
221 const reference = document.createElement("div")
222 const refChild = document.createElement("span")
223 refChild.id = "newChild"
224 reference.appendChild(refChild)
225
226 let morphedCalled = false
227 morph(parent, reference, {
228 afterNodeVisited: () => {
229 morphedCalled = true
230 },
231 })
232
233 expect(morphedCalled).toBe(true)
234 })
235 })
236
237 describe("Callback cancellation", () => {
238 it("should call beforeAttributeUpdated and cancel attribute removal when it returns false", () => {
239 const div = document.createElement("div")
240 div.setAttribute("data-keep", "value")
241 div.setAttribute("data-remove", "value")
242
243 const reference = document.createElement("div")
244 reference.setAttribute("data-keep", "value")
245
246 morph(div, reference, {
247 beforeAttributeUpdated: (element, name, value) => {
248 if (name === "data-remove" && value === null) {
249 return false // Cancel removal
250 }
251 return true
252 },
253 })
254
255 // Attribute should still be there because callback returned false
256 expect(div.hasAttribute("data-remove")).toBe(true)
257 })
258 })
259
260 describe("Empty ID handling", () => {
261 it("should ignore elements with empty id attribute", () => {
262 const parent = document.createElement("div")
263 const child1 = document.createElement("div")
264 child1.setAttribute("id", "") // Empty ID
265 child1.textContent = "First"
266
267 const child2 = document.createElement("div")
268 child2.id = "valid-id"
269 child2.textContent = "Second"
270
271 parent.appendChild(child1)
272 parent.appendChild(child2)
273
274 const reference = document.createElement("div")
275 const refChild1 = document.createElement("div")
276 refChild1.setAttribute("id", "")
277 refChild1.textContent = "First Updated"
278
279 const refChild2 = document.createElement("div")
280 refChild2.id = "valid-id"
281 refChild2.textContent = "Second Updated"
282
283 reference.appendChild(refChild1)
284 reference.appendChild(refChild2)
285
286 morph(parent, reference)
287
288 expect(child1.textContent).toBe("First Updated")
289 expect(child2.textContent).toBe("Second Updated")
290 })
291 })
292
293 describe("Complex morphing scenarios", () => {
294 it("should handle mixed content with sensitive and non-sensitive elements", () => {
295 const parent = document.createElement("div")
296 container.appendChild(parent)
297
298 const div = document.createElement("div")
299 div.id = "div1"
300
301 const input = document.createElement("input")
302 input.id = "input1"
303 input.value = "test"
304 input.defaultValue = ""
305
306 const canvas = document.createElement("canvas")
307 canvas.id = "canvas1"
308
309 parent.appendChild(div)
310 parent.appendChild(input)
311 parent.appendChild(canvas)
312
313 const reference = document.createElement("div")
314
315 const refCanvas = document.createElement("canvas")
316 refCanvas.id = "canvas1"
317
318 const refInput = document.createElement("input")
319 refInput.id = "input1"
320
321 const refDiv = document.createElement("div")
322 refDiv.id = "div1"
323
324 reference.appendChild(refCanvas)
325 reference.appendChild(refInput)
326 reference.appendChild(refDiv)
327
328 morph(parent, reference)
329
330 expect(parent.children.length).toBe(3)
331 })
332
333 it("should match elements by overlapping ID sets", () => {
334 // Test lines 372-373 - matching by overlapping ID sets
335 const parent = document.createElement("div")
336
337 const outer1 = document.createElement("div")
338 outer1.id = "outer1"
339 const inner1a = document.createElement("span")
340 inner1a.id = "inner1a"
341 const inner1b = document.createElement("span")
342 inner1b.id = "inner1b"
343 outer1.appendChild(inner1a)
344 outer1.appendChild(inner1b)
345
346 const outer2 = document.createElement("div")
347 outer2.id = "outer2"
348 const inner2 = document.createElement("span")
349 inner2.id = "inner2"
350 outer2.appendChild(inner2)
351
352 parent.appendChild(outer1)
353 parent.appendChild(outer2)
354
355 const reference = document.createElement("div")
356
357 // Reference wants outer1 to come second, but references an inner ID
358 const refOuter2 = document.createElement("div")
359 refOuter2.id = "outer2"
360 const refInner2 = document.createElement("span")
361 refInner2.id = "inner2"
362 refOuter2.appendChild(refInner2)
363
364 const refOuter1 = document.createElement("div")
365 refOuter1.id = "outer1"
366 const refInner1a = document.createElement("span")
367 refInner1a.id = "inner1a"
368 refOuter1.appendChild(refInner1a)
369
370 reference.appendChild(refOuter2)
371 reference.appendChild(refOuter1)
372
373 morph(parent, reference)
374
375 expect(parent.children[0].id).toBe("outer2")
376 })
377
378 it("should add completely new element when no match found by tag or ID", () => {
379 // Test lines 386-389 - adding new node with callbacks
380 const parent = document.createElement("div")
381 const existing = document.createElement("div")
382 existing.id = "existing"
383 parent.appendChild(existing)
384
385 const reference = document.createElement("div")
386 const refNew = document.createElement("article")
387 refNew.id = "brand-new"
388 refNew.textContent = "New content"
389 const refExisting = document.createElement("div")
390 refExisting.id = "existing"
391 reference.appendChild(refNew)
392 reference.appendChild(refExisting)
393
394 let addedNode: Node | null = null
395 let afterAddedCalled = false
396 morph(parent, reference, {
397 beforeNodeAdded: (node) => {
398 addedNode = node
399 return true
400 },
401 afterNodeAdded: () => {
402 afterAddedCalled = true
403 },
404 })
405
406 expect(addedNode).toBeTruthy()
407 expect(afterAddedCalled).toBe(true)
408 expect(parent.children[0].tagName).toBe("ARTICLE")
409 })
410
411 describe("DOMParser edge cases", () => {
412 it("should explore parser behavior to trigger line 74", () => {
413 // Line 74 checks if doc.childNodes.length === 1
414 // This is checking the document's childNodes, not body's childNodes
415 // DOMParser always returns a document with html element as child
416 // So doc.childNodes.length is always 1 (the html element)
417 // The else branch on line 74 appears to be unreachable in normal usage
418
419 // Let's verify with actual morph call
420 const parent = document.createElement("div")
421 const div = document.createElement("div")
422 div.textContent = "Original"
423 parent.appendChild(div)
424
425 // This should work fine
426 morph(div, "<span>Test</span>")
427 expect(parent.firstChild?.textContent).toBe("Test")
428 })
429 })
430
431 describe("Additional edge cases for remaining coverage", () => {
432 it("should handle element matching with nested IDs and no direct ID match", () => {
433 // More specific test for lines 372-373
434 const parent = document.createElement("div")
435
436 const container1 = document.createElement("section")
437 const child1a = document.createElement("div")
438 child1a.id = "shared-id-a"
439 const child1b = document.createElement("div")
440 child1b.id = "shared-id-b"
441 container1.appendChild(child1a)
442 container1.appendChild(child1b)
443
444 const container2 = document.createElement("section")
445 const child2 = document.createElement("div")
446 child2.id = "other-id"
447 container2.appendChild(child2)
448
449 parent.appendChild(container1)
450 parent.appendChild(container2)
451
452 const reference = document.createElement("div")
453 const refContainer = document.createElement("section")
454 const refChild = document.createElement("div")
455 refChild.id = "shared-id-a"
456 refContainer.appendChild(refChild)
457
458 reference.appendChild(refContainer)
459
460 morph(parent, reference)
461
462 expect(parent.children.length).toBeGreaterThanOrEqual(1)
463 })
464
465 it("should insert node before when no ID or tag match exists", () => {
466 // Test for lines 386-389 with different scenario
467 const parent = document.createElement("div")
468 const oldChild = document.createElement("p")
469 oldChild.textContent = "Old"
470 parent.appendChild(oldChild)
471
472 const reference = document.createElement("div")
473 const newChild = document.createElement("article")
474 newChild.textContent = "New"
475 reference.appendChild(newChild)
476
477 let beforeAddCalled = false
478 let afterAddCalled = false
479
480 morph(parent, reference, {
481 beforeNodeAdded: () => {
482 beforeAddCalled = true
483 return true
484 },
485 afterNodeAdded: () => {
486 afterAddCalled = true
487 },
488 })
489
490 expect(beforeAddCalled).toBe(true)
491 expect(afterAddCalled).toBe(true)
492 })
493
494 it("should match by overlapping ID sets in sibling scan - lines 372-373", () => {
495 // Lines 372-373: Match element by overlapping ID sets when ID doesn't match
496 // This requires: currentNode has ID != reference.id, but has nested IDs that overlap
497 const parent = document.createElement("div")
498
499 // First child with nested IDs
500 const div1 = document.createElement("div")
501 div1.id = "div1"
502 const nested1 = document.createElement("span")
503 nested1.id = "overlap-id"
504 div1.appendChild(nested1)
505
506 // Second child that's a match by tag name
507 const div2 = document.createElement("div")
508 div2.id = "div2"
509
510 parent.appendChild(div1)
511 parent.appendChild(div2)
512
513 // Reference wants div2 first, but references the overlap-id
514 const reference = document.createElement("div")
515 const refDiv = document.createElement("div")
516 refDiv.id = "target"
517 const refNested = document.createElement("span")
518 refNested.id = "overlap-id" // This ID exists nested in div1
519 refDiv.appendChild(refNested)
520 reference.appendChild(refDiv)
521
522 morph(parent, reference)
523
524 expect(parent.children.length).toBeGreaterThanOrEqual(1)
525 })
526
527 it("should add new node when no tag match exists - lines 386-389", () => {
528 // Lines 386-389: No nextMatchByTagName, so add new node
529 // This requires the reference child has a tag that doesn't exist in current children
530 const parent = document.createElement("div")
531 const p = document.createElement("p")
532 p.textContent = "Paragraph"
533 parent.appendChild(p)
534
535 const reference = document.createElement("div")
536 // Use article tag which doesn't exist in parent
537 const article = document.createElement("article")
538 article.textContent = "Article"
539 const refP = document.createElement("p")
540 reference.appendChild(article)
541 reference.appendChild(refP)
542
543 let addedNode: Node | null = null
544 morph(parent, reference, {
545 beforeNodeAdded: (node) => {
546 addedNode = node
547 return true
548 },
549 afterNodeAdded: () => {
550 // Lines 388-389
551 },
552 })
553
554 expect(addedNode).toBeTruthy()
555 expect(parent.querySelector("article")).toBeTruthy()
556 })
557
558 it("should trigger line 74 - unreachable error path in parseChildNodeFromString", () => {
559 // Line 74: else throw new Error("[Morphlex] The string was not a valid HTML node.");
560 // This line is actually unreachable because DOMParser always returns doc with childNodes.length === 1
561 // However, we can document this as a known unreachable path
562 // The parser always creates: doc -> html -> (head + body)
563 // So doc.childNodes.length is always 1 (the html element)
564
565 // All valid HTML strings will pass the check on line 72
566 const parent = document.createElement("div")
567 const child = document.createElement("div")
568 parent.appendChild(child)
569
570 // Even empty string parses to valid document structure
571 morph(child, "<p>Test</p>")
572 expect(parent.querySelector("p")?.textContent).toBe("Test")
573 })
574
575 it("should handle case where child exists but refChild doesn't in loop - lines 332-333", () => {
576 // Lines 332-333 are actually unreachable in the for loop
577 // because we iterate up to refChildNodes.length, so refChild will always exist
578 // The cleanup happens in the while loop below (lines 338-341)
579 // This test documents that lines 332-333 appear to be dead code
580 const parent = document.createElement("div")
581 const child1 = document.createElement("span")
582 child1.textContent = "Keep"
583 const child2 = document.createElement("span")
584 child2.textContent = "Remove via while loop"
585 parent.appendChild(child1)
586 parent.appendChild(child2)
587
588 const reference = document.createElement("div")
589 const refChild = document.createElement("span")
590 refChild.textContent = "Keep Updated"
591 reference.appendChild(refChild)
592
593 morph(parent, reference)
594
595 expect(parent.children.length).toBe(1)
596 })
597
598 it("should add new node when no ID or tag match exists - lines 386-389", () => {
599 // Lines 386-389 require: no nextMatchByTagName AND beforeNodeAdded returns true
600 // This means the first child must not match by tag, and no sibling matches either
601 const parent = document.createElement("div")
602 // Use a tag that won't match
603 const article = document.createElement("article")
604 article.id = "article1"
605 article.textContent = "Article"
606 parent.appendChild(article)
607
608 const reference = document.createElement("div")
609 // First child is a section (different tag), and has no ID match in siblings
610 const section = document.createElement("section")
611 section.id = "section1"
612 section.textContent = "Section"
613 // Second child to trigger morphChildElement for the article
614 const refArticle = document.createElement("article")
615 refArticle.id = "article1"
616 reference.appendChild(section)
617 reference.appendChild(refArticle)
618
619 let beforeCalled = false
620 let afterCalled = false
621
622 morph(parent, reference, {
623 beforeNodeAdded: () => {
624 beforeCalled = true
625 return true // Line 387: insertBefore is called
626 },
627 afterNodeAdded: () => {
628 afterCalled = true // Line 389
629 },
630 })
631
632 expect(beforeCalled).toBe(true)
633 expect(afterCalled).toBe(true)
634 })
635
636 describe("Exact uncovered line tests", () => {
637 it("should cancel morphing with beforeNodeVisited returning false in morphChildElement - line 300", () => {
638 // Line 300: return early when beforeNodeMorphed returns false in morphChildElement
639 const parent = document.createElement("div")
640 const child = document.createElement("div")
641 child.id = "child"
642 parent.appendChild(child)
643
644 const reference = document.createElement("div")
645 const refChild = document.createElement("div")
646 refChild.id = "child"
647 refChild.textContent = "updated"
648 reference.appendChild(refChild)
649
650 let callbackInvoked = false
651 morph(parent, reference, {
652 beforeNodeVisited: (node) => {
653 if (node === child) {
654 callbackInvoked = true
655 return false // This triggers line 300 return
656 }
657 return true
658 },
659 })
660
661 expect(callbackInvoked).toBe(true)
662 // Child should not be updated because callback returned false
663 expect(child.textContent).toBe("")
664 })
665
666 it("should add completely new element type with no matches - lines 386-389", () => {
667 // Lines 386-389: else branch where no nextMatchByTagName exists
668 // Need a reference child with a tag that doesn't exist anywhere in parent
669 const parent = document.createElement("div")
670 const p = document.createElement("p")
671 p.textContent = "paragraph"
672 parent.appendChild(p)
673
674 const reference = document.createElement("div")
675 // Use custom element or uncommon tag
676 const custom = document.createElement("custom-element")
677 custom.textContent = "custom"
678 const refP = document.createElement("p")
679 reference.appendChild(custom)
680 reference.appendChild(refP)
681
682 let beforeCalled = false
683 let afterCalled = false
684
685 morph(parent, reference, {
686 beforeNodeAdded: (_parent, node, _insertionPoint) => {
687 if ((node as Element).tagName === "CUSTOM-ELEMENT") {
688 beforeCalled = true
689 return true // Line 387-388
690 }
691 return true
692 },
693 afterNodeAdded: (node) => {
694 if ((node as Element).tagName === "CUSTOM-ELEMENT") {
695 afterCalled = true // Line 389
696 }
697 },
698 })
699
700 expect(beforeCalled).toBe(true)
701 expect(afterCalled).toBe(true)
702 })
703 })
704 })
705 })
706})