Precise DOM morphing
morphing typescript dom

More tests

+412 -1
+1 -1
test/morphlex-edge-cases.test.ts
··· 97 97 98 98 it("should trigger line 402 by moving an element in browsers with moveBefore", () => { 99 99 // Mock moveBefore if it doesn't exist 100 - const originalMoveBefore = Element.prototype.moveBefore 100 + const originalMoveBefore = (Element.prototype as any).moveBefore 101 101 if (!originalMoveBefore) { 102 102 // Since moveBefore doesn't exist in happy-dom, we can't test line 402 103 103 // This line is only reachable in real browsers that support moveBefore
+411
test/morphlex-uncovered.test.ts
··· 1 + import { describe, it, expect, vi } from "vitest" 2 + import { morph, morphInner } from "../src/morphlex" 3 + 4 + describe("Morphlex - Remaining Uncovered Lines", () => { 5 + describe("Invalid HTML string error (line 39)", () => { 6 + it("should verify the error is thrown with correct message and stack trace", () => { 7 + const div = document.createElement("div") 8 + div.innerHTML = "<span>Test</span>" 9 + document.body.appendChild(div) 10 + 11 + // Verify the error is actually thrown from the correct line 12 + try { 13 + morphInner(div.firstChild!, "<p>First</p><p>Second</p>") 14 + expect.fail("Should have thrown an error") 15 + } catch (e: any) { 16 + expect(e.message).toBe("[Morphlex] The string was not a valid HTML element.") 17 + // The error should be thrown from morphInner function 18 + expect(e.stack).toContain("morphInner") 19 + } 20 + 21 + div.remove() 22 + }) 23 + 24 + it("should throw error when string contains multiple root elements", () => { 25 + const div = document.createElement("div") 26 + div.innerHTML = "<span>Test</span>" 27 + document.body.appendChild(div) 28 + 29 + // String with multiple root elements should throw when using morphInner 30 + expect(() => { 31 + morphInner(div.firstChild!, "<p>First</p><p>Second</p>") 32 + }).toThrow("[Morphlex] The string was not a valid HTML element.") 33 + 34 + div.remove() 35 + }) 36 + 37 + it("should throw error when string contains only text content", () => { 38 + const div = document.createElement("div") 39 + div.innerHTML = "<span>Test</span>" 40 + document.body.appendChild(div) 41 + 42 + // String with only text (no element) should throw 43 + expect(() => { 44 + morphInner(div.firstChild!, "Just plain text") 45 + }).toThrow("[Morphlex] The string was not a valid HTML element.") 46 + 47 + div.remove() 48 + }) 49 + 50 + it("should throw error when string contains comment only", () => { 51 + const div = document.createElement("div") 52 + div.innerHTML = "<span>Test</span>" 53 + document.body.appendChild(div) 54 + 55 + // String with only a comment should throw 56 + expect(() => { 57 + morphInner(div.firstChild!, "<!-- just a comment -->") 58 + }).toThrow("[Morphlex] The string was not a valid HTML element.") 59 + 60 + div.remove() 61 + }) 62 + 63 + it("should throw error when string is empty", () => { 64 + const div = document.createElement("div") 65 + div.innerHTML = "<span>Test</span>" 66 + document.body.appendChild(div) 67 + 68 + // Empty string should throw 69 + expect(() => { 70 + morphInner(div.firstChild!, "") 71 + }).toThrow("[Morphlex] The string was not a valid HTML element.") 72 + 73 + div.remove() 74 + }) 75 + 76 + it("should throw error when string contains whitespace only", () => { 77 + const div = document.createElement("div") 78 + div.innerHTML = "<span>Test</span>" 79 + document.body.appendChild(div) 80 + 81 + // Whitespace-only string should throw 82 + expect(() => { 83 + morphInner(div.firstChild!, " \n\t ") 84 + }).toThrow("[Morphlex] The string was not a valid HTML element.") 85 + 86 + div.remove() 87 + }) 88 + 89 + it("should throw error when morphInner receives string with text and element", () => { 90 + const div = document.createElement("div") 91 + div.innerHTML = "<span>Test</span>" 92 + document.body.appendChild(div) 93 + 94 + // String with text before element 95 + expect(() => { 96 + morphInner(div.firstChild!, "text before <div>element</div>") 97 + }).toThrow("[Morphlex] The string was not a valid HTML element.") 98 + 99 + div.remove() 100 + }) 101 + }) 102 + 103 + describe("morphOneToMany with empty array (lines 116-125)", () => { 104 + it("should remove node when morphing to empty NodeList", () => { 105 + const parent = document.createElement("div") 106 + const child = document.createElement("span") 107 + child.textContent = "Will be removed" 108 + parent.appendChild(child) 109 + document.body.appendChild(parent) 110 + 111 + // Create an empty NodeList by parsing empty content 112 + const template = document.createElement("template") 113 + const emptyNodeList = template.content.childNodes 114 + 115 + // Morph the child to empty NodeList 116 + morph(child, emptyNodeList) 117 + 118 + // Child should be removed from parent 119 + expect(parent.children.length).toBe(0) 120 + expect(parent.contains(child)).toBe(false) 121 + 122 + parent.remove() 123 + }) 124 + 125 + it("should remove node when morphing to empty string parsed as NodeList", () => { 126 + const parent = document.createElement("div") 127 + const element = document.createElement("p") 128 + element.id = "test-element" 129 + element.textContent = "Original" 130 + parent.appendChild(element) 131 + document.body.appendChild(parent) 132 + 133 + // Morph to empty string (gets parsed to empty NodeList) 134 + morph(element, "") 135 + 136 + // Element should be removed 137 + expect(parent.querySelector("#test-element")).toBe(null) 138 + expect(parent.children.length).toBe(0) 139 + 140 + parent.remove() 141 + }) 142 + 143 + it("should call beforeNodeRemoved/afterNodeRemoved when removing via empty NodeList", () => { 144 + const parent = document.createElement("div") 145 + const child = document.createElement("span") 146 + child.id = "to-remove" 147 + parent.appendChild(child) 148 + document.body.appendChild(parent) 149 + 150 + let beforeRemoveCalled = false 151 + let afterRemoveCalled = false 152 + let removedNode: Node | null = null 153 + 154 + // Create empty NodeList 155 + const template = document.createElement("template") 156 + const emptyNodeList = template.content.childNodes 157 + 158 + // Morph with callbacks 159 + morph(child, emptyNodeList, { 160 + beforeNodeRemoved: (node) => { 161 + beforeRemoveCalled = true 162 + removedNode = node 163 + return true 164 + }, 165 + afterNodeRemoved: (node) => { 166 + afterRemoveCalled = true 167 + expect(node).toBe(removedNode) 168 + }, 169 + }) 170 + 171 + expect(beforeRemoveCalled).toBe(true) 172 + expect(afterRemoveCalled).toBe(true) 173 + expect(removedNode).toBe(child) 174 + expect(parent.children.length).toBe(0) 175 + 176 + parent.remove() 177 + }) 178 + 179 + it("should not remove node when beforeNodeRemoved returns false", () => { 180 + const parent = document.createElement("div") 181 + const child = document.createElement("span") 182 + child.textContent = "Should not be removed" 183 + parent.appendChild(child) 184 + document.body.appendChild(parent) 185 + 186 + // Create empty NodeList 187 + const template = document.createElement("template") 188 + const emptyNodeList = template.content.childNodes 189 + 190 + // Morph with beforeNodeRemoved returning false 191 + morph(child, emptyNodeList, { 192 + beforeNodeRemoved: () => false, 193 + }) 194 + 195 + // Child should still be in parent 196 + expect(parent.children.length).toBe(1) 197 + expect(parent.contains(child)).toBe(true) 198 + 199 + parent.remove() 200 + }) 201 + 202 + it("should morph one element to multiple elements from string", () => { 203 + const parent = document.createElement("div") 204 + const single = document.createElement("span") 205 + single.id = "single" 206 + single.textContent = "Single" 207 + parent.appendChild(single) 208 + document.body.appendChild(parent) 209 + 210 + // Morph single element to multiple elements using a string 211 + morph(single, "<span id='first'>First</span><span id='second'>Second</span><span id='third'>Third</span>") 212 + 213 + // Should have morphed the first element and added the rest 214 + expect(parent.children.length).toBe(3) 215 + expect(parent.children[0].id).toBe("first") 216 + expect(parent.children[0].textContent).toBe("First") 217 + expect(parent.children[1].id).toBe("second") 218 + expect(parent.children[2].id).toBe("third") 219 + 220 + parent.remove() 221 + }) 222 + 223 + it("should call callbacks when morphing one to many", () => { 224 + const parent = document.createElement("div") 225 + const single = document.createElement("span") 226 + single.textContent = "Single" 227 + parent.appendChild(single) 228 + document.body.appendChild(parent) 229 + 230 + const addedNodes: Node[] = [] 231 + let morphedCalled = false 232 + 233 + morph(single, "<span>First</span><span>Second</span>", { 234 + beforeNodeAdded: (_node) => { 235 + return true // Allow addition 236 + }, 237 + afterNodeAdded: (node) => { 238 + addedNodes.push(node) 239 + }, 240 + afterNodeMorphed: (_from, _to) => { 241 + morphedCalled = true 242 + // The 'from' could be the original single element or its child nodes after morphing 243 + // Just verify the callback was called 244 + }, 245 + }) 246 + 247 + expect(morphedCalled).toBe(true) 248 + expect(addedNodes.length).toBe(1) // Only second span was added, first was morphed 249 + expect(parent.children.length).toBe(2) 250 + 251 + parent.remove() 252 + }) 253 + 254 + it("should prevent adding nodes when beforeNodeAdded returns false", () => { 255 + const parent = document.createElement("div") 256 + const single = document.createElement("span") 257 + single.id = "original" 258 + single.textContent = "Original" 259 + parent.appendChild(single) 260 + document.body.appendChild(parent) 261 + 262 + morph(single, "<span id='first'>First</span><span id='second'>Second</span>", { 263 + beforeNodeAdded: () => false, // Prevent all additions 264 + }) 265 + 266 + // Only the first element should be morphed, second should not be added 267 + expect(parent.children.length).toBe(1) 268 + expect(parent.children[0].id).toBe("first") // First was morphed 269 + expect(parent.children[0].textContent).toBe("First") 270 + 271 + parent.remove() 272 + }) 273 + 274 + it("should handle morphing to single text node", () => { 275 + const parent = document.createElement("div") 276 + const element = document.createElement("span") 277 + element.textContent = "Element" 278 + parent.appendChild(element) 279 + document.body.appendChild(parent) 280 + 281 + // Morph to just text content (which creates a text node in NodeList) 282 + morph(element, "Just text") 283 + 284 + // Element should be replaced with text node 285 + expect(parent.children.length).toBe(0) // No elements 286 + expect(parent.textContent).toBe("Just text") 287 + 288 + parent.remove() 289 + }) 290 + }) 291 + 292 + describe("moveBefore API usage (line 66)", () => { 293 + it("should use moveBefore when available and node is in same parent", () => { 294 + const parent = document.createElement("div") 295 + const child1 = document.createElement("span") 296 + child1.id = "first" 297 + const child2 = document.createElement("span") 298 + child2.id = "second" 299 + 300 + parent.appendChild(child1) 301 + parent.appendChild(child2) 302 + document.body.appendChild(parent) 303 + 304 + // Mock moveBefore if it doesn't exist, to test the condition 305 + const originalMoveBefore = (parent as any).moveBefore 306 + if (!("moveBefore" in parent)) { 307 + // Add a mock moveBefore to test the branch 308 + ;(parent as any).moveBefore = vi.fn((node: Node, before: Node | null) => { 309 + // Simulate moveBefore behavior 310 + if (node.parentNode === parent) { 311 + parent.insertBefore(node, before) 312 + } 313 + }) 314 + } 315 + 316 + // Morph to reverse order - should trigger moveBefore if available 317 + morph(parent, '<div><span id="second"></span><span id="first"></span></div>') 318 + 319 + // Check order is reversed 320 + expect(parent.children[0].id).toBe("second") 321 + expect(parent.children[1].id).toBe("first") 322 + 323 + // Restore original moveBefore (if it existed) 324 + if (originalMoveBefore === undefined) { 325 + delete (parent as any).moveBefore 326 + } else { 327 + ;(parent as any).moveBefore = originalMoveBefore 328 + } 329 + 330 + parent.remove() 331 + }) 332 + 333 + it("should fall back to insertBefore when moveBefore is not available", () => { 334 + const parent = document.createElement("div") 335 + const child1 = document.createElement("span") 336 + child1.id = "a" 337 + const child2 = document.createElement("span") 338 + child2.id = "b" 339 + 340 + parent.appendChild(child1) 341 + parent.appendChild(child2) 342 + document.body.appendChild(parent) 343 + 344 + // Ensure moveBefore is not available 345 + const originalMoveBefore = (parent as any).moveBefore 346 + if ("moveBefore" in parent) { 347 + delete (parent as any).moveBefore 348 + } 349 + 350 + // Morph to reverse order - should use insertBefore fallback 351 + morph(parent, '<div><span id="b"></span><span id="a"></span></div>') 352 + 353 + // Check order is reversed 354 + expect(parent.children[0].id).toBe("b") 355 + expect(parent.children[1].id).toBe("a") 356 + 357 + // Restore original moveBefore if it existed 358 + if (originalMoveBefore !== undefined) { 359 + ;(parent as any).moveBefore = originalMoveBefore 360 + } 361 + 362 + parent.remove() 363 + }) 364 + 365 + it("should use insertBefore when node is not already in the same parent", () => { 366 + const parent1 = document.createElement("div") 367 + const parent2 = document.createElement("div") 368 + const child = document.createElement("span") 369 + child.id = "movable" 370 + child.textContent = "Move me" 371 + 372 + parent2.appendChild(child) 373 + document.body.appendChild(parent1) 374 + document.body.appendChild(parent2) 375 + 376 + // Add mock moveBefore to parent1 377 + let moveBeforeCalled = false 378 + const originalMoveBefore = (parent1 as any).moveBefore 379 + if (!("moveBefore" in parent1)) { 380 + ;(parent1 as any).moveBefore = vi.fn(() => { 381 + moveBeforeCalled = true 382 + }) 383 + } 384 + 385 + // Create a reference element in parent1 386 + const reference = document.createElement("span") 387 + reference.id = "ref" 388 + parent1.appendChild(reference) 389 + 390 + // Morph parent1 to include the child from parent2 391 + // The child with id="movable" will be found in parent2 and moved to parent1 392 + morph(parent1, '<div><span id="movable">Move me</span><span id="ref"></span></div>') 393 + 394 + // moveBefore should NOT be called since node was in different parent 395 + expect(moveBeforeCalled).toBe(false) 396 + 397 + // Child should now be in parent1 398 + expect(parent1.querySelector("#movable")).toBeTruthy() 399 + 400 + // Restore original moveBefore 401 + if (originalMoveBefore === undefined) { 402 + delete (parent1 as any).moveBefore 403 + } else { 404 + ;(parent1 as any).moveBefore = originalMoveBefore 405 + } 406 + 407 + parent1.remove() 408 + parent2.remove() 409 + }) 410 + }) 411 + })