@tkeron/html-parser 0.1.3 → 0.1.5
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/README.md +5 -5
- package/bun.lock +4 -4
- package/index.ts +0 -5
- package/package.json +1 -1
- package/src/css-selector.ts +0 -5
- package/src/dom-simulator.ts +122 -45
- package/src/tokenizer.ts +0 -20
- package/tests/advanced.test.ts +2 -2
- package/tests/cloneNode.test.ts +587 -0
- package/tests/custom-elements.test.ts +8 -8
- package/tests/official/acid/acid-tests.test.ts +6 -6
- package/tests/official/final-output/final-output.test.ts +15 -15
- package/tests/official/html5lib/tokenizer-utils.ts +19 -31
- package/tests/official/html5lib/tokenizer.test.ts +4 -4
- package/tests/official/html5lib/tree-construction-utils.ts +20 -34
- package/tests/official/html5lib/tree-construction.test.ts +5 -5
- package/tests/official/validator/validator-tests.test.ts +11 -11
- package/tests/official/wpt/wpt-tests.test.ts +5 -5
- package/tests/outerHTML-replacement.test.ts +208 -0
- package/tests/parser.test.ts +1 -1
- package/tests/test-page-0.txt +12 -355
- package/tests/api-integration.test.ts +0 -114
- package/tests/dom-adoption.test.ts +0 -363
- package/tests/dom-synchronization.test.ts +0 -675
- package/tests/setAttribute-outerHTML.test.ts +0 -102
|
@@ -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
|
+
|
|
15
|
+
expect(cloned).toBeTruthy();
|
|
16
|
+
expect(cloned.nodeName).toBe("DIV");
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
expect(cloned.getAttribute("id")).toBe("original");
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
expect(cloned.textContent).toBe("Hello World");
|
|
23
|
+
|
|
24
|
+
|
|
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
|
+
|
|
42
|
+
expect(cloned.nodeName).toBe("DIV");
|
|
43
|
+
expect(cloned.getAttribute("id")).toBe("parent");
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
expect(cloned.childNodes.length).toBeGreaterThan(0);
|
|
47
|
+
|
|
48
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
93
|
+
const originalInnerHTML = container.innerHTML;
|
|
94
|
+
expect(originalInnerHTML).toBeTruthy();
|
|
95
|
+
expect(originalInnerHTML.length).toBeGreaterThan(0);
|
|
96
|
+
|
|
97
|
+
const cloned = container.cloneNode(true);
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
expect(cloned.innerHTML).toBeTruthy();
|
|
101
|
+
expect(cloned.innerHTML.length).toBeGreaterThan(0);
|
|
102
|
+
|
|
103
|
+
|
|
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
|
+
|
|
115
|
+
const originalChildCount = mixed.childNodes.length;
|
|
116
|
+
expect(originalChildCount).toBeGreaterThan(0);
|
|
117
|
+
|
|
118
|
+
const cloned = mixed.cloneNode(true);
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
expect(cloned.childNodes.length).toBe(originalChildCount);
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
expect(cloned.textContent).toBe("Text beforebold textText after");
|
|
125
|
+
|
|
126
|
+
|
|
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
|
+
|
|
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
|
+
|
|
165
|
+
expect(cloned.nodeName).toBe("ARTICLE");
|
|
166
|
+
expect(cloned.getAttribute("id")).toBe("article");
|
|
167
|
+
|
|
168
|
+
|
|
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
|
+
|
|
177
|
+
const highlight = cloned.querySelector(".highlight");
|
|
178
|
+
expect(highlight).toBeTruthy();
|
|
179
|
+
expect(highlight?.querySelector("span")?.textContent).toBe("Highlighted");
|
|
180
|
+
|
|
181
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
238
|
+
expect(cloned.getAttribute("id")).toBe("test");
|
|
239
|
+
expect(cloned.getAttribute("class")).toBe("container");
|
|
240
|
+
expect(cloned.getAttribute("data-value")).toBe("123");
|
|
241
|
+
|
|
242
|
+
|
|
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
|
+
|
|
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
|
+
|
|
277
|
+
clonedChild?.setAttribute("data-cloned", "yes");
|
|
278
|
+
|
|
279
|
+
|
|
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
|
+
|
|
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
|
+
|
|
344
|
+
const originalInnerHTML = container.innerHTML;
|
|
345
|
+
expect(originalInnerHTML).toBeTruthy();
|
|
346
|
+
|
|
347
|
+
const cloned = container.cloneNode(true);
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
const clonedInnerHTML = cloned.innerHTML;
|
|
351
|
+
expect(clonedInnerHTML).toBeTruthy();
|
|
352
|
+
expect(clonedInnerHTML.length).toBeGreaterThan(0);
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
expect(clonedInnerHTML).toContain("<p>Paragraph 1</p>");
|
|
356
|
+
expect(clonedInnerHTML).toContain("<p>Paragraph 2</p>");
|
|
357
|
+
|
|
358
|
+
|
|
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
|
+
|
|
373
|
+
expect(cloned.childNodes.length).toBe(originalChildCount);
|
|
374
|
+
|
|
375
|
+
|
|
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
|
+
|
|
390
|
+
expect(cloned.children).toBeTruthy();
|
|
391
|
+
expect(Array.isArray(cloned.children)).toBe(true);
|
|
392
|
+
expect(cloned.children.length).toBe(3);
|
|
393
|
+
|
|
394
|
+
|
|
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
|
+
|
|
408
|
+
expect(cloned.firstChild).toBeTruthy();
|
|
409
|
+
expect(cloned.lastChild).toBeTruthy();
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
|
|
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
|
+
|
|
431
|
+
dynamic.innerHTML = "<p>Dynamic content</p><span>More content</span>";
|
|
432
|
+
|
|
433
|
+
const cloned = dynamic.cloneNode(true);
|
|
434
|
+
|
|
435
|
+
|
|
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
|
+
|
|
450
|
+
expect(cloned.querySelector("p")?.textContent).toBe("Original");
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
cloned.innerHTML = "<span>Modified</span>";
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
expect(original.querySelector("p")?.textContent).toBe("Original");
|
|
457
|
+
expect(original.querySelector("span")).toBeNull();
|
|
458
|
+
|
|
459
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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);
|
|
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
|
+
|
|
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
|
+
});
|
|
@@ -210,7 +210,7 @@ describe('Custom Elements Support', () => {
|
|
|
210
210
|
const ast = parse(tokens);
|
|
211
211
|
|
|
212
212
|
const element = ast.children![0]!;
|
|
213
|
-
|
|
213
|
+
|
|
214
214
|
if (element.nodeName) {
|
|
215
215
|
expect(element.nodeName.toUpperCase()).toBe('MY-COMP');
|
|
216
216
|
}
|
|
@@ -382,11 +382,11 @@ describe('Custom Elements Support', () => {
|
|
|
382
382
|
const tokens = tokenize(html);
|
|
383
383
|
const ast = parse(tokens);
|
|
384
384
|
|
|
385
|
-
|
|
385
|
+
|
|
386
386
|
const userProfile = ast.children!.find(node => node.type === ASTNodeType.ELEMENT)!;
|
|
387
387
|
expect(userProfile.tagName).toBe('user-profile');
|
|
388
388
|
|
|
389
|
-
|
|
389
|
+
|
|
390
390
|
expect(userProfile.children).toBeDefined();
|
|
391
391
|
expect(userProfile.children!.length).toBeGreaterThan(0);
|
|
392
392
|
});
|
|
@@ -412,7 +412,7 @@ describe('Custom Elements Support', () => {
|
|
|
412
412
|
const tokens = tokenize(html);
|
|
413
413
|
const ast = parse(tokens);
|
|
414
414
|
|
|
415
|
-
|
|
415
|
+
|
|
416
416
|
const appRoot = ast.children!.find(node => node.type === ASTNodeType.ELEMENT)!;
|
|
417
417
|
expect(appRoot.tagName).toBe('app-root');
|
|
418
418
|
});
|
|
@@ -471,12 +471,12 @@ describe('Custom Elements Support', () => {
|
|
|
471
471
|
test('tokenizer should capture full custom element name', () => {
|
|
472
472
|
const tokens = tokenize('<my-component-123></my-component-123>');
|
|
473
473
|
|
|
474
|
-
|
|
474
|
+
|
|
475
475
|
const openTag = tokens.find(t => t.type === 'TAG_OPEN');
|
|
476
476
|
expect(openTag).toBeDefined();
|
|
477
477
|
expect(openTag!.value).toBe('my-component-123');
|
|
478
478
|
|
|
479
|
-
|
|
479
|
+
|
|
480
480
|
const closeTag = tokens.find(t => t.type === 'TAG_CLOSE');
|
|
481
481
|
expect(closeTag).toBeDefined();
|
|
482
482
|
expect(closeTag!.value).toBe('my-component-123');
|
|
@@ -642,7 +642,7 @@ describe('Custom Elements Support', () => {
|
|
|
642
642
|
const tokens = tokenize(html);
|
|
643
643
|
const ast = parse(tokens);
|
|
644
644
|
|
|
645
|
-
|
|
645
|
+
|
|
646
646
|
const myComp = ast.children!.find(node => node.type === ASTNodeType.ELEMENT)!;
|
|
647
647
|
expect(myComp.tagName).toBe('my-comp');
|
|
648
648
|
});
|
|
@@ -700,7 +700,7 @@ describe('Custom Elements Support', () => {
|
|
|
700
700
|
const tokens = tokenize('<table><tr><td><my-cell>content</my-cell></td></tr></table>');
|
|
701
701
|
const ast = parse(tokens);
|
|
702
702
|
|
|
703
|
-
|
|
703
|
+
|
|
704
704
|
const table = ast.children![0]!;
|
|
705
705
|
expect(table.tagName).toBe('table');
|
|
706
706
|
});
|
|
@@ -233,7 +233,7 @@ describe('Performance Benchmarks', () => {
|
|
|
233
233
|
const end = performance.now();
|
|
234
234
|
|
|
235
235
|
expect(ast).toBeDefined();
|
|
236
|
-
expect(end - start).toBeLessThan(10);
|
|
236
|
+
expect(end - start).toBeLessThan(10);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
it('should handle medium-sized HTML', () => {
|
|
@@ -245,7 +245,7 @@ describe('Performance Benchmarks', () => {
|
|
|
245
245
|
const end = performance.now();
|
|
246
246
|
|
|
247
247
|
expect(ast).toBeDefined();
|
|
248
|
-
expect(end - start).toBeLessThan(100);
|
|
248
|
+
expect(end - start).toBeLessThan(100);
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
it('should handle large HTML documents', () => {
|
|
@@ -257,7 +257,7 @@ describe('Performance Benchmarks', () => {
|
|
|
257
257
|
const end = performance.now();
|
|
258
258
|
|
|
259
259
|
expect(ast).toBeDefined();
|
|
260
|
-
expect(end - start).toBeLessThan(1000);
|
|
260
|
+
expect(end - start).toBeLessThan(1000);
|
|
261
261
|
});
|
|
262
262
|
|
|
263
263
|
it('should handle deeply nested HTML', () => {
|
|
@@ -276,7 +276,7 @@ describe('Performance Benchmarks', () => {
|
|
|
276
276
|
const end = performance.now();
|
|
277
277
|
|
|
278
278
|
expect(ast).toBeDefined();
|
|
279
|
-
expect(end - start).toBeLessThan(500);
|
|
279
|
+
expect(end - start).toBeLessThan(500);
|
|
280
280
|
});
|
|
281
281
|
});
|
|
282
282
|
|
|
@@ -284,14 +284,14 @@ describe('Memory Usage Tests', () => {
|
|
|
284
284
|
it('should not leak memory on repeated parsing', () => {
|
|
285
285
|
const testHtml = '<div><p>Memory test</p></div>';
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
|
|
288
288
|
for (let i = 0; i < 1000; i++) {
|
|
289
289
|
const tokens = tokenize(testHtml);
|
|
290
290
|
const ast = parse(tokens);
|
|
291
291
|
expect(ast).toBeDefined();
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
|
|
295
295
|
expect(true).toBe(true);
|
|
296
296
|
});
|
|
297
297
|
|