@tkeron/html-parser 0.1.3 → 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/tests/cloneNode-bug-reproduction.test.ts +325 -0
- package/tests/cloneNode-interactive.ts +235 -0
- package/tests/cloneNode.test.ts +587 -0
package/bun.lock
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"lockfileVersion": 1,
|
|
3
|
-
"configVersion":
|
|
3
|
+
"configVersion": 1,
|
|
4
4
|
"workspaces": {
|
|
5
5
|
"": {
|
|
6
|
-
"name": "html-parser",
|
|
6
|
+
"name": "@tkeron/html-parser",
|
|
7
7
|
"devDependencies": {
|
|
8
8
|
"@types/bun": "^1.3.4",
|
|
9
9
|
},
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"packages": {
|
|
16
16
|
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
|
17
17
|
|
|
18
|
-
"@types/node": ["@types/node@
|
|
18
|
+
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
19
19
|
|
|
20
20
|
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
|
21
21
|
|
|
22
22
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
23
23
|
|
|
24
|
-
"undici-types": ["undici-types@7.
|
|
24
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
25
25
|
}
|
|
26
26
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseHTML } from "../index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests específicos para reproducir el problema reportado:
|
|
6
|
+
* "Cuando se clona un nodo con cloneNode(true), el nodo clonado
|
|
7
|
+
* pierde su contenido interno (_internalInnerHTML y estructura de hijos)."
|
|
8
|
+
*
|
|
9
|
+
* Estos tests intentan reproducir el problema exacto descrito.
|
|
10
|
+
*/
|
|
11
|
+
describe("cloneNode - Bug Reproduction Tests", () => {
|
|
12
|
+
|
|
13
|
+
it("REPRODUCCIÓN: debe copiar _internalInnerHTML correctamente", () => {
|
|
14
|
+
const html = `
|
|
15
|
+
<div id="container">
|
|
16
|
+
<h1>Título Principal</h1>
|
|
17
|
+
<p>Este es un párrafo con <strong>texto en negrita</strong>.</p>
|
|
18
|
+
<ul>
|
|
19
|
+
<li>Item 1</li>
|
|
20
|
+
<li>Item 2</li>
|
|
21
|
+
<li>Item 3</li>
|
|
22
|
+
</ul>
|
|
23
|
+
</div>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const doc = parseHTML(html);
|
|
27
|
+
const container = doc.querySelector("#container")!;
|
|
28
|
+
|
|
29
|
+
// Verificar estado ANTES del clonado
|
|
30
|
+
console.log("=== ANTES DEL CLONADO ===");
|
|
31
|
+
console.log("Original innerHTML:", container.innerHTML);
|
|
32
|
+
console.log("Original _internalInnerHTML:", (container as any)._internalInnerHTML);
|
|
33
|
+
console.log("Original childNodes.length:", container.childNodes.length);
|
|
34
|
+
console.log("Original children.length:", container.children.length);
|
|
35
|
+
|
|
36
|
+
// Realizar el clonado
|
|
37
|
+
const cloned = container.cloneNode(true);
|
|
38
|
+
|
|
39
|
+
// Verificar estado DESPUÉS del clonado
|
|
40
|
+
console.log("\n=== DESPUÉS DEL CLONADO ===");
|
|
41
|
+
console.log("Clonado innerHTML:", cloned.innerHTML);
|
|
42
|
+
console.log("Clonado _internalInnerHTML:", (cloned as any)._internalInnerHTML);
|
|
43
|
+
console.log("Clonado childNodes.length:", cloned.childNodes.length);
|
|
44
|
+
console.log("Clonado children.length:", cloned.children.length);
|
|
45
|
+
|
|
46
|
+
// VERIFICACIONES CRÍTICAS
|
|
47
|
+
|
|
48
|
+
// 1. innerHTML no debe estar vacío
|
|
49
|
+
expect(cloned.innerHTML).toBeTruthy();
|
|
50
|
+
expect(cloned.innerHTML.length).toBeGreaterThan(0);
|
|
51
|
+
|
|
52
|
+
// 2. Debe contener los elementos esperados
|
|
53
|
+
expect(cloned.innerHTML).toContain("Título Principal");
|
|
54
|
+
expect(cloned.innerHTML).toContain("<strong>texto en negrita</strong>");
|
|
55
|
+
expect(cloned.innerHTML).toContain("<li>Item 1</li>");
|
|
56
|
+
|
|
57
|
+
// 3. childNodes debe tener contenido
|
|
58
|
+
expect(cloned.childNodes.length).toBeGreaterThan(0);
|
|
59
|
+
expect(cloned.childNodes.length).toBe(container.childNodes.length);
|
|
60
|
+
|
|
61
|
+
// 4. children debe tener los elementos
|
|
62
|
+
expect(cloned.children.length).toBe(3); // h1, p, ul
|
|
63
|
+
expect(cloned.children.length).toBe(container.children.length);
|
|
64
|
+
|
|
65
|
+
// 5. Los elementos deben ser accesibles mediante querySelector
|
|
66
|
+
expect(cloned.querySelector("h1")).toBeTruthy();
|
|
67
|
+
expect(cloned.querySelector("p")).toBeTruthy();
|
|
68
|
+
expect(cloned.querySelector("ul")).toBeTruthy();
|
|
69
|
+
expect(cloned.querySelectorAll("li").length).toBe(3);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("REPRODUCCIÓN: debe mantener la estructura completa de hijos", () => {
|
|
73
|
+
const html = `
|
|
74
|
+
<div id="parent">
|
|
75
|
+
<div class="level-1">
|
|
76
|
+
<div class="level-2">
|
|
77
|
+
<div class="level-3">
|
|
78
|
+
Contenido profundo
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const doc = parseHTML(html);
|
|
86
|
+
const parent = doc.querySelector("#parent")!;
|
|
87
|
+
|
|
88
|
+
console.log("\n=== ESTRUCTURA DE HIJOS ANTES ===");
|
|
89
|
+
console.log("Parent childNodes:", parent.childNodes.length);
|
|
90
|
+
console.log("Level-1 existe:", !!parent.querySelector(".level-1"));
|
|
91
|
+
console.log("Level-2 existe:", !!parent.querySelector(".level-2"));
|
|
92
|
+
console.log("Level-3 existe:", !!parent.querySelector(".level-3"));
|
|
93
|
+
|
|
94
|
+
const cloned = parent.cloneNode(true);
|
|
95
|
+
|
|
96
|
+
console.log("\n=== ESTRUCTURA DE HIJOS DESPUÉS ===");
|
|
97
|
+
console.log("Clonado childNodes:", cloned.childNodes.length);
|
|
98
|
+
console.log("Level-1 existe:", !!cloned.querySelector(".level-1"));
|
|
99
|
+
console.log("Level-2 existe:", !!cloned.querySelector(".level-2"));
|
|
100
|
+
console.log("Level-3 existe:", !!cloned.querySelector(".level-3"));
|
|
101
|
+
|
|
102
|
+
// VERIFICAR que la estructura se mantiene
|
|
103
|
+
expect(cloned.childNodes.length).toBe(parent.childNodes.length);
|
|
104
|
+
expect(cloned.querySelector(".level-1")).toBeTruthy();
|
|
105
|
+
expect(cloned.querySelector(".level-2")).toBeTruthy();
|
|
106
|
+
expect(cloned.querySelector(".level-3")).toBeTruthy();
|
|
107
|
+
expect(cloned.querySelector(".level-3")?.textContent).toContain("Contenido profundo");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("REPRODUCCIÓN: debe copiar atributos Y contenido simultáneamente", () => {
|
|
111
|
+
const html = `
|
|
112
|
+
<div
|
|
113
|
+
id="complex"
|
|
114
|
+
class="container main"
|
|
115
|
+
data-id="123"
|
|
116
|
+
data-type="test"
|
|
117
|
+
aria-label="Complex element"
|
|
118
|
+
>
|
|
119
|
+
<header>
|
|
120
|
+
<h1>Header Title</h1>
|
|
121
|
+
</header>
|
|
122
|
+
<main>
|
|
123
|
+
<p>Main content</p>
|
|
124
|
+
</main>
|
|
125
|
+
<footer>
|
|
126
|
+
<p>Footer content</p>
|
|
127
|
+
</footer>
|
|
128
|
+
</div>
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
const doc = parseHTML(html);
|
|
132
|
+
const complex = doc.querySelector("#complex")!;
|
|
133
|
+
|
|
134
|
+
console.log("\n=== ANTES: ATRIBUTOS Y CONTENIDO ===");
|
|
135
|
+
console.log("Atributos:", JSON.stringify(complex.attributes, null, 2));
|
|
136
|
+
console.log("innerHTML length:", complex.innerHTML.length);
|
|
137
|
+
console.log("Hijos:", complex.children.length);
|
|
138
|
+
|
|
139
|
+
const cloned = complex.cloneNode(true);
|
|
140
|
+
|
|
141
|
+
console.log("\n=== DESPUÉS: ATRIBUTOS Y CONTENIDO ===");
|
|
142
|
+
console.log("Atributos clonados:", JSON.stringify(cloned.attributes, null, 2));
|
|
143
|
+
console.log("innerHTML length clonado:", cloned.innerHTML.length);
|
|
144
|
+
console.log("Hijos clonados:", cloned.children.length);
|
|
145
|
+
|
|
146
|
+
// VERIFICAR atributos
|
|
147
|
+
expect(cloned.getAttribute("id")).toBe("complex");
|
|
148
|
+
expect(cloned.getAttribute("class")).toBe("container main");
|
|
149
|
+
expect(cloned.getAttribute("data-id")).toBe("123");
|
|
150
|
+
expect(cloned.getAttribute("data-type")).toBe("test");
|
|
151
|
+
expect(cloned.getAttribute("aria-label")).toBe("Complex element");
|
|
152
|
+
|
|
153
|
+
// VERIFICAR contenido
|
|
154
|
+
expect(cloned.innerHTML.length).toBeGreaterThan(0);
|
|
155
|
+
expect(cloned.children.length).toBe(3); // header, main, footer
|
|
156
|
+
expect(cloned.querySelector("header h1")?.textContent).toBe("Header Title");
|
|
157
|
+
expect(cloned.querySelector("main p")?.textContent).toBe("Main content");
|
|
158
|
+
expect(cloned.querySelector("footer p")?.textContent).toBe("Footer content");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("REPRODUCCIÓN: comparar original vs clonado lado a lado", () => {
|
|
162
|
+
const html = `
|
|
163
|
+
<article id="article">
|
|
164
|
+
<h1>Artículo de Prueba</h1>
|
|
165
|
+
<p class="intro">Introducción del artículo.</p>
|
|
166
|
+
<section>
|
|
167
|
+
<h2>Sección 1</h2>
|
|
168
|
+
<p>Contenido de la sección 1.</p>
|
|
169
|
+
</section>
|
|
170
|
+
<section>
|
|
171
|
+
<h2>Sección 2</h2>
|
|
172
|
+
<p>Contenido de la sección 2.</p>
|
|
173
|
+
</section>
|
|
174
|
+
</article>
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
const doc = parseHTML(html);
|
|
178
|
+
const original = doc.querySelector("#article")!;
|
|
179
|
+
const cloned = original.cloneNode(true);
|
|
180
|
+
|
|
181
|
+
console.log("\n=== COMPARACIÓN ORIGINAL VS CLONADO ===");
|
|
182
|
+
|
|
183
|
+
const comparison = {
|
|
184
|
+
nodeName: {
|
|
185
|
+
original: original.nodeName,
|
|
186
|
+
cloned: cloned.nodeName,
|
|
187
|
+
match: original.nodeName === cloned.nodeName
|
|
188
|
+
},
|
|
189
|
+
id: {
|
|
190
|
+
original: original.getAttribute("id"),
|
|
191
|
+
cloned: cloned.getAttribute("id"),
|
|
192
|
+
match: original.getAttribute("id") === cloned.getAttribute("id")
|
|
193
|
+
},
|
|
194
|
+
childNodesLength: {
|
|
195
|
+
original: original.childNodes.length,
|
|
196
|
+
cloned: cloned.childNodes.length,
|
|
197
|
+
match: original.childNodes.length === cloned.childNodes.length
|
|
198
|
+
},
|
|
199
|
+
childrenLength: {
|
|
200
|
+
original: original.children.length,
|
|
201
|
+
cloned: cloned.children.length,
|
|
202
|
+
match: original.children.length === cloned.children.length
|
|
203
|
+
},
|
|
204
|
+
innerHTMLLength: {
|
|
205
|
+
original: original.innerHTML.length,
|
|
206
|
+
cloned: cloned.innerHTML.length,
|
|
207
|
+
match: original.innerHTML.length === cloned.innerHTML.length
|
|
208
|
+
},
|
|
209
|
+
textContentLength: {
|
|
210
|
+
original: original.textContent.length,
|
|
211
|
+
cloned: cloned.textContent.length,
|
|
212
|
+
match: original.textContent.length === cloned.textContent.length
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
console.table(comparison);
|
|
217
|
+
|
|
218
|
+
// VERIFICAR que todos coinciden
|
|
219
|
+
expect(comparison.nodeName.match).toBe(true);
|
|
220
|
+
expect(comparison.id.match).toBe(true);
|
|
221
|
+
expect(comparison.childNodesLength.match).toBe(true);
|
|
222
|
+
expect(comparison.childrenLength.match).toBe(true);
|
|
223
|
+
expect(comparison.innerHTMLLength.match).toBe(true);
|
|
224
|
+
expect(comparison.textContentLength.match).toBe(true);
|
|
225
|
+
|
|
226
|
+
// Verificar contenido específico
|
|
227
|
+
expect(cloned.querySelector("h1")?.textContent).toBe("Artículo de Prueba");
|
|
228
|
+
expect(cloned.querySelector(".intro")?.textContent).toBe("Introducción del artículo.");
|
|
229
|
+
expect(cloned.querySelectorAll("section").length).toBe(2);
|
|
230
|
+
expect(cloned.querySelectorAll("h2").length).toBe(2);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("REPRODUCCIÓN: verificar que innerHTML NO está vacío después de clonar", () => {
|
|
234
|
+
const testCases = [
|
|
235
|
+
{
|
|
236
|
+
name: "Elemento simple con texto",
|
|
237
|
+
html: `<div>Texto simple</div>`
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "Elemento con un hijo",
|
|
241
|
+
html: `<div><span>Un hijo</span></div>`
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "Elemento con múltiples hijos",
|
|
245
|
+
html: `<div><p>P1</p><p>P2</p><p>P3</p></div>`
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "Elemento con anidación profunda",
|
|
249
|
+
html: `<div><div><div><div>Profundo</div></div></div></div>`
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "Lista compleja",
|
|
253
|
+
html: `<ul><li>A</li><li>B<ul><li>B1</li><li>B2</li></ul></li><li>C</li></ul>`
|
|
254
|
+
}
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
console.log("\n=== VERIFICANDO innerHTML EN MÚLTIPLES CASOS ===");
|
|
258
|
+
|
|
259
|
+
testCases.forEach(({ name, html }) => {
|
|
260
|
+
const doc = parseHTML(html);
|
|
261
|
+
const root = doc.body || doc;
|
|
262
|
+
const element = root.querySelector("div") || root.querySelector("ul")!;
|
|
263
|
+
const cloned = element.cloneNode(true);
|
|
264
|
+
|
|
265
|
+
const result = {
|
|
266
|
+
caso: name,
|
|
267
|
+
originalInnerHTML_vacio: !element.innerHTML || element.innerHTML.length === 0,
|
|
268
|
+
clonedInnerHTML_vacio: !cloned.innerHTML || cloned.innerHTML.length === 0,
|
|
269
|
+
originalLength: element.innerHTML.length,
|
|
270
|
+
clonedLength: cloned.innerHTML.length,
|
|
271
|
+
match: element.innerHTML.length === cloned.innerHTML.length
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
console.log(JSON.stringify(result, null, 2));
|
|
275
|
+
|
|
276
|
+
// VERIFICAR que ningún innerHTML está vacío (excepto elementos sin hijos)
|
|
277
|
+
if (element.children.length > 0) {
|
|
278
|
+
expect(cloned.innerHTML).toBeTruthy();
|
|
279
|
+
expect(cloned.innerHTML.length).toBeGreaterThan(0);
|
|
280
|
+
expect(cloned.innerHTML.length).toBe(element.innerHTML.length);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("REPRODUCCIÓN: acceder a childNodes inmediatamente después de clonar", () => {
|
|
286
|
+
const html = `
|
|
287
|
+
<div id="test">
|
|
288
|
+
Texto inicial
|
|
289
|
+
<span>Span 1</span>
|
|
290
|
+
Texto intermedio
|
|
291
|
+
<span>Span 2</span>
|
|
292
|
+
Texto final
|
|
293
|
+
</div>
|
|
294
|
+
`;
|
|
295
|
+
|
|
296
|
+
const doc = parseHTML(html);
|
|
297
|
+
const test = doc.querySelector("#test")!;
|
|
298
|
+
|
|
299
|
+
console.log("\n=== ACCESO A childNodes ===");
|
|
300
|
+
console.log("Original childNodes.length:", test.childNodes.length);
|
|
301
|
+
|
|
302
|
+
const cloned = test.cloneNode(true);
|
|
303
|
+
|
|
304
|
+
console.log("Clonado childNodes.length INMEDIATAMENTE:", cloned.childNodes.length);
|
|
305
|
+
|
|
306
|
+
// Verificar inmediatamente después del clonado
|
|
307
|
+
expect(cloned.childNodes).toBeTruthy();
|
|
308
|
+
expect(cloned.childNodes.length).toBeGreaterThan(0);
|
|
309
|
+
expect(cloned.childNodes.length).toBe(test.childNodes.length);
|
|
310
|
+
|
|
311
|
+
// Verificar que podemos iterar los childNodes
|
|
312
|
+
console.log("\nIterando childNodes del clon:");
|
|
313
|
+
for (let i = 0; i < cloned.childNodes.length; i++) {
|
|
314
|
+
const node = cloned.childNodes[i];
|
|
315
|
+
console.log(` ${i}: nodeType=${node.nodeType}, nodeName=${node.nodeName}, content="${node.textContent?.substring(0, 20)}"`);
|
|
316
|
+
expect(node).toBeTruthy();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Verificar que los spans son accesibles
|
|
320
|
+
const spans = cloned.querySelectorAll("span");
|
|
321
|
+
expect(spans.length).toBe(2);
|
|
322
|
+
expect(spans[0]?.textContent).toBe("Span 1");
|
|
323
|
+
expect(spans[1]?.textContent).toBe("Span 2");
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script interactivo para probar cloneNode manualmente
|
|
5
|
+
*
|
|
6
|
+
* Uso:
|
|
7
|
+
* bun run tests/cloneNode-interactive.ts
|
|
8
|
+
*
|
|
9
|
+
* Este script permite probar cloneNode con diferentes HTML inputs
|
|
10
|
+
* y ver en detalle qué sucede durante el clonado.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { parseHTML } from "../index.js";
|
|
14
|
+
|
|
15
|
+
// Colores para la consola
|
|
16
|
+
const colors = {
|
|
17
|
+
reset: "\x1b[0m",
|
|
18
|
+
bright: "\x1b[1m",
|
|
19
|
+
green: "\x1b[32m",
|
|
20
|
+
blue: "\x1b[34m",
|
|
21
|
+
yellow: "\x1b[33m",
|
|
22
|
+
red: "\x1b[31m",
|
|
23
|
+
cyan: "\x1b[36m",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function log(color: string, ...args: any[]) {
|
|
27
|
+
console.log(color + args.join(" ") + colors.reset);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function separator() {
|
|
31
|
+
console.log("\n" + "=".repeat(70) + "\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function testCloneNode(html: string, selector: string, testName: string) {
|
|
35
|
+
separator();
|
|
36
|
+
log(colors.bright + colors.blue, `🧪 TEST: ${testName}`);
|
|
37
|
+
console.log();
|
|
38
|
+
|
|
39
|
+
log(colors.cyan, "📝 HTML Input:");
|
|
40
|
+
console.log(html);
|
|
41
|
+
console.log();
|
|
42
|
+
|
|
43
|
+
// Parse HTML
|
|
44
|
+
const doc = parseHTML(html);
|
|
45
|
+
const element = doc.querySelector(selector);
|
|
46
|
+
|
|
47
|
+
if (!element) {
|
|
48
|
+
log(colors.red, "❌ ERROR: No se encontró elemento con selector:", selector);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
log(colors.yellow, "🔍 Elemento original encontrado:");
|
|
53
|
+
console.log(" - nodeName:", element.nodeName);
|
|
54
|
+
console.log(" - id:", element.getAttribute("id"));
|
|
55
|
+
console.log(" - childNodes.length:", element.childNodes.length);
|
|
56
|
+
console.log(" - children.length:", element.children.length);
|
|
57
|
+
console.log(" - innerHTML.length:", element.innerHTML.length);
|
|
58
|
+
console.log(" - textContent.length:", element.textContent.length);
|
|
59
|
+
console.log();
|
|
60
|
+
|
|
61
|
+
log(colors.bright, "⚙️ Clonando con cloneNode(true)...");
|
|
62
|
+
const cloned = element.cloneNode(true);
|
|
63
|
+
console.log();
|
|
64
|
+
|
|
65
|
+
log(colors.yellow, "🔍 Elemento clonado:");
|
|
66
|
+
console.log(" - nodeName:", cloned.nodeName);
|
|
67
|
+
console.log(" - id:", cloned.getAttribute("id"));
|
|
68
|
+
console.log(" - childNodes.length:", cloned.childNodes.length);
|
|
69
|
+
console.log(" - children.length:", cloned.children.length);
|
|
70
|
+
console.log(" - innerHTML.length:", cloned.innerHTML.length);
|
|
71
|
+
console.log(" - textContent.length:", cloned.textContent.length);
|
|
72
|
+
console.log();
|
|
73
|
+
|
|
74
|
+
// Comparación
|
|
75
|
+
log(colors.bright + colors.cyan, "📊 Comparación:");
|
|
76
|
+
const checks = {
|
|
77
|
+
"nodeName coincide":
|
|
78
|
+
element.nodeName === cloned.nodeName ? "✅" : "❌",
|
|
79
|
+
"childNodes.length coincide":
|
|
80
|
+
element.childNodes.length === cloned.childNodes.length ? "✅" : "❌",
|
|
81
|
+
"children.length coincide":
|
|
82
|
+
element.children.length === cloned.children.length ? "✅" : "❌",
|
|
83
|
+
"innerHTML.length coincide":
|
|
84
|
+
element.innerHTML.length === cloned.innerHTML.length ? "✅" : "❌",
|
|
85
|
+
"innerHTML no vacío":
|
|
86
|
+
cloned.innerHTML.length > 0 ? "✅" : "❌",
|
|
87
|
+
"childNodes no vacío":
|
|
88
|
+
cloned.childNodes.length > 0 ? "✅" : "❌",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const [check, result] of Object.entries(checks)) {
|
|
92
|
+
console.log(` ${result} ${check}`);
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
|
|
96
|
+
// Mostrar innerHTML
|
|
97
|
+
log(colors.cyan, "📄 Original innerHTML:");
|
|
98
|
+
console.log(element.innerHTML);
|
|
99
|
+
console.log();
|
|
100
|
+
|
|
101
|
+
log(colors.cyan, "📄 Clonado innerHTML:");
|
|
102
|
+
console.log(cloned.innerHTML);
|
|
103
|
+
console.log();
|
|
104
|
+
|
|
105
|
+
// Verificar querySelector en el clon
|
|
106
|
+
log(colors.bright + colors.cyan, "🔍 Pruebas de querySelector en el clon:");
|
|
107
|
+
const queries = [
|
|
108
|
+
"div",
|
|
109
|
+
"p",
|
|
110
|
+
"span",
|
|
111
|
+
"h1, h2, h3",
|
|
112
|
+
"ul",
|
|
113
|
+
"li",
|
|
114
|
+
"[id]",
|
|
115
|
+
"[class]",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const query of queries) {
|
|
119
|
+
const result = cloned.querySelector(query);
|
|
120
|
+
console.log(
|
|
121
|
+
` ${result ? "✅" : "⚪"} querySelector("${query}"): ${
|
|
122
|
+
result ? result.nodeName : "no encontrado"
|
|
123
|
+
}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const allDone = Object.values(checks).every((v) => v === "✅");
|
|
128
|
+
console.log();
|
|
129
|
+
log(
|
|
130
|
+
allDone ? colors.green : colors.red,
|
|
131
|
+
allDone ? "✅ TEST PASÓ" : "❌ TEST FALLÓ"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// TESTS
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
console.clear();
|
|
140
|
+
log(
|
|
141
|
+
colors.bright + colors.green,
|
|
142
|
+
"\n🧬 cloneNode Interactive Test Suite\n"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Test 1: Simple element
|
|
146
|
+
testCloneNode(
|
|
147
|
+
`<div id="simple">Hello World</div>`,
|
|
148
|
+
"#simple",
|
|
149
|
+
"Elemento simple con texto"
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Test 2: Nested elements
|
|
153
|
+
testCloneNode(
|
|
154
|
+
`
|
|
155
|
+
<div id="nested">
|
|
156
|
+
<h1>Title</h1>
|
|
157
|
+
<p>Paragraph with <strong>bold</strong> text</p>
|
|
158
|
+
<ul>
|
|
159
|
+
<li>Item 1</li>
|
|
160
|
+
<li>Item 2</li>
|
|
161
|
+
</ul>
|
|
162
|
+
</div>
|
|
163
|
+
`,
|
|
164
|
+
"#nested",
|
|
165
|
+
"Elementos anidados múltiples niveles"
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Test 3: Complex structure
|
|
169
|
+
testCloneNode(
|
|
170
|
+
`
|
|
171
|
+
<article id="article">
|
|
172
|
+
<header>
|
|
173
|
+
<h1>Article Title</h1>
|
|
174
|
+
<p class="meta">By Author Name</p>
|
|
175
|
+
</header>
|
|
176
|
+
<section class="content">
|
|
177
|
+
<p>First paragraph</p>
|
|
178
|
+
<p>Second paragraph</p>
|
|
179
|
+
<div class="callout">
|
|
180
|
+
<strong>Important:</strong> This is a callout
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
<footer>
|
|
184
|
+
<a href="#">Read more</a>
|
|
185
|
+
</footer>
|
|
186
|
+
</article>
|
|
187
|
+
`,
|
|
188
|
+
"#article",
|
|
189
|
+
"Estructura compleja tipo artículo"
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Test 4: Form elements
|
|
193
|
+
testCloneNode(
|
|
194
|
+
`
|
|
195
|
+
<form id="form">
|
|
196
|
+
<input type="text" name="username" value="john" />
|
|
197
|
+
<input type="email" name="email" value="test@example.com" />
|
|
198
|
+
<textarea name="bio">User bio</textarea>
|
|
199
|
+
<button type="submit">Submit</button>
|
|
200
|
+
</form>
|
|
201
|
+
`,
|
|
202
|
+
"#form",
|
|
203
|
+
"Formulario con inputs"
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Test 5: Table
|
|
207
|
+
testCloneNode(
|
|
208
|
+
`
|
|
209
|
+
<table id="table">
|
|
210
|
+
<thead>
|
|
211
|
+
<tr>
|
|
212
|
+
<th>Name</th>
|
|
213
|
+
<th>Age</th>
|
|
214
|
+
</tr>
|
|
215
|
+
</thead>
|
|
216
|
+
<tbody>
|
|
217
|
+
<tr>
|
|
218
|
+
<td>John</td>
|
|
219
|
+
<td>30</td>
|
|
220
|
+
</tr>
|
|
221
|
+
<tr>
|
|
222
|
+
<td>Jane</td>
|
|
223
|
+
<td>25</td>
|
|
224
|
+
</tr>
|
|
225
|
+
</tbody>
|
|
226
|
+
</table>
|
|
227
|
+
`,
|
|
228
|
+
"#table",
|
|
229
|
+
"Tabla HTML"
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
separator();
|
|
233
|
+
log(colors.bright + colors.green, "✅ Todos los tests interactivos completados");
|
|
234
|
+
log(colors.cyan, "\n💡 TIP: Modifica este archivo para agregar tus propios tests");
|
|
235
|
+
console.log();
|
|
@@ -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
|
+
});
|