import { describe, it, expect, beforeEach, afterEach } from "vitest" import { morph, morphInner } from "../src/morphlex" describe("Morphlex - Coverage Tests", () => { let container: HTMLElement beforeEach(() => { container = document.createElement("div") document.body.appendChild(container) }) afterEach(() => { if (container && container.parentNode) { container.parentNode.removeChild(container) } }) describe("String parsing error cases", () => { it("should throw error when parseElementFromString receives non-element string", () => { const div = document.createElement("div") container.appendChild(div) // Text node is not an element expect(() => { morphInner(div, "Just text") }).toThrow("[Morphlex] The string was not a valid HTML element.") }) it("should parse multiple elements as valid HTML (they go into body)", () => { const div = document.createElement("div") container.appendChild(div) // Multiple root nodes actually work because DOMParser wraps them in body // This test just verifies the parsing works const reference = "
Content
" morph(div, reference) expect(div.textContent).toBe("Content") }) it("should throw error when morphInner called with non-matching elements", () => { const div = document.createElement("div") const span = document.createElement("span") container.appendChild(div) expect(() => { morphInner(div, span) }).toThrow("[Morphlex] You can only do an inner morph with matching elements.") }) }) describe("ariaBusy handling", () => { it("should handle ariaBusy for non-element nodes", () => { const parent = document.createElement("div") const textNode = document.createTextNode("Original") parent.appendChild(textNode) const referenceParent = document.createElement("div") const refTextNode = document.createTextNode("Updated") referenceParent.appendChild(refTextNode) morph(parent, referenceParent) expect(parent.textContent).toBe("Updated") }) }) describe("Property updates", () => { it("should update input disabled property", () => { const parent = document.createElement("div") const input = document.createElement("input") input.disabled = false parent.appendChild(input) const reference = document.createElement("div") const refInput = document.createElement("input") refInput.disabled = true reference.appendChild(refInput) morph(parent, reference) expect(input.disabled).toBe(true) }) it("should not update file input value", () => { const parent = document.createElement("div") const input = document.createElement("input") input.type = "file" parent.appendChild(input) const reference = document.createElement("div") const refInput = document.createElement("input") refInput.type = "file" reference.appendChild(refInput) morph(parent, reference) expect(input.type).toBe("file") }) }) describe("Head element special handling", () => { it("should handle nested head elements in child morphing", () => { const parent = document.createElement("div") const head = document.createElement("head") const meta1 = document.createElement("meta") meta1.setAttribute("name", "test") meta1.setAttribute("content", "value") head.appendChild(meta1) parent.appendChild(head) const reference = document.createElement("div") const refHead = document.createElement("head") const refMeta = document.createElement("meta") refMeta.setAttribute("name", "test") refMeta.setAttribute("content", "updated") refHead.appendChild(refMeta) reference.appendChild(refHead) morph(parent, reference) expect(parent.querySelector("head")).toBeTruthy() }) }) describe("Child element morphing edge cases", () => { it("should handle ID matching with overlapping ID sets", () => { const parent = document.createElement("div") const child1 = document.createElement("div") child1.id = "child1" const nested = document.createElement("span") nested.id = "nested" child1.appendChild(nested) const child2 = document.createElement("div") child2.id = "child2" parent.appendChild(child1) parent.appendChild(child2) const reference = document.createElement("div") const refChild1 = document.createElement("div") refChild1.id = "child1" const refNested = document.createElement("span") refNested.id = "different" refChild1.appendChild(refNested) const refChild2 = document.createElement("div") refChild2.id = "child2" reference.appendChild(refChild1) reference.appendChild(refChild2) morph(parent, reference) expect(parent.children[0].id).toBe("child1") }) it("should insert new node when no match found and beforeNodeAdded returns true", () => { const parent = document.createElement("div") const existing = document.createElement("div") existing.id = "existing" parent.appendChild(existing) const reference = document.createElement("div") const refNew = document.createElement("div") refNew.id = "new" const refExisting = document.createElement("div") refExisting.id = "existing" reference.appendChild(refNew) reference.appendChild(refExisting) let addedNode: Node | null = null morph(parent, reference, { beforeNodeAdded: (node) => { addedNode = node return true }, }) expect(addedNode).toBeTruthy() expect(parent.children[0].id).toBe("new") }) it("should not insert new node when beforeNodeAdded returns false", () => { const parent = document.createElement("div") const existing = document.createElement("div") existing.id = "existing" existing.textContent = "original" parent.appendChild(existing) const reference = document.createElement("div") const refNew = document.createElement("span") refNew.id = "new" refNew.textContent = "new content" const refExisting = document.createElement("div") refExisting.id = "existing" refExisting.textContent = "updated" reference.appendChild(refNew) reference.appendChild(refExisting) let addCallbackCalled = false morph(parent, reference, { beforeNodeAdded: () => { addCallbackCalled = true return false }, }) // beforeNodeAdded should have been called expect(addCallbackCalled).toBe(true) // The existing div will be morphed to match reference expect(parent.children[0].tagName).toBe("DIV") }) it("should call afterNodeVisited for child elements even when new node inserted", () => { const parent = document.createElement("div") const child = document.createElement("div") child.id = "child" parent.appendChild(child) const reference = document.createElement("div") const refChild = document.createElement("span") refChild.id = "newChild" reference.appendChild(refChild) let morphedCalled = false morph(parent, reference, { afterNodeVisited: () => { morphedCalled = true }, }) expect(morphedCalled).toBe(true) }) }) describe("Callback cancellation", () => { it("should call beforeAttributeUpdated and cancel attribute removal when it returns false", () => { const div = document.createElement("div") div.setAttribute("data-keep", "value") div.setAttribute("data-remove", "value") const reference = document.createElement("div") reference.setAttribute("data-keep", "value") morph(div, reference, { beforeAttributeUpdated: (element, name, value) => { if (name === "data-remove" && value === null) { return false // Cancel removal } return true }, }) // Attribute should still be there because callback returned false expect(div.hasAttribute("data-remove")).toBe(true) }) }) describe("Empty ID handling", () => { it("should ignore elements with empty id attribute", () => { const parent = document.createElement("div") const child1 = document.createElement("div") child1.setAttribute("id", "") // Empty ID child1.textContent = "First" const child2 = document.createElement("div") child2.id = "valid-id" child2.textContent = "Second" parent.appendChild(child1) parent.appendChild(child2) const reference = document.createElement("div") const refChild1 = document.createElement("div") refChild1.setAttribute("id", "") refChild1.textContent = "First Updated" const refChild2 = document.createElement("div") refChild2.id = "valid-id" refChild2.textContent = "Second Updated" reference.appendChild(refChild1) reference.appendChild(refChild2) morph(parent, reference) expect(child1.textContent).toBe("First Updated") expect(child2.textContent).toBe("Second Updated") }) }) describe("Complex morphing scenarios", () => { it("should handle mixed content with sensitive and non-sensitive elements", () => { const parent = document.createElement("div") container.appendChild(parent) const div = document.createElement("div") div.id = "div1" const input = document.createElement("input") input.id = "input1" input.value = "test" input.defaultValue = "" const canvas = document.createElement("canvas") canvas.id = "canvas1" parent.appendChild(div) parent.appendChild(input) parent.appendChild(canvas) const reference = document.createElement("div") const refCanvas = document.createElement("canvas") refCanvas.id = "canvas1" const refInput = document.createElement("input") refInput.id = "input1" const refDiv = document.createElement("div") refDiv.id = "div1" reference.appendChild(refCanvas) reference.appendChild(refInput) reference.appendChild(refDiv) morph(parent, reference) expect(parent.children.length).toBe(3) }) it("should match elements by overlapping ID sets", () => { // Test lines 372-373 - matching by overlapping ID sets const parent = document.createElement("div") const outer1 = document.createElement("div") outer1.id = "outer1" const inner1a = document.createElement("span") inner1a.id = "inner1a" const inner1b = document.createElement("span") inner1b.id = "inner1b" outer1.appendChild(inner1a) outer1.appendChild(inner1b) const outer2 = document.createElement("div") outer2.id = "outer2" const inner2 = document.createElement("span") inner2.id = "inner2" outer2.appendChild(inner2) parent.appendChild(outer1) parent.appendChild(outer2) const reference = document.createElement("div") // Reference wants outer1 to come second, but references an inner ID const refOuter2 = document.createElement("div") refOuter2.id = "outer2" const refInner2 = document.createElement("span") refInner2.id = "inner2" refOuter2.appendChild(refInner2) const refOuter1 = document.createElement("div") refOuter1.id = "outer1" const refInner1a = document.createElement("span") refInner1a.id = "inner1a" refOuter1.appendChild(refInner1a) reference.appendChild(refOuter2) reference.appendChild(refOuter1) morph(parent, reference) expect(parent.children[0].id).toBe("outer2") }) it("should add completely new element when no match found by tag or ID", () => { // Test lines 386-389 - adding new node with callbacks const parent = document.createElement("div") const existing = document.createElement("div") existing.id = "existing" parent.appendChild(existing) const reference = document.createElement("div") const refNew = document.createElement("article") refNew.id = "brand-new" refNew.textContent = "New content" const refExisting = document.createElement("div") refExisting.id = "existing" reference.appendChild(refNew) reference.appendChild(refExisting) let addedNode: Node | null = null let afterAddedCalled = false morph(parent, reference, { beforeNodeAdded: (node) => { addedNode = node return true }, afterNodeAdded: () => { afterAddedCalled = true }, }) expect(addedNode).toBeTruthy() expect(afterAddedCalled).toBe(true) expect(parent.children[0].tagName).toBe("ARTICLE") }) describe("DOMParser edge cases", () => { it("should explore parser behavior to trigger line 74", () => { // Line 74 checks if doc.childNodes.length === 1 // This is checking the document's childNodes, not body's childNodes // DOMParser always returns a document with html element as child // So doc.childNodes.length is always 1 (the html element) // The else branch on line 74 appears to be unreachable in normal usage // Let's verify with actual morph call const parent = document.createElement("div") const div = document.createElement("div") div.textContent = "Original" parent.appendChild(div) // This should work fine morph(div, "Test") expect(parent.firstChild?.textContent).toBe("Test") }) }) describe("Additional edge cases for remaining coverage", () => { it("should handle element matching with nested IDs and no direct ID match", () => { // More specific test for lines 372-373 const parent = document.createElement("div") const container1 = document.createElement("section") const child1a = document.createElement("div") child1a.id = "shared-id-a" const child1b = document.createElement("div") child1b.id = "shared-id-b" container1.appendChild(child1a) container1.appendChild(child1b) const container2 = document.createElement("section") const child2 = document.createElement("div") child2.id = "other-id" container2.appendChild(child2) parent.appendChild(container1) parent.appendChild(container2) const reference = document.createElement("div") const refContainer = document.createElement("section") const refChild = document.createElement("div") refChild.id = "shared-id-a" refContainer.appendChild(refChild) reference.appendChild(refContainer) morph(parent, reference) expect(parent.children.length).toBeGreaterThanOrEqual(1) }) it("should insert node before when no ID or tag match exists", () => { // Test for lines 386-389 with different scenario const parent = document.createElement("div") const oldChild = document.createElement("p") oldChild.textContent = "Old" parent.appendChild(oldChild) const reference = document.createElement("div") const newChild = document.createElement("article") newChild.textContent = "New" reference.appendChild(newChild) let beforeAddCalled = false let afterAddCalled = false morph(parent, reference, { beforeNodeAdded: () => { beforeAddCalled = true return true }, afterNodeAdded: () => { afterAddCalled = true }, }) expect(beforeAddCalled).toBe(true) expect(afterAddCalled).toBe(true) }) it("should match by overlapping ID sets in sibling scan - lines 372-373", () => { // Lines 372-373: Match element by overlapping ID sets when ID doesn't match // This requires: currentNode has ID != reference.id, but has nested IDs that overlap const parent = document.createElement("div") // First child with nested IDs const div1 = document.createElement("div") div1.id = "div1" const nested1 = document.createElement("span") nested1.id = "overlap-id" div1.appendChild(nested1) // Second child that's a match by tag name const div2 = document.createElement("div") div2.id = "div2" parent.appendChild(div1) parent.appendChild(div2) // Reference wants div2 first, but references the overlap-id const reference = document.createElement("div") const refDiv = document.createElement("div") refDiv.id = "target" const refNested = document.createElement("span") refNested.id = "overlap-id" // This ID exists nested in div1 refDiv.appendChild(refNested) reference.appendChild(refDiv) morph(parent, reference) expect(parent.children.length).toBeGreaterThanOrEqual(1) }) it("should add new node when no tag match exists - lines 386-389", () => { // Lines 386-389: No nextMatchByTagName, so add new node // This requires the reference child has a tag that doesn't exist in current children const parent = document.createElement("div") const p = document.createElement("p") p.textContent = "Paragraph" parent.appendChild(p) const reference = document.createElement("div") // Use article tag which doesn't exist in parent const article = document.createElement("article") article.textContent = "Article" const refP = document.createElement("p") reference.appendChild(article) reference.appendChild(refP) let addedNode: Node | null = null morph(parent, reference, { beforeNodeAdded: (node) => { addedNode = node return true }, afterNodeAdded: () => { // Lines 388-389 }, }) expect(addedNode).toBeTruthy() expect(parent.querySelector("article")).toBeTruthy() }) it("should trigger line 74 - unreachable error path in parseChildNodeFromString", () => { // Line 74: else throw new Error("[Morphlex] The string was not a valid HTML node."); // This line is actually unreachable because DOMParser always returns doc with childNodes.length === 1 // However, we can document this as a known unreachable path // The parser always creates: doc -> html -> (head + body) // So doc.childNodes.length is always 1 (the html element) // All valid HTML strings will pass the check on line 72 const parent = document.createElement("div") const child = document.createElement("div") parent.appendChild(child) // Even empty string parses to valid document structure morph(child, "

Test

") expect(parent.querySelector("p")?.textContent).toBe("Test") }) it("should handle case where child exists but refChild doesn't in loop - lines 332-333", () => { // Lines 332-333 are actually unreachable in the for loop // because we iterate up to refChildNodes.length, so refChild will always exist // The cleanup happens in the while loop below (lines 338-341) // This test documents that lines 332-333 appear to be dead code const parent = document.createElement("div") const child1 = document.createElement("span") child1.textContent = "Keep" const child2 = document.createElement("span") child2.textContent = "Remove via while loop" parent.appendChild(child1) parent.appendChild(child2) const reference = document.createElement("div") const refChild = document.createElement("span") refChild.textContent = "Keep Updated" reference.appendChild(refChild) morph(parent, reference) expect(parent.children.length).toBe(1) }) it("should add new node when no ID or tag match exists - lines 386-389", () => { // Lines 386-389 require: no nextMatchByTagName AND beforeNodeAdded returns true // This means the first child must not match by tag, and no sibling matches either const parent = document.createElement("div") // Use a tag that won't match const article = document.createElement("article") article.id = "article1" article.textContent = "Article" parent.appendChild(article) const reference = document.createElement("div") // First child is a section (different tag), and has no ID match in siblings const section = document.createElement("section") section.id = "section1" section.textContent = "Section" // Second child to trigger morphChildElement for the article const refArticle = document.createElement("article") refArticle.id = "article1" reference.appendChild(section) reference.appendChild(refArticle) let beforeCalled = false let afterCalled = false morph(parent, reference, { beforeNodeAdded: () => { beforeCalled = true return true // Line 387: insertBefore is called }, afterNodeAdded: () => { afterCalled = true // Line 389 }, }) expect(beforeCalled).toBe(true) expect(afterCalled).toBe(true) }) describe("Exact uncovered line tests", () => { it("should cancel morphing with beforeNodeVisited returning false in morphChildElement - line 300", () => { // Line 300: return early when beforeNodeMorphed returns false in morphChildElement const parent = document.createElement("div") const child = document.createElement("div") child.id = "child" parent.appendChild(child) const reference = document.createElement("div") const refChild = document.createElement("div") refChild.id = "child" refChild.textContent = "updated" reference.appendChild(refChild) let callbackInvoked = false morph(parent, reference, { beforeNodeVisited: (node) => { if (node === child) { callbackInvoked = true return false // This triggers line 300 return } return true }, }) expect(callbackInvoked).toBe(true) // Child should not be updated because callback returned false expect(child.textContent).toBe("") }) it("should add completely new element type with no matches - lines 386-389", () => { // Lines 386-389: else branch where no nextMatchByTagName exists // Need a reference child with a tag that doesn't exist anywhere in parent const parent = document.createElement("div") const p = document.createElement("p") p.textContent = "paragraph" parent.appendChild(p) const reference = document.createElement("div") // Use custom element or uncommon tag const custom = document.createElement("custom-element") custom.textContent = "custom" const refP = document.createElement("p") reference.appendChild(custom) reference.appendChild(refP) let beforeCalled = false let afterCalled = false morph(parent, reference, { beforeNodeAdded: (_parent, node, _insertionPoint) => { if ((node as Element).tagName === "CUSTOM-ELEMENT") { beforeCalled = true return true // Line 387-388 } return true }, afterNodeAdded: (node) => { if ((node as Element).tagName === "CUSTOM-ELEMENT") { afterCalled = true // Line 389 } }, }) expect(beforeCalled).toBe(true) expect(afterCalled).toBe(true) }) }) }) }) })