import { describe, it, expect, vi } from "vitest"
import { morph, morphInner } from "../src/morphlex"
describe("Morphlex - Remaining Uncovered Lines", () => {
describe("Invalid HTML string error (line 39)", () => {
it("should verify the error is thrown with correct message and stack trace", () => {
const div = document.createElement("div")
div.innerHTML = "Test"
document.body.appendChild(div)
// Verify the error is actually thrown from the correct line
try {
morphInner(div.firstChild!, "
First
Second
")
expect.fail("Should have thrown an error")
} catch (e: any) {
expect(e.message).toBe("[Morphlex] The string was not a valid HTML element.")
// The error should be thrown from morphInner function
expect(e.stack).toContain("morphInner")
}
div.remove()
})
it("should throw error when string contains multiple root elements", () => {
const div = document.createElement("div")
div.innerHTML = "Test"
document.body.appendChild(div)
// String with multiple root elements should throw when using morphInner
expect(() => {
morphInner(div.firstChild!, "First
Second
")
}).toThrow("[Morphlex] The string was not a valid HTML element.")
div.remove()
})
it("should throw error when string contains only text content", () => {
const div = document.createElement("div")
div.innerHTML = "Test"
document.body.appendChild(div)
// String with only text (no element) should throw
expect(() => {
morphInner(div.firstChild!, "Just plain text")
}).toThrow("[Morphlex] The string was not a valid HTML element.")
div.remove()
})
it("should throw error when string contains comment only", () => {
const div = document.createElement("div")
div.innerHTML = "Test"
document.body.appendChild(div)
// String with only a comment should throw
expect(() => {
morphInner(div.firstChild!, "")
}).toThrow("[Morphlex] The string was not a valid HTML element.")
div.remove()
})
it("should throw error when string is empty", () => {
const div = document.createElement("div")
div.innerHTML = "Test"
document.body.appendChild(div)
// Empty string should throw
expect(() => {
morphInner(div.firstChild!, "")
}).toThrow("[Morphlex] The string was not a valid HTML element.")
div.remove()
})
it("should throw error when string contains whitespace only", () => {
const div = document.createElement("div")
div.innerHTML = "Test"
document.body.appendChild(div)
// Whitespace-only string should throw
expect(() => {
morphInner(div.firstChild!, " \n\t ")
}).toThrow("[Morphlex] The string was not a valid HTML element.")
div.remove()
})
it("should throw error when morphInner receives string with text and element", () => {
const div = document.createElement("div")
div.innerHTML = "Test"
document.body.appendChild(div)
// String with text before element
expect(() => {
morphInner(div.firstChild!, "text before element
")
}).toThrow("[Morphlex] The string was not a valid HTML element.")
div.remove()
})
})
describe("morphOneToMany with empty array (lines 116-125)", () => {
it("should remove node when morphing to empty NodeList", () => {
const parent = document.createElement("div")
const child = document.createElement("span")
child.textContent = "Will be removed"
parent.appendChild(child)
document.body.appendChild(parent)
// Create an empty NodeList by parsing empty content
const template = document.createElement("template")
const emptyNodeList = template.content.childNodes
// Morph the child to empty NodeList
morph(child, emptyNodeList)
// Child should be removed from parent
expect(parent.children.length).toBe(0)
expect(parent.contains(child)).toBe(false)
parent.remove()
})
it("should remove node when morphing to empty string parsed as NodeList", () => {
const parent = document.createElement("div")
const element = document.createElement("p")
element.id = "test-element"
element.textContent = "Original"
parent.appendChild(element)
document.body.appendChild(parent)
// Morph to empty string (gets parsed to empty NodeList)
morph(element, "")
// Element should be removed
expect(parent.querySelector("#test-element")).toBe(null)
expect(parent.children.length).toBe(0)
parent.remove()
})
it("should call beforeNodeRemoved/afterNodeRemoved when removing via empty NodeList", () => {
const parent = document.createElement("div")
const child = document.createElement("span")
child.id = "to-remove"
parent.appendChild(child)
document.body.appendChild(parent)
let beforeRemoveCalled = false
let afterRemoveCalled = false
let removedNode: Node | null = null
// Create empty NodeList
const template = document.createElement("template")
const emptyNodeList = template.content.childNodes
// Morph with callbacks
morph(child, emptyNodeList, {
beforeNodeRemoved: (node) => {
beforeRemoveCalled = true
removedNode = node
return true
},
afterNodeRemoved: (node) => {
afterRemoveCalled = true
expect(node).toBe(removedNode)
},
})
expect(beforeRemoveCalled).toBe(true)
expect(afterRemoveCalled).toBe(true)
expect(removedNode).toBe(child)
expect(parent.children.length).toBe(0)
parent.remove()
})
it("should not remove node when beforeNodeRemoved returns false", () => {
const parent = document.createElement("div")
const child = document.createElement("span")
child.textContent = "Should not be removed"
parent.appendChild(child)
document.body.appendChild(parent)
// Create empty NodeList
const template = document.createElement("template")
const emptyNodeList = template.content.childNodes
// Morph with beforeNodeRemoved returning false
morph(child, emptyNodeList, {
beforeNodeRemoved: () => false,
})
// Child should still be in parent
expect(parent.children.length).toBe(1)
expect(parent.contains(child)).toBe(true)
parent.remove()
})
it("should morph one element to multiple elements from string", () => {
const parent = document.createElement("div")
const single = document.createElement("span")
single.id = "single"
single.textContent = "Single"
parent.appendChild(single)
document.body.appendChild(parent)
// Morph single element to multiple elements using a string
morph(single, "FirstSecondThird")
// Should have morphed the first element and added the rest
expect(parent.children.length).toBe(3)
expect(parent.children[0].id).toBe("first")
expect(parent.children[0].textContent).toBe("First")
expect(parent.children[1].id).toBe("second")
expect(parent.children[2].id).toBe("third")
parent.remove()
})
it("should call callbacks when morphing one to many", () => {
const parent = document.createElement("div")
const single = document.createElement("span")
single.textContent = "Single"
parent.appendChild(single)
document.body.appendChild(parent)
const addedNodes: Node[] = []
let morphedCalled = false
morph(single, "FirstSecond", {
beforeNodeAdded: (_node) => {
return true // Allow addition
},
afterNodeAdded: (node) => {
addedNodes.push(node)
},
afterNodeVisited: (_from, _to) => {
morphedCalled = true
// The 'from' could be the original single element or its child nodes after morphing
// Just verify the callback was called
},
})
expect(morphedCalled).toBe(true)
expect(addedNodes.length).toBe(1) // Only second span was added, first was morphed
expect(parent.children.length).toBe(2)
parent.remove()
})
it("should prevent adding nodes when beforeNodeAdded returns false", () => {
const parent = document.createElement("div")
const single = document.createElement("span")
single.id = "original"
single.textContent = "Original"
parent.appendChild(single)
document.body.appendChild(parent)
morph(single, "FirstSecond", {
beforeNodeAdded: () => false, // Prevent all additions
})
// Only the first element should be morphed, second should not be added
expect(parent.children.length).toBe(1)
expect(parent.children[0].id).toBe("first") // First was morphed
expect(parent.children[0].textContent).toBe("First")
parent.remove()
})
it("should handle morphing to single text node", () => {
const parent = document.createElement("div")
const element = document.createElement("span")
element.textContent = "Element"
parent.appendChild(element)
document.body.appendChild(parent)
// Morph to just text content (which creates a text node in NodeList)
morph(element, "Just text")
// Element should be replaced with text node
expect(parent.children.length).toBe(0) // No elements
expect(parent.textContent).toBe("Just text")
parent.remove()
})
})
describe("moveBefore API usage (line 66)", () => {
it("should use moveBefore when available and node is in same parent", () => {
const parent = document.createElement("div")
const child1 = document.createElement("span")
child1.id = "first"
const child2 = document.createElement("span")
child2.id = "second"
parent.appendChild(child1)
parent.appendChild(child2)
document.body.appendChild(parent)
// Mock moveBefore if it doesn't exist, to test the condition
const originalMoveBefore = (parent as any).moveBefore
if (!("moveBefore" in parent)) {
// Add a mock moveBefore to test the branch
;(parent as any).moveBefore = vi.fn((node: Node, before: Node | null) => {
// Simulate moveBefore behavior
if (node.parentNode === parent) {
parent.insertBefore(node, before)
}
})
}
// Morph to reverse order - should trigger moveBefore if available
morph(parent, '
')
// Check order is reversed
expect(parent.children[0].id).toBe("second")
expect(parent.children[1].id).toBe("first")
// Restore original moveBefore (if it existed)
if (originalMoveBefore === undefined) {
delete (parent as any).moveBefore
} else {
;(parent as any).moveBefore = originalMoveBefore
}
parent.remove()
})
it("should fall back to insertBefore when moveBefore is not available", () => {
const parent = document.createElement("div")
const child1 = document.createElement("span")
child1.id = "a"
const child2 = document.createElement("span")
child2.id = "b"
parent.appendChild(child1)
parent.appendChild(child2)
document.body.appendChild(parent)
// Ensure moveBefore is not available
const originalMoveBefore = (parent as any).moveBefore
if ("moveBefore" in parent) {
delete (parent as any).moveBefore
}
// Morph to reverse order - should use insertBefore fallback
morph(parent, '
')
// Check order is reversed
expect(parent.children[0].id).toBe("b")
expect(parent.children[1].id).toBe("a")
// Restore original moveBefore if it existed
if (originalMoveBefore !== undefined) {
;(parent as any).moveBefore = originalMoveBefore
}
parent.remove()
})
it("should use insertBefore when node is not already in the same parent", () => {
const parent1 = document.createElement("div")
const parent2 = document.createElement("div")
const child = document.createElement("span")
child.id = "movable"
child.textContent = "Move me"
parent2.appendChild(child)
document.body.appendChild(parent1)
document.body.appendChild(parent2)
// Add mock moveBefore to parent1
let moveBeforeCalled = false
const originalMoveBefore = (parent1 as any).moveBefore
if (!("moveBefore" in parent1)) {
;(parent1 as any).moveBefore = vi.fn(() => {
moveBeforeCalled = true
})
}
// Create a reference element in parent1
const reference = document.createElement("span")
reference.id = "ref"
parent1.appendChild(reference)
// Morph parent1 to include the child from parent2
// The child with id="movable" will be found in parent2 and moved to parent1
morph(parent1, 'Move me
')
// moveBefore should NOT be called since node was in different parent
expect(moveBeforeCalled).toBe(false)
// Child should now be in parent1
expect(parent1.querySelector("#movable")).toBeTruthy()
// Restore original moveBefore
if (originalMoveBefore === undefined) {
delete (parent1 as any).moveBefore
} else {
;(parent1 as any).moveBefore = originalMoveBefore
}
parent1.remove()
parent2.remove()
})
})
})