@tkeron/html-parser 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,587 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseHTML } from "../index.js";
3
+ import { NodeType } from "../src/dom-simulator.js";
4
+
5
+ describe("cloneNode functionality", () => {
6
+ describe("cloneNode(true) - deep cloning", () => {
7
+ it("should clone a simple element with text content", () => {
8
+ const html = `<div id="original">Hello World</div>`;
9
+ const doc = parseHTML(html);
10
+ const original = doc.querySelector("#original")!;
11
+
12
+ const cloned = original.cloneNode(true);
13
+
14
+ // Verificar que el nodo clonado existe
15
+ expect(cloned).toBeTruthy();
16
+ expect(cloned.nodeName).toBe("DIV");
17
+
18
+ // Verificar que los atributos se copian
19
+ expect(cloned.getAttribute("id")).toBe("original");
20
+
21
+ // Verificar que el contenido de texto se copia
22
+ expect(cloned.textContent).toBe("Hello World");
23
+
24
+ // Verificar que tiene los hijos correctos
25
+ expect(cloned.childNodes.length).toBeGreaterThan(0);
26
+ });
27
+
28
+ it("should clone nested elements with multiple levels", () => {
29
+ const html = `
30
+ <div id="parent">
31
+ <div class="child">
32
+ <span>Nested Text</span>
33
+ </div>
34
+ </div>
35
+ `;
36
+ const doc = parseHTML(html);
37
+ const parent = doc.querySelector("#parent")!;
38
+
39
+ const cloned = parent.cloneNode(true);
40
+
41
+ // Verificar estructura básica
42
+ expect(cloned.nodeName).toBe("DIV");
43
+ expect(cloned.getAttribute("id")).toBe("parent");
44
+
45
+ // Verificar que los hijos se clonaron
46
+ expect(cloned.childNodes.length).toBeGreaterThan(0);
47
+
48
+ // Verificar que el hijo div está presente
49
+ const childDiv = cloned.querySelector(".child");
50
+ expect(childDiv).toBeTruthy();
51
+ expect(childDiv?.nodeName).toBe("DIV");
52
+ expect(childDiv?.getAttribute("class")).toBe("child");
53
+
54
+ // Verificar el nieto span
55
+ const span = cloned.querySelector("span");
56
+ expect(span).toBeTruthy();
57
+ expect(span?.textContent).toBe("Nested Text");
58
+ });
59
+
60
+ it("should clone element with multiple children", () => {
61
+ const html = `
62
+ <ul id="list">
63
+ <li>Item 1</li>
64
+ <li>Item 2</li>
65
+ <li>Item 3</li>
66
+ </ul>
67
+ `;
68
+ const doc = parseHTML(html);
69
+ const list = doc.querySelector("#list")!;
70
+
71
+ const cloned = list.cloneNode(true);
72
+
73
+ // Verificar que todos los items se clonaron
74
+ const items = cloned.querySelectorAll("li");
75
+ expect(items.length).toBe(3);
76
+ expect(items[0]?.textContent).toBe("Item 1");
77
+ expect(items[1]?.textContent).toBe("Item 2");
78
+ expect(items[2]?.textContent).toBe("Item 3");
79
+ });
80
+
81
+ it("should preserve innerHTML after cloning", () => {
82
+ const html = `
83
+ <div id="container">
84
+ <h1>Title</h1>
85
+ <p>Paragraph 1</p>
86
+ <p>Paragraph 2</p>
87
+ </div>
88
+ `;
89
+ const doc = parseHTML(html);
90
+ const container = doc.querySelector("#container")!;
91
+
92
+ // Guardar innerHTML original
93
+ const originalInnerHTML = container.innerHTML;
94
+ expect(originalInnerHTML).toBeTruthy();
95
+ expect(originalInnerHTML.length).toBeGreaterThan(0);
96
+
97
+ const cloned = container.cloneNode(true);
98
+
99
+ // Verificar que innerHTML del clon no esté vacío
100
+ expect(cloned.innerHTML).toBeTruthy();
101
+ expect(cloned.innerHTML.length).toBeGreaterThan(0);
102
+
103
+ // Verificar que el contenido es similar
104
+ expect(cloned.innerHTML).toContain("<h1>Title</h1>");
105
+ expect(cloned.innerHTML).toContain("<p>Paragraph 1</p>");
106
+ expect(cloned.innerHTML).toContain("<p>Paragraph 2</p>");
107
+ });
108
+
109
+ it("should clone element with mixed content (elements and text nodes)", () => {
110
+ const html = `<div id="mixed">Text before<strong>bold text</strong>Text after</div>`;
111
+ const doc = parseHTML(html);
112
+ const mixed = doc.querySelector("#mixed")!;
113
+
114
+ // Verificar estructura original
115
+ const originalChildCount = mixed.childNodes.length;
116
+ expect(originalChildCount).toBeGreaterThan(0);
117
+
118
+ const cloned = mixed.cloneNode(true);
119
+
120
+ // Verificar que tiene la misma cantidad de hijos
121
+ expect(cloned.childNodes.length).toBe(originalChildCount);
122
+
123
+ // Verificar contenido completo
124
+ expect(cloned.textContent).toBe("Text beforebold textText after");
125
+
126
+ // Verificar que el elemento strong existe
127
+ const strong = cloned.querySelector("strong");
128
+ expect(strong).toBeTruthy();
129
+ expect(strong?.textContent).toBe("bold text");
130
+ });
131
+
132
+ it("should clone all attributes including custom ones", () => {
133
+ const html = `<div id="attrs" class="test" data-value="123" data-custom="abc">Content</div>`;
134
+ const doc = parseHTML(html);
135
+ const element = doc.querySelector("#attrs")!;
136
+
137
+ const cloned = element.cloneNode(true);
138
+
139
+ // Verificar todos los atributos
140
+ expect(cloned.getAttribute("id")).toBe("attrs");
141
+ expect(cloned.getAttribute("class")).toBe("test");
142
+ expect(cloned.getAttribute("data-value")).toBe("123");
143
+ expect(cloned.getAttribute("data-custom")).toBe("abc");
144
+ expect(cloned.textContent).toBe("Content");
145
+ });
146
+
147
+ it("should clone complex structure with different node types", () => {
148
+ const html = `
149
+ <article id="article">
150
+ <h2>Article Title</h2>
151
+ <!-- This is a comment -->
152
+ <p>First paragraph</p>
153
+ <div class="highlight">
154
+ <span>Highlighted</span> text
155
+ </div>
156
+ <p>Last paragraph</p>
157
+ </article>
158
+ `;
159
+ const doc = parseHTML(html);
160
+ const article = doc.querySelector("#article")!;
161
+
162
+ const cloned = article.cloneNode(true);
163
+
164
+ // Verificar estructura
165
+ expect(cloned.nodeName).toBe("ARTICLE");
166
+ expect(cloned.getAttribute("id")).toBe("article");
167
+
168
+ // Verificar elementos
169
+ expect(cloned.querySelector("h2")?.textContent).toBe("Article Title");
170
+
171
+ const paragraphs = cloned.querySelectorAll("p");
172
+ expect(paragraphs.length).toBe(2);
173
+ expect(paragraphs[0]?.textContent).toBe("First paragraph");
174
+ expect(paragraphs[1]?.textContent).toBe("Last paragraph");
175
+
176
+ // Verificar div anidado
177
+ const highlight = cloned.querySelector(".highlight");
178
+ expect(highlight).toBeTruthy();
179
+ expect(highlight?.querySelector("span")?.textContent).toBe("Highlighted");
180
+
181
+ // Verificar que se copió el comentario
182
+ const hasComment = Array.from(cloned.childNodes).some(
183
+ (node: any) => node.nodeType === NodeType.COMMENT_NODE
184
+ );
185
+ expect(hasComment).toBe(true);
186
+ });
187
+
188
+ it("should clone element with empty children", () => {
189
+ const html = `<div id="container"><p></p><span></span></div>`;
190
+ const doc = parseHTML(html);
191
+ const container = doc.querySelector("#container")!;
192
+
193
+ const cloned = container.cloneNode(true);
194
+
195
+ expect(cloned.querySelector("p")).toBeTruthy();
196
+ expect(cloned.querySelector("span")).toBeTruthy();
197
+ });
198
+
199
+ it("should maintain outerHTML structure in cloned node", () => {
200
+ const html = `<section class="main"><h1>Title</h1><p>Text</p></section>`;
201
+ const doc = parseHTML(html);
202
+ const section = doc.querySelector("section")!;
203
+
204
+ const cloned = section.cloneNode(true);
205
+
206
+ // Verificar outerHTML
207
+ expect(cloned.outerHTML).toBeTruthy();
208
+ expect(cloned.outerHTML).toContain("section");
209
+ expect(cloned.outerHTML).toContain("class=\"main\"");
210
+ expect(cloned.outerHTML).toContain("<h1>Title</h1>");
211
+ expect(cloned.outerHTML).toContain("<p>Text</p>");
212
+ });
213
+ });
214
+
215
+ describe("cloneNode(false) - shallow cloning", () => {
216
+ it("should clone element without children when deep is false", () => {
217
+ const html = `<div id="parent"><p>Child</p></div>`;
218
+ const doc = parseHTML(html);
219
+ const parent = doc.querySelector("#parent")!;
220
+
221
+ const cloned = parent.cloneNode(false);
222
+
223
+ // Debe copiar el elemento pero no los hijos
224
+ expect(cloned.nodeName).toBe("DIV");
225
+ expect(cloned.getAttribute("id")).toBe("parent");
226
+ expect(cloned.childNodes.length).toBe(0);
227
+ expect(cloned.querySelector("p")).toBeNull();
228
+ });
229
+
230
+ it("should preserve attributes in shallow clone", () => {
231
+ const html = `<div id="test" class="container" data-value="123"><span>Content</span></div>`;
232
+ const doc = parseHTML(html);
233
+ const element = doc.querySelector("#test")!;
234
+
235
+ const cloned = element.cloneNode(false);
236
+
237
+ // Atributos deben copiarse
238
+ expect(cloned.getAttribute("id")).toBe("test");
239
+ expect(cloned.getAttribute("class")).toBe("container");
240
+ expect(cloned.getAttribute("data-value")).toBe("123");
241
+
242
+ // Hijos NO deben copiarse
243
+ expect(cloned.childNodes.length).toBe(0);
244
+ expect(cloned.innerHTML).toBe("");
245
+ });
246
+ });
247
+
248
+ describe("cloneNode independence", () => {
249
+ it("cloned node should be independent from original", () => {
250
+ const html = `<div id="original">Original</div>`;
251
+ const doc = parseHTML(html);
252
+ const original = doc.querySelector("#original")!;
253
+
254
+ const cloned = original.cloneNode(true);
255
+
256
+ // Modificar el clon no debe afectar el original
257
+ cloned.setAttribute("id", "cloned");
258
+ cloned.setAttribute("data-modified", "true");
259
+
260
+ expect(original.getAttribute("id")).toBe("original");
261
+ expect(original.hasAttribute("data-modified")).toBe(false);
262
+ expect(cloned.getAttribute("id")).toBe("cloned");
263
+ expect(cloned.getAttribute("data-modified")).toBe("true");
264
+ });
265
+
266
+ it("modifying cloned children should not affect original", () => {
267
+ const html = `<div id="parent"><p id="child">Text</p></div>`;
268
+ const doc = parseHTML(html);
269
+ const parent = doc.querySelector("#parent")!;
270
+
271
+ const cloned = parent.cloneNode(true);
272
+ const clonedChild = cloned.querySelector("#child");
273
+
274
+ expect(clonedChild).toBeTruthy();
275
+
276
+ // Modificar hijo del clon
277
+ clonedChild?.setAttribute("data-cloned", "yes");
278
+
279
+ // El hijo original no debe verse afectado
280
+ const originalChild = parent.querySelector("#child");
281
+ expect(originalChild?.hasAttribute("data-cloned")).toBe(false);
282
+ });
283
+ });
284
+
285
+ describe("cloneNode edge cases", () => {
286
+ it("should handle cloning of self-closing tags", () => {
287
+ const html = `<div><img src="test.jpg" alt="Test" /><br /></div>`;
288
+ const doc = parseHTML(html);
289
+ const div = doc.querySelector("div")!;
290
+
291
+ const cloned = div.cloneNode(true);
292
+
293
+ const img = cloned.querySelector("img");
294
+ expect(img).toBeTruthy();
295
+ expect(img?.getAttribute("src")).toBe("test.jpg");
296
+ expect(img?.getAttribute("alt")).toBe("Test");
297
+ });
298
+
299
+ it("should clone elements with special characters in content", () => {
300
+ const html = `<div id="special">Text with &amp; &lt; &gt; entities</div>`;
301
+ const doc = parseHTML(html);
302
+ const element = doc.querySelector("#special")!;
303
+
304
+ const cloned = element.cloneNode(true);
305
+
306
+ expect(cloned.textContent).toBeTruthy();
307
+ expect(cloned.textContent.length).toBeGreaterThan(0);
308
+ });
309
+
310
+ it("should handle deeply nested structures", () => {
311
+ const html = `
312
+ <div id="level1">
313
+ <div id="level2">
314
+ <div id="level3">
315
+ <div id="level4">
316
+ <div id="level5">Deep Content</div>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ `;
322
+ const doc = parseHTML(html);
323
+ const level1 = doc.querySelector("#level1")!;
324
+
325
+ const cloned = level1.cloneNode(true);
326
+
327
+ // Verificar todos los niveles
328
+ expect(cloned.querySelector("#level2")).toBeTruthy();
329
+ expect(cloned.querySelector("#level3")).toBeTruthy();
330
+ expect(cloned.querySelector("#level4")).toBeTruthy();
331
+ const level5 = cloned.querySelector("#level5");
332
+ expect(level5).toBeTruthy();
333
+ expect(level5?.textContent).toBe("Deep Content");
334
+ });
335
+ });
336
+
337
+ describe("cloneNode internal properties", () => {
338
+ it("should verify _internalInnerHTML is properly set in clone", () => {
339
+ const html = `<div id="container"><p>Paragraph 1</p><p>Paragraph 2</p></div>`;
340
+ const doc = parseHTML(html);
341
+ const container = doc.querySelector("#container")!;
342
+
343
+ // Acceder a innerHTML para asegurar que _internalInnerHTML esté establecido
344
+ const originalInnerHTML = container.innerHTML;
345
+ expect(originalInnerHTML).toBeTruthy();
346
+
347
+ const cloned = container.cloneNode(true);
348
+
349
+ // Verificar que innerHTML funciona en el clon
350
+ const clonedInnerHTML = cloned.innerHTML;
351
+ expect(clonedInnerHTML).toBeTruthy();
352
+ expect(clonedInnerHTML.length).toBeGreaterThan(0);
353
+
354
+ // Verificar que contiene el mismo contenido
355
+ expect(clonedInnerHTML).toContain("<p>Paragraph 1</p>");
356
+ expect(clonedInnerHTML).toContain("<p>Paragraph 2</p>");
357
+
358
+ // Verificar que el accessor de innerHTML funciona correctamente
359
+ expect(typeof cloned.innerHTML).toBe("string");
360
+ });
361
+
362
+ it("should maintain proper childNodes structure after clone", () => {
363
+ const html = `<div id="parent">Text<span>Span</span>More text</div>`;
364
+ const doc = parseHTML(html);
365
+ const parent = doc.querySelector("#parent")!;
366
+
367
+ const originalChildCount = parent.childNodes.length;
368
+ expect(originalChildCount).toBeGreaterThan(0);
369
+
370
+ const cloned = parent.cloneNode(true);
371
+
372
+ // Verificar que childNodes tiene la misma estructura
373
+ expect(cloned.childNodes.length).toBe(originalChildCount);
374
+
375
+ // Verificar que podemos acceder a cada hijo
376
+ for (let i = 0; i < cloned.childNodes.length; i++) {
377
+ expect(cloned.childNodes[i]).toBeTruthy();
378
+ expect(cloned.childNodes[i].nodeType).toBeDefined();
379
+ }
380
+ });
381
+
382
+ it("should properly initialize children array in cloned element", () => {
383
+ const html = `<div id="container"><span>1</span><span>2</span><span>3</span></div>`;
384
+ const doc = parseHTML(html);
385
+ const container = doc.querySelector("#container")!;
386
+
387
+ const cloned = container.cloneNode(true);
388
+
389
+ // Verificar que el array children está correctamente poblado
390
+ expect(cloned.children).toBeTruthy();
391
+ expect(Array.isArray(cloned.children)).toBe(true);
392
+ expect(cloned.children.length).toBe(3);
393
+
394
+ // Verificar que todos son elementos
395
+ for (const child of cloned.children) {
396
+ expect(child.nodeType).toBe(NodeType.ELEMENT_NODE);
397
+ }
398
+ });
399
+
400
+ it("should clone and maintain firstChild and lastChild references", () => {
401
+ const html = `<ul id="list"><li>First</li><li>Middle</li><li>Last</li></ul>`;
402
+ const doc = parseHTML(html);
403
+ const list = doc.querySelector("#list")!;
404
+
405
+ const cloned = list.cloneNode(true);
406
+
407
+ // Verificar referencias firstChild y lastChild
408
+ expect(cloned.firstChild).toBeTruthy();
409
+ expect(cloned.lastChild).toBeTruthy();
410
+
411
+ // En DOM real, firstChild puede ser un nodo de texto (whitespace)
412
+ // pero debemos asegurar que existen
413
+ expect(cloned.firstElementChild).toBeTruthy();
414
+ expect(cloned.lastElementChild).toBeTruthy();
415
+
416
+ const firstLi = cloned.firstElementChild;
417
+ const lastLi = cloned.lastElementChild;
418
+
419
+ expect(firstLi?.textContent).toContain("First");
420
+ expect(lastLi?.textContent).toContain("Last");
421
+ });
422
+ });
423
+
424
+ describe("cloneNode with innerHTML manipulation", () => {
425
+ it("should clone element after innerHTML was modified", () => {
426
+ const html = `<div id="dynamic"></div>`;
427
+ const doc = parseHTML(html);
428
+ const dynamic = doc.querySelector("#dynamic")!;
429
+
430
+ // Modificar innerHTML antes de clonar
431
+ dynamic.innerHTML = "<p>Dynamic content</p><span>More content</span>";
432
+
433
+ const cloned = dynamic.cloneNode(true);
434
+
435
+ // Verificar que el contenido modificado se clonó
436
+ expect(cloned.querySelector("p")).toBeTruthy();
437
+ expect(cloned.querySelector("p")?.textContent).toBe("Dynamic content");
438
+ expect(cloned.querySelector("span")).toBeTruthy();
439
+ expect(cloned.querySelector("span")?.textContent).toBe("More content");
440
+ });
441
+
442
+ it("should clone element and allow innerHTML manipulation on clone", () => {
443
+ const html = `<div id="original"><p>Original</p></div>`;
444
+ const doc = parseHTML(html);
445
+ const original = doc.querySelector("#original")!;
446
+
447
+ const cloned = original.cloneNode(true);
448
+
449
+ // Verificar contenido clonado
450
+ expect(cloned.querySelector("p")?.textContent).toBe("Original");
451
+
452
+ // Modificar innerHTML del clon
453
+ cloned.innerHTML = "<span>Modified</span>";
454
+
455
+ // Original no debe cambiar
456
+ expect(original.querySelector("p")?.textContent).toBe("Original");
457
+ expect(original.querySelector("span")).toBeNull();
458
+
459
+ // Clon debe tener el nuevo contenido
460
+ expect(cloned.querySelector("span")?.textContent).toBe("Modified");
461
+ expect(cloned.querySelector("p")).toBeNull();
462
+ });
463
+ });
464
+
465
+ describe("cloneNode real-world scenarios", () => {
466
+ it("should clone a complete card component", () => {
467
+ const html = `
468
+ <div class="card" data-id="123">
469
+ <div class="card-header">
470
+ <h3 class="card-title">Card Title</h3>
471
+ <button class="close">×</button>
472
+ </div>
473
+ <div class="card-body">
474
+ <p>This is the card content with <strong>bold</strong> text.</p>
475
+ <ul>
476
+ <li>Item 1</li>
477
+ <li>Item 2</li>
478
+ </ul>
479
+ </div>
480
+ <div class="card-footer">
481
+ <button class="btn-primary">Save</button>
482
+ <button class="btn-secondary">Cancel</button>
483
+ </div>
484
+ </div>
485
+ `;
486
+ const doc = parseHTML(html);
487
+ const card = doc.querySelector(".card")!;
488
+
489
+ const cloned = card.cloneNode(true);
490
+
491
+ // Verificar estructura completa
492
+ expect(cloned.getAttribute("data-id")).toBe("123");
493
+ expect(cloned.querySelector(".card-header")).toBeTruthy();
494
+ expect(cloned.querySelector(".card-body")).toBeTruthy();
495
+ expect(cloned.querySelector(".card-footer")).toBeTruthy();
496
+
497
+ // Verificar contenido específico
498
+ expect(cloned.querySelector(".card-title")?.textContent).toBe("Card Title");
499
+ expect(cloned.querySelector("strong")?.textContent).toBe("bold");
500
+
501
+ const items = cloned.querySelectorAll("li");
502
+ expect(items.length).toBe(2);
503
+
504
+ const buttons = cloned.querySelectorAll("button");
505
+ expect(buttons.length).toBe(3); // close, save, cancel
506
+ });
507
+
508
+ it("should clone a form with various input types", () => {
509
+ const html = `
510
+ <form id="user-form">
511
+ <input type="text" name="username" value="john" />
512
+ <input type="email" name="email" value="john@example.com" />
513
+ <textarea name="bio">User bio</textarea>
514
+ <select name="country">
515
+ <option value="us">USA</option>
516
+ <option value="uk" selected>UK</option>
517
+ </select>
518
+ </form>
519
+ `;
520
+ const doc = parseHTML(html);
521
+ const form = doc.querySelector("#user-form")!;
522
+
523
+ const cloned = form.cloneNode(true);
524
+
525
+ // Verificar que todos los inputs se clonaron
526
+ const textInput = cloned.querySelector('[name="username"]');
527
+ expect(textInput).toBeTruthy();
528
+ expect(textInput?.getAttribute("value")).toBe("john");
529
+
530
+ const emailInput = cloned.querySelector('[name="email"]');
531
+ expect(emailInput).toBeTruthy();
532
+ expect(emailInput?.getAttribute("value")).toBe("john@example.com");
533
+
534
+ const textarea = cloned.querySelector("textarea");
535
+ expect(textarea).toBeTruthy();
536
+ expect(textarea?.textContent).toBe("User bio");
537
+
538
+ const select = cloned.querySelector("select");
539
+ expect(select).toBeTruthy();
540
+ const options = select?.querySelectorAll("option");
541
+ expect(options?.length).toBe(2);
542
+ });
543
+
544
+ it("should clone a table structure", () => {
545
+ const html = `
546
+ <table id="data-table">
547
+ <thead>
548
+ <tr>
549
+ <th>Name</th>
550
+ <th>Age</th>
551
+ </tr>
552
+ </thead>
553
+ <tbody>
554
+ <tr>
555
+ <td>John</td>
556
+ <td>30</td>
557
+ </tr>
558
+ <tr>
559
+ <td>Jane</td>
560
+ <td>25</td>
561
+ </tr>
562
+ </tbody>
563
+ </table>
564
+ `;
565
+ const doc = parseHTML(html);
566
+ const table = doc.querySelector("#data-table")!;
567
+
568
+ const cloned = table.cloneNode(true);
569
+
570
+ expect(cloned.querySelector("thead")).toBeTruthy();
571
+ expect(cloned.querySelector("tbody")).toBeTruthy();
572
+
573
+ const headers = cloned.querySelectorAll("th");
574
+ expect(headers.length).toBe(2);
575
+ expect(headers[0]?.textContent).toBe("Name");
576
+ expect(headers[1]?.textContent).toBe("Age");
577
+
578
+ const rows = cloned.querySelectorAll("tbody tr");
579
+ expect(rows.length).toBe(2);
580
+
581
+ const firstRowCells = rows[0]?.querySelectorAll("td");
582
+ expect(firstRowCells?.length).toBe(2);
583
+ expect(firstRowCells?.[0]?.textContent).toBe("John");
584
+ expect(firstRowCells?.[1]?.textContent).toBe("30");
585
+ });
586
+ });
587
+ });