···97979898 it("should trigger line 402 by moving an element in browsers with moveBefore", () => {
9999 // Mock moveBefore if it doesn't exist
100100- const originalMoveBefore = Element.prototype.moveBefore
100100+ const originalMoveBefore = (Element.prototype as any).moveBefore
101101 if (!originalMoveBefore) {
102102 // Since moveBefore doesn't exist in happy-dom, we can't test line 402
103103 // This line is only reachable in real browsers that support moveBefore
+411
test/morphlex-uncovered.test.ts
···11+import { describe, it, expect, vi } from "vitest"
22+import { morph, morphInner } from "../src/morphlex"
33+44+describe("Morphlex - Remaining Uncovered Lines", () => {
55+ describe("Invalid HTML string error (line 39)", () => {
66+ it("should verify the error is thrown with correct message and stack trace", () => {
77+ const div = document.createElement("div")
88+ div.innerHTML = "<span>Test</span>"
99+ document.body.appendChild(div)
1010+1111+ // Verify the error is actually thrown from the correct line
1212+ try {
1313+ morphInner(div.firstChild!, "<p>First</p><p>Second</p>")
1414+ expect.fail("Should have thrown an error")
1515+ } catch (e: any) {
1616+ expect(e.message).toBe("[Morphlex] The string was not a valid HTML element.")
1717+ // The error should be thrown from morphInner function
1818+ expect(e.stack).toContain("morphInner")
1919+ }
2020+2121+ div.remove()
2222+ })
2323+2424+ it("should throw error when string contains multiple root elements", () => {
2525+ const div = document.createElement("div")
2626+ div.innerHTML = "<span>Test</span>"
2727+ document.body.appendChild(div)
2828+2929+ // String with multiple root elements should throw when using morphInner
3030+ expect(() => {
3131+ morphInner(div.firstChild!, "<p>First</p><p>Second</p>")
3232+ }).toThrow("[Morphlex] The string was not a valid HTML element.")
3333+3434+ div.remove()
3535+ })
3636+3737+ it("should throw error when string contains only text content", () => {
3838+ const div = document.createElement("div")
3939+ div.innerHTML = "<span>Test</span>"
4040+ document.body.appendChild(div)
4141+4242+ // String with only text (no element) should throw
4343+ expect(() => {
4444+ morphInner(div.firstChild!, "Just plain text")
4545+ }).toThrow("[Morphlex] The string was not a valid HTML element.")
4646+4747+ div.remove()
4848+ })
4949+5050+ it("should throw error when string contains comment only", () => {
5151+ const div = document.createElement("div")
5252+ div.innerHTML = "<span>Test</span>"
5353+ document.body.appendChild(div)
5454+5555+ // String with only a comment should throw
5656+ expect(() => {
5757+ morphInner(div.firstChild!, "<!-- just a comment -->")
5858+ }).toThrow("[Morphlex] The string was not a valid HTML element.")
5959+6060+ div.remove()
6161+ })
6262+6363+ it("should throw error when string is empty", () => {
6464+ const div = document.createElement("div")
6565+ div.innerHTML = "<span>Test</span>"
6666+ document.body.appendChild(div)
6767+6868+ // Empty string should throw
6969+ expect(() => {
7070+ morphInner(div.firstChild!, "")
7171+ }).toThrow("[Morphlex] The string was not a valid HTML element.")
7272+7373+ div.remove()
7474+ })
7575+7676+ it("should throw error when string contains whitespace only", () => {
7777+ const div = document.createElement("div")
7878+ div.innerHTML = "<span>Test</span>"
7979+ document.body.appendChild(div)
8080+8181+ // Whitespace-only string should throw
8282+ expect(() => {
8383+ morphInner(div.firstChild!, " \n\t ")
8484+ }).toThrow("[Morphlex] The string was not a valid HTML element.")
8585+8686+ div.remove()
8787+ })
8888+8989+ it("should throw error when morphInner receives string with text and element", () => {
9090+ const div = document.createElement("div")
9191+ div.innerHTML = "<span>Test</span>"
9292+ document.body.appendChild(div)
9393+9494+ // String with text before element
9595+ expect(() => {
9696+ morphInner(div.firstChild!, "text before <div>element</div>")
9797+ }).toThrow("[Morphlex] The string was not a valid HTML element.")
9898+9999+ div.remove()
100100+ })
101101+ })
102102+103103+ describe("morphOneToMany with empty array (lines 116-125)", () => {
104104+ it("should remove node when morphing to empty NodeList", () => {
105105+ const parent = document.createElement("div")
106106+ const child = document.createElement("span")
107107+ child.textContent = "Will be removed"
108108+ parent.appendChild(child)
109109+ document.body.appendChild(parent)
110110+111111+ // Create an empty NodeList by parsing empty content
112112+ const template = document.createElement("template")
113113+ const emptyNodeList = template.content.childNodes
114114+115115+ // Morph the child to empty NodeList
116116+ morph(child, emptyNodeList)
117117+118118+ // Child should be removed from parent
119119+ expect(parent.children.length).toBe(0)
120120+ expect(parent.contains(child)).toBe(false)
121121+122122+ parent.remove()
123123+ })
124124+125125+ it("should remove node when morphing to empty string parsed as NodeList", () => {
126126+ const parent = document.createElement("div")
127127+ const element = document.createElement("p")
128128+ element.id = "test-element"
129129+ element.textContent = "Original"
130130+ parent.appendChild(element)
131131+ document.body.appendChild(parent)
132132+133133+ // Morph to empty string (gets parsed to empty NodeList)
134134+ morph(element, "")
135135+136136+ // Element should be removed
137137+ expect(parent.querySelector("#test-element")).toBe(null)
138138+ expect(parent.children.length).toBe(0)
139139+140140+ parent.remove()
141141+ })
142142+143143+ it("should call beforeNodeRemoved/afterNodeRemoved when removing via empty NodeList", () => {
144144+ const parent = document.createElement("div")
145145+ const child = document.createElement("span")
146146+ child.id = "to-remove"
147147+ parent.appendChild(child)
148148+ document.body.appendChild(parent)
149149+150150+ let beforeRemoveCalled = false
151151+ let afterRemoveCalled = false
152152+ let removedNode: Node | null = null
153153+154154+ // Create empty NodeList
155155+ const template = document.createElement("template")
156156+ const emptyNodeList = template.content.childNodes
157157+158158+ // Morph with callbacks
159159+ morph(child, emptyNodeList, {
160160+ beforeNodeRemoved: (node) => {
161161+ beforeRemoveCalled = true
162162+ removedNode = node
163163+ return true
164164+ },
165165+ afterNodeRemoved: (node) => {
166166+ afterRemoveCalled = true
167167+ expect(node).toBe(removedNode)
168168+ },
169169+ })
170170+171171+ expect(beforeRemoveCalled).toBe(true)
172172+ expect(afterRemoveCalled).toBe(true)
173173+ expect(removedNode).toBe(child)
174174+ expect(parent.children.length).toBe(0)
175175+176176+ parent.remove()
177177+ })
178178+179179+ it("should not remove node when beforeNodeRemoved returns false", () => {
180180+ const parent = document.createElement("div")
181181+ const child = document.createElement("span")
182182+ child.textContent = "Should not be removed"
183183+ parent.appendChild(child)
184184+ document.body.appendChild(parent)
185185+186186+ // Create empty NodeList
187187+ const template = document.createElement("template")
188188+ const emptyNodeList = template.content.childNodes
189189+190190+ // Morph with beforeNodeRemoved returning false
191191+ morph(child, emptyNodeList, {
192192+ beforeNodeRemoved: () => false,
193193+ })
194194+195195+ // Child should still be in parent
196196+ expect(parent.children.length).toBe(1)
197197+ expect(parent.contains(child)).toBe(true)
198198+199199+ parent.remove()
200200+ })
201201+202202+ it("should morph one element to multiple elements from string", () => {
203203+ const parent = document.createElement("div")
204204+ const single = document.createElement("span")
205205+ single.id = "single"
206206+ single.textContent = "Single"
207207+ parent.appendChild(single)
208208+ document.body.appendChild(parent)
209209+210210+ // Morph single element to multiple elements using a string
211211+ morph(single, "<span id='first'>First</span><span id='second'>Second</span><span id='third'>Third</span>")
212212+213213+ // Should have morphed the first element and added the rest
214214+ expect(parent.children.length).toBe(3)
215215+ expect(parent.children[0].id).toBe("first")
216216+ expect(parent.children[0].textContent).toBe("First")
217217+ expect(parent.children[1].id).toBe("second")
218218+ expect(parent.children[2].id).toBe("third")
219219+220220+ parent.remove()
221221+ })
222222+223223+ it("should call callbacks when morphing one to many", () => {
224224+ const parent = document.createElement("div")
225225+ const single = document.createElement("span")
226226+ single.textContent = "Single"
227227+ parent.appendChild(single)
228228+ document.body.appendChild(parent)
229229+230230+ const addedNodes: Node[] = []
231231+ let morphedCalled = false
232232+233233+ morph(single, "<span>First</span><span>Second</span>", {
234234+ beforeNodeAdded: (_node) => {
235235+ return true // Allow addition
236236+ },
237237+ afterNodeAdded: (node) => {
238238+ addedNodes.push(node)
239239+ },
240240+ afterNodeMorphed: (_from, _to) => {
241241+ morphedCalled = true
242242+ // The 'from' could be the original single element or its child nodes after morphing
243243+ // Just verify the callback was called
244244+ },
245245+ })
246246+247247+ expect(morphedCalled).toBe(true)
248248+ expect(addedNodes.length).toBe(1) // Only second span was added, first was morphed
249249+ expect(parent.children.length).toBe(2)
250250+251251+ parent.remove()
252252+ })
253253+254254+ it("should prevent adding nodes when beforeNodeAdded returns false", () => {
255255+ const parent = document.createElement("div")
256256+ const single = document.createElement("span")
257257+ single.id = "original"
258258+ single.textContent = "Original"
259259+ parent.appendChild(single)
260260+ document.body.appendChild(parent)
261261+262262+ morph(single, "<span id='first'>First</span><span id='second'>Second</span>", {
263263+ beforeNodeAdded: () => false, // Prevent all additions
264264+ })
265265+266266+ // Only the first element should be morphed, second should not be added
267267+ expect(parent.children.length).toBe(1)
268268+ expect(parent.children[0].id).toBe("first") // First was morphed
269269+ expect(parent.children[0].textContent).toBe("First")
270270+271271+ parent.remove()
272272+ })
273273+274274+ it("should handle morphing to single text node", () => {
275275+ const parent = document.createElement("div")
276276+ const element = document.createElement("span")
277277+ element.textContent = "Element"
278278+ parent.appendChild(element)
279279+ document.body.appendChild(parent)
280280+281281+ // Morph to just text content (which creates a text node in NodeList)
282282+ morph(element, "Just text")
283283+284284+ // Element should be replaced with text node
285285+ expect(parent.children.length).toBe(0) // No elements
286286+ expect(parent.textContent).toBe("Just text")
287287+288288+ parent.remove()
289289+ })
290290+ })
291291+292292+ describe("moveBefore API usage (line 66)", () => {
293293+ it("should use moveBefore when available and node is in same parent", () => {
294294+ const parent = document.createElement("div")
295295+ const child1 = document.createElement("span")
296296+ child1.id = "first"
297297+ const child2 = document.createElement("span")
298298+ child2.id = "second"
299299+300300+ parent.appendChild(child1)
301301+ parent.appendChild(child2)
302302+ document.body.appendChild(parent)
303303+304304+ // Mock moveBefore if it doesn't exist, to test the condition
305305+ const originalMoveBefore = (parent as any).moveBefore
306306+ if (!("moveBefore" in parent)) {
307307+ // Add a mock moveBefore to test the branch
308308+ ;(parent as any).moveBefore = vi.fn((node: Node, before: Node | null) => {
309309+ // Simulate moveBefore behavior
310310+ if (node.parentNode === parent) {
311311+ parent.insertBefore(node, before)
312312+ }
313313+ })
314314+ }
315315+316316+ // Morph to reverse order - should trigger moveBefore if available
317317+ morph(parent, '<div><span id="second"></span><span id="first"></span></div>')
318318+319319+ // Check order is reversed
320320+ expect(parent.children[0].id).toBe("second")
321321+ expect(parent.children[1].id).toBe("first")
322322+323323+ // Restore original moveBefore (if it existed)
324324+ if (originalMoveBefore === undefined) {
325325+ delete (parent as any).moveBefore
326326+ } else {
327327+ ;(parent as any).moveBefore = originalMoveBefore
328328+ }
329329+330330+ parent.remove()
331331+ })
332332+333333+ it("should fall back to insertBefore when moveBefore is not available", () => {
334334+ const parent = document.createElement("div")
335335+ const child1 = document.createElement("span")
336336+ child1.id = "a"
337337+ const child2 = document.createElement("span")
338338+ child2.id = "b"
339339+340340+ parent.appendChild(child1)
341341+ parent.appendChild(child2)
342342+ document.body.appendChild(parent)
343343+344344+ // Ensure moveBefore is not available
345345+ const originalMoveBefore = (parent as any).moveBefore
346346+ if ("moveBefore" in parent) {
347347+ delete (parent as any).moveBefore
348348+ }
349349+350350+ // Morph to reverse order - should use insertBefore fallback
351351+ morph(parent, '<div><span id="b"></span><span id="a"></span></div>')
352352+353353+ // Check order is reversed
354354+ expect(parent.children[0].id).toBe("b")
355355+ expect(parent.children[1].id).toBe("a")
356356+357357+ // Restore original moveBefore if it existed
358358+ if (originalMoveBefore !== undefined) {
359359+ ;(parent as any).moveBefore = originalMoveBefore
360360+ }
361361+362362+ parent.remove()
363363+ })
364364+365365+ it("should use insertBefore when node is not already in the same parent", () => {
366366+ const parent1 = document.createElement("div")
367367+ const parent2 = document.createElement("div")
368368+ const child = document.createElement("span")
369369+ child.id = "movable"
370370+ child.textContent = "Move me"
371371+372372+ parent2.appendChild(child)
373373+ document.body.appendChild(parent1)
374374+ document.body.appendChild(parent2)
375375+376376+ // Add mock moveBefore to parent1
377377+ let moveBeforeCalled = false
378378+ const originalMoveBefore = (parent1 as any).moveBefore
379379+ if (!("moveBefore" in parent1)) {
380380+ ;(parent1 as any).moveBefore = vi.fn(() => {
381381+ moveBeforeCalled = true
382382+ })
383383+ }
384384+385385+ // Create a reference element in parent1
386386+ const reference = document.createElement("span")
387387+ reference.id = "ref"
388388+ parent1.appendChild(reference)
389389+390390+ // Morph parent1 to include the child from parent2
391391+ // The child with id="movable" will be found in parent2 and moved to parent1
392392+ morph(parent1, '<div><span id="movable">Move me</span><span id="ref"></span></div>')
393393+394394+ // moveBefore should NOT be called since node was in different parent
395395+ expect(moveBeforeCalled).toBe(false)
396396+397397+ // Child should now be in parent1
398398+ expect(parent1.querySelector("#movable")).toBeTruthy()
399399+400400+ // Restore original moveBefore
401401+ if (originalMoveBefore === undefined) {
402402+ delete (parent1 as any).moveBefore
403403+ } else {
404404+ ;(parent1 as any).moveBefore = originalMoveBefore
405405+ }
406406+407407+ parent1.remove()
408408+ parent2.remove()
409409+ })
410410+ })
411411+})