@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.
- package/bun.lock +4 -4
- package/package.json +1 -1
- package/src/dom-simulator.ts +396 -51
- package/src/dom-types.ts +3 -0
- package/tests/cloneNode-bug-reproduction.test.ts +325 -0
- package/tests/cloneNode-interactive.ts +235 -0
- package/tests/cloneNode.test.ts +587 -0
- package/tests/dom-adoption.test.ts +363 -0
- package/tests/dom-manipulation.test.ts +688 -0
- package/tests/dom-synchronization.test.ts +675 -0
|
@@ -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 & < > 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
|
+
});
|