import { describe, it, expect, beforeEach, afterEach } from "vitest" import { morph, morphInner } from "../src/morphlex" describe("Morphlex Vitest Suite", () => { let container: HTMLElement beforeEach(() => { container = document.createElement("div") document.body.appendChild(container) }) afterEach(() => { if (container && container.parentNode) { container.parentNode.removeChild(container) } }) describe("morph() - Basic functionality", () => { it("should update text content", () => { const original = document.createElement("div") original.textContent = "Hello" const reference = document.createElement("div") reference.textContent = "World" morph(original, reference) expect(original.textContent).toBe("World") }) it("should accept HTML string as reference", () => { const original = document.createElement("div") original.textContent = "Old" morph(original, "
New
") expect(original.textContent).toBe("New") }) it("should preserve element when morphing matching tags", () => { const original = document.createElement("div") original.id = "test" const elementRef = original const reference = document.createElement("div") reference.textContent = "Updated" morph(original, reference) expect(original).toBe(elementRef) expect(original.textContent).toBe("Updated") }) it("should replace element when morphing different tags", () => { const original = document.createElement("div") const parent = document.createElement("section") parent.appendChild(original) const reference = document.createElement("span") reference.textContent = "Updated" morph(original, reference) expect(parent.querySelector("span")).toBeTruthy() expect(parent.querySelector("div")).toBeFalsy() }) }) describe("morph() - Attribute handling", () => { it("should add attributes", () => { const original = document.createElement("button") const reference = document.createElement("button") reference.setAttribute("class", "btn-primary") reference.setAttribute("disabled", "") morph(original, reference) expect(original.className).toBe("btn-primary") expect(original.hasAttribute("disabled")).toBe(true) }) it("should remove attributes", () => { const original = document.createElement("div") original.setAttribute("data-test", "value") const reference = document.createElement("div") morph(original, reference) expect(original.hasAttribute("data-test")).toBe(false) }) it("should update attributes", () => { const original = document.createElement("div") original.setAttribute("data-value", "old") const reference = document.createElement("div") reference.setAttribute("data-value", "new") morph(original, reference) expect(original.getAttribute("data-value")).toBe("new") }) it("should update class attribute", () => { const original = document.createElement("div") original.className = "old-class" const reference = document.createElement("div") reference.className = "new-class" morph(original, reference) expect(original.className).toBe("new-class") }) }) describe("morph() - Child elements", () => { it("should add child elements", () => { const original = document.createElement("ul") const reference = document.createElement("ul") const li1 = document.createElement("li") li1.textContent = "Item 1" const li2 = document.createElement("li") li2.textContent = "Item 2" reference.appendChild(li1) reference.appendChild(li2) morph(original, reference) expect(original.children.length).toBe(2) expect(original.children[0].textContent).toBe("Item 1") }) it("should remove excess child elements", () => { const original = document.createElement("ul") original.innerHTML = "
  • A
  • B
  • C
  • " const reference = document.createElement("ul") reference.innerHTML = "
  • A
  • " morph(original, reference) expect(original.children.length).toBe(1) }) it("should morph existing child elements", () => { const original = document.createElement("div") const child = document.createElement("span") child.textContent = "old" original.appendChild(child) const reference = document.createElement("div") const refChild = document.createElement("span") refChild.textContent = "new" reference.appendChild(refChild) morph(original, reference) expect(original.children[0].textContent).toBe("new") }) it("should handle text nodes", () => { const original = document.createElement("div") original.appendChild(document.createTextNode("Hello")) const reference = document.createElement("div") reference.appendChild(document.createTextNode("World")) morph(original, reference) expect(original.textContent).toBe("World") }) it("should handle mixed text and element nodes", () => { const original = document.createElement("div") original.appendChild(document.createTextNode("Start ")) const span = document.createElement("span") span.textContent = "middle" original.appendChild(span) original.appendChild(document.createTextNode(" end")) const reference = document.createElement("div") reference.appendChild(document.createTextNode("Start ")) const refSpan = document.createElement("span") refSpan.textContent = "updated" reference.appendChild(refSpan) reference.appendChild(document.createTextNode(" end")) morph(original, reference) expect(original.textContent).toBe("Start updated end") }) }) describe("morph() - Element identity and IDs", () => { it("should preserve element identity when using IDs", () => { const original = document.createElement("div") original.innerHTML = '

    Para 1

    Para 2

    ' const reference = document.createElement("div") reference.innerHTML = '

    Para 2

    Para 1

    ' const p1Original = original.querySelector("#p1") morph(original, reference) const p1After = original.querySelector("#p1") expect(p1After).toBe(p1Original) }) it("should reorder elements with IDs correctly", () => { const original = document.createElement("div") original.innerHTML = 'ABC' const reference = document.createElement("div") reference.innerHTML = 'CAB' const originalA = original.querySelector("#a") morph(original, reference) const newA = original.querySelector("#a") expect(newA).toBe(originalA) expect(original.children[1]).toBe(newA) }) }) describe("morph() - Callbacks", () => { it("should call beforeNodeVisited and afterNodeVisited", () => { const original = document.createElement("div") original.textContent = "Before" const reference = document.createElement("div") reference.textContent = "After" let beforeCalled = false let afterCalled = false morph(original, reference, { beforeNodeVisited: () => { beforeCalled = true return true }, afterNodeVisited: () => { afterCalled = true }, }) expect(beforeCalled).toBe(true) expect(afterCalled).toBe(true) }) it("should cancel morphing if beforeNodeVisited returns false", () => { const original = document.createElement("div") original.textContent = "Original" const reference = document.createElement("div") reference.textContent = "Reference" morph(original, reference, { beforeNodeVisited: () => false, }) expect(original.textContent).toBe("Original") }) it("should call beforeNodeAdded and afterNodeAdded", () => { const original = document.createElement("div") const reference = document.createElement("div") const newChild = document.createElement("p") newChild.textContent = "New" reference.appendChild(newChild) let beforeAddCalled = false let afterAddCalled = false morph(original, reference, { beforeNodeAdded: () => { beforeAddCalled = true return true }, afterNodeAdded: () => { afterAddCalled = true }, }) expect(beforeAddCalled).toBe(true) expect(afterAddCalled).toBe(true) }) it("should call beforeNodeRemoved and afterNodeRemoved", () => { const original = document.createElement("div") const child = document.createElement("p") child.textContent = "To remove" original.appendChild(child) const reference = document.createElement("div") let beforeRemoveCalled = false let afterRemoveCalled = false morph(original, reference, { beforeNodeRemoved: () => { beforeRemoveCalled = true return true }, afterNodeRemoved: () => { afterRemoveCalled = true }, }) expect(beforeRemoveCalled).toBe(true) expect(afterRemoveCalled).toBe(true) }) it("should call attribute update callbacks", () => { const original = document.createElement("div") const reference = document.createElement("div") reference.setAttribute("data-test", "value") let callbackCalled = false morph(original, reference, { afterAttributeUpdated: (element, attrName) => { if (attrName === "data-test") { callbackCalled = true } }, }) expect(callbackCalled).toBe(true) }) }) describe("morph() - Form elements", () => { it("should update textarea value", () => { const original = document.createElement("textarea") as HTMLTextAreaElement original.textContent = "old text" const reference = document.createElement("textarea") as HTMLTextAreaElement reference.textContent = "new text" morph(original, reference) expect(original.textContent).toBe("new text") }) }) describe("morphInner() - Basic functionality", () => { it("should morph inner content only", () => { const original = document.createElement("div") original.id = "container" original.innerHTML = "

    Old

    " const reference = document.createElement("div") reference.innerHTML = "

    New

    " morphInner(original, reference) expect(original.id).toBe("container") expect(original.innerHTML).toBe("

    New

    ") }) it("should accept string reference for morphInner", () => { const original = document.createElement("div") original.innerHTML = "Old" const reference = document.createElement("div") reference.innerHTML = "New" morphInner(original, reference) expect(original.innerHTML).toBe("New") }) it("should preserve outer element attributes with morphInner", () => { const original = document.createElement("div") original.setAttribute("class", "container") original.setAttribute("data-id", "123") original.innerHTML = "

    Old

    " const reference = document.createElement("div") reference.setAttribute("class", "different") reference.innerHTML = "

    New

    " morphInner(original, reference) expect(original.getAttribute("class")).toBe("container") expect(original.getAttribute("data-id")).toBe("123") expect(original.innerHTML).toBe("

    New

    ") }) it("should update multiple children with morphInner", () => { const original = document.createElement("ul") original.innerHTML = "
  • Item 1
  • Item 2
  • " const reference = document.createElement("ul") reference.innerHTML = "
  • Item A
  • Item B
  • Item C
  • " morphInner(original, reference) expect(original.children.length).toBe(3) expect(original.children[0].textContent).toBe("Item A") expect(original.children[2].textContent).toBe("Item C") }) it("should empty contents with morphInner when reference has no children", () => { const original = document.createElement("div") original.innerHTML = "Content

    More

    " const reference = document.createElement("div") morphInner(original, reference) expect(original.children.length).toBe(0) }) }) describe("Edge cases and complex scenarios", () => { it("should handle empty elements", () => { const original = document.createElement("div") const reference = document.createElement("div") expect(() => morph(original, reference)).not.toThrow() expect(original.children.length).toBe(0) }) it("should handle deeply nested structures", () => { const original = document.createElement("div") original.innerHTML = "
    Deep
    " const reference = document.createElement("div") reference.innerHTML = "
    Updated
    " morph(original, reference) expect(original.querySelector("span")?.textContent).toBe("Updated") }) it("should handle special characters in text", () => { const original = document.createElement("div") original.textContent = "Hello & goodbye" const reference = document.createElement("div") reference.textContent = 'Special <> characters "test"' morph(original, reference) expect(original.textContent).toBe('Special <> characters "test"') }) it("should handle multiple class names", () => { const original = document.createElement("div") original.classList.add("class1", "class2", "class3") const reference = document.createElement("div") reference.classList.add("class2", "class3", "class4") morph(original, reference) expect(original.classList.contains("class2")).toBe(true) expect(original.classList.contains("class3")).toBe(true) expect(original.classList.contains("class4")).toBe(true) expect(original.classList.contains("class1")).toBe(false) }) it("should handle element replacement", () => { const original = document.createElement("div") const span = document.createElement("span") span.textContent = "Span" original.appendChild(span) const reference = document.createElement("div") const p = document.createElement("p") p.textContent = "Paragraph" reference.appendChild(p) morph(original, reference) expect(original.children[0].nodeName).toBe("P") expect(original.children[0].textContent).toBe("Paragraph") }) it("should handle list updates with ID preservation", () => { const original = document.createElement("ul") original.innerHTML = '
  • Item 1
  • Item 2
  • Item 3
  • ' const item2Ref = original.querySelector("#item-2") const reference = document.createElement("ul") reference.innerHTML = '
  • Item 1
  • Item 3
  • Item 2
  • Item 4
  • ' morph(original, reference) expect(original.querySelector("#item-2")).toBe(item2Ref) expect(original.children.length).toBe(4) }) it("should handle complex page-like structure", () => { const original = document.createElement("main") original.innerHTML = `

    Old paragraph

    ` const reference = document.createElement("main") reference.innerHTML = `

    New paragraph

    Another paragraph

    ` const headerRef = original.querySelector("#header") morph(original, reference) expect(original.querySelector("#header")).toBe(headerRef) expect(original.querySelector("h1")?.textContent).toBe("New Title") expect(original.querySelectorAll("article p").length).toBe(2) }) it("should handle nested element morphing with updates", () => { const original = document.createElement("div") original.innerHTML = "

    Old content

    " const reference = document.createElement("div") reference.innerHTML = "

    New text

    " morph(original, reference) expect(original.innerHTML).toBe("

    New text

    ") }) it("should preserve element reference through morph", () => { const original = document.createElement("div") original.textContent = "Original" const reference = document.createElement("div") reference.textContent = "Updated" const originalRef = original morph(original, reference) expect(original).toBe(originalRef) expect(original.textContent).toBe("Updated") }) }) })