Precise DOM morphing
morphing typescript dom
at main 411 lines 13 kB view raw
1import { describe, it, expect, vi } from "vitest" 2import { morph, morphInner } from "../src/morphlex" 3 4describe("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 afterNodeVisited: (_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})