@tkeron/html-parser 1.5.2 → 1.5.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/.github/workflows/npm_deploy.yml +13 -18
- package/bun.lock +3 -3
- package/package.json +7 -3
- package/tests/append.test.ts +246 -0
- package/tests/appendChild.test.ts +313 -0
- package/tests/attributes.test.ts +336 -0
- package/tests/innerOuterHTML.test.ts +461 -0
- package/tests/prepend.test.ts +235 -0
- package/tests/querySelector.test.ts +461 -0
- package/tests/removeChild.test.ts +312 -0
- package/tests/replaceChild.test.ts +375 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { parseHTML } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("DOM Content - innerHTML setter", () => {
|
|
5
|
+
describe("Basic innerHTML setter functionality", () => {
|
|
6
|
+
it("should set innerHTML and replace all children", () => {
|
|
7
|
+
const doc = parseHTML("<div><span>Old</span></div>");
|
|
8
|
+
const div = doc.querySelector("div");
|
|
9
|
+
|
|
10
|
+
div!.innerHTML = "<p>New</p>";
|
|
11
|
+
|
|
12
|
+
expect(div!.childNodes.length).toBe(1);
|
|
13
|
+
expect(div!.children[0]!.tagName).toBe("P");
|
|
14
|
+
expect(div!.children[0]!.textContent).toBe("New");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should handle empty string", () => {
|
|
18
|
+
const doc = parseHTML("<div><span>Content</span></div>");
|
|
19
|
+
const div = doc.querySelector("div");
|
|
20
|
+
|
|
21
|
+
div!.innerHTML = "";
|
|
22
|
+
|
|
23
|
+
expect(div!.childNodes.length).toBe(0);
|
|
24
|
+
expect(div!.innerHTML).toBe("");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should handle text content", () => {
|
|
28
|
+
const doc = parseHTML("<div><span>Old</span></div>");
|
|
29
|
+
const div = doc.querySelector("div");
|
|
30
|
+
|
|
31
|
+
div!.innerHTML = "Just text";
|
|
32
|
+
|
|
33
|
+
expect(div!.childNodes.length).toBe(1);
|
|
34
|
+
expect(div!.childNodes[0]!.nodeType).toBe(3);
|
|
35
|
+
expect(div!.textContent).toBe("Just text");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should handle malformed HTML gracefully", () => {
|
|
39
|
+
const doc = parseHTML("<div></div>");
|
|
40
|
+
const div = doc.querySelector("div");
|
|
41
|
+
|
|
42
|
+
div!.innerHTML = "<p>Unclosed<p>Another";
|
|
43
|
+
|
|
44
|
+
expect(div!.childNodes.length).toBe(2);
|
|
45
|
+
expect(div!.children[0]!.tagName).toBe("P");
|
|
46
|
+
expect(div!.children[1]!.tagName).toBe("P");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle special characters", () => {
|
|
50
|
+
const doc = parseHTML("<div></div>");
|
|
51
|
+
const div = doc.querySelector("div");
|
|
52
|
+
|
|
53
|
+
div!.innerHTML = "<p><script>alert('xss')</script></p>";
|
|
54
|
+
|
|
55
|
+
expect(div!.innerHTML).toContain("<script>");
|
|
56
|
+
expect(div!.textContent).toBe("<script>alert('xss')</script>");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle nested elements", () => {
|
|
60
|
+
const doc = parseHTML("<div></div>");
|
|
61
|
+
const div = doc.querySelector("div");
|
|
62
|
+
|
|
63
|
+
div!.innerHTML = "<div><span>Inner</span></div>";
|
|
64
|
+
|
|
65
|
+
expect(div!.childNodes.length).toBe(1);
|
|
66
|
+
expect(div!.children[0]!.tagName).toBe("DIV");
|
|
67
|
+
expect(div!.children[0]!.children[0]!.tagName).toBe("SPAN");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should clear all existing children", () => {
|
|
71
|
+
const doc = parseHTML(
|
|
72
|
+
"<div><span>1</span><span>2</span><span>3</span></div>",
|
|
73
|
+
);
|
|
74
|
+
const div = doc.querySelector("div");
|
|
75
|
+
|
|
76
|
+
div!.innerHTML = "<p>New</p>";
|
|
77
|
+
|
|
78
|
+
expect(div!.childNodes.length).toBe(1);
|
|
79
|
+
expect(div!.children.length).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should update firstChild and lastChild", () => {
|
|
83
|
+
const doc = parseHTML("<div><span>Old</span></div>");
|
|
84
|
+
const div = doc.querySelector("div");
|
|
85
|
+
|
|
86
|
+
div!.innerHTML = "<p>First</p><p>Last</p>";
|
|
87
|
+
|
|
88
|
+
expect(div!.firstChild!.textContent).toBe("First");
|
|
89
|
+
expect(div!.lastChild!.textContent).toBe("Last");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should update children array", () => {
|
|
93
|
+
const doc = parseHTML("<div><span>Old</span></div>");
|
|
94
|
+
const div = doc.querySelector("div");
|
|
95
|
+
|
|
96
|
+
div!.innerHTML = "<p>1</p><p>2</p><p>3</p>";
|
|
97
|
+
|
|
98
|
+
expect(div!.children.length).toBe(3);
|
|
99
|
+
expect(div!.children[0]!.tagName).toBe("P");
|
|
100
|
+
expect(div!.children[1]!.tagName).toBe("P");
|
|
101
|
+
expect(div!.children[2]!.tagName).toBe("P");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("innerHTML with various HTML structures", () => {
|
|
106
|
+
it("should handle multiple sibling elements", () => {
|
|
107
|
+
const doc = parseHTML("<div></div>");
|
|
108
|
+
const div = doc.querySelector("div");
|
|
109
|
+
|
|
110
|
+
div!.innerHTML = "<span>1</span><span>2</span><span>3</span>";
|
|
111
|
+
|
|
112
|
+
expect(div!.children.length).toBe(3);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should handle mixed content", () => {
|
|
116
|
+
const doc = parseHTML("<div></div>");
|
|
117
|
+
const div = doc.querySelector("div");
|
|
118
|
+
|
|
119
|
+
div!.innerHTML = "Text <span>Element</span> More text";
|
|
120
|
+
|
|
121
|
+
expect(div!.childNodes.length).toBe(3);
|
|
122
|
+
expect(div!.childNodes[0]!.nodeType).toBe(3);
|
|
123
|
+
expect(div!.childNodes[1]!.nodeType).toBe(1);
|
|
124
|
+
expect(div!.childNodes[2]!.nodeType).toBe(3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should handle deeply nested content", () => {
|
|
128
|
+
const doc = parseHTML("<div></div>");
|
|
129
|
+
const div = doc.querySelector("div");
|
|
130
|
+
|
|
131
|
+
div!.innerHTML = "<div><div><div><span>Deep</span></div></div></div>";
|
|
132
|
+
|
|
133
|
+
const deep = div!.querySelector("span");
|
|
134
|
+
expect(deep!.textContent).toBe("Deep");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should handle comments", () => {
|
|
138
|
+
const doc = parseHTML("<div></div>");
|
|
139
|
+
const div = doc.querySelector("div");
|
|
140
|
+
|
|
141
|
+
div!.innerHTML = "<!-- comment --><span>Content</span>";
|
|
142
|
+
|
|
143
|
+
expect(div!.childNodes.length).toBe(2);
|
|
144
|
+
expect(div!.childNodes[0]!.nodeType).toBe(8);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should handle void elements", () => {
|
|
148
|
+
const doc = parseHTML("<div></div>");
|
|
149
|
+
const div = doc.querySelector("div");
|
|
150
|
+
|
|
151
|
+
div!.innerHTML = "<input type='text'><br><hr>";
|
|
152
|
+
|
|
153
|
+
expect(div!.children.length).toBe(3);
|
|
154
|
+
expect(div!.children[0]!.tagName).toBe("INPUT");
|
|
155
|
+
expect(div!.children[1]!.tagName).toBe("BR");
|
|
156
|
+
expect(div!.children[2]!.tagName).toBe("HR");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should handle script tags (as parsed structure, not executed)", () => {
|
|
160
|
+
const doc = parseHTML("<div></div>");
|
|
161
|
+
const div = doc.querySelector("div");
|
|
162
|
+
|
|
163
|
+
div!.innerHTML = "<script>var x = 1;</script>";
|
|
164
|
+
|
|
165
|
+
expect(div!.children.length).toBe(1);
|
|
166
|
+
expect(div!.children[0]!.tagName).toBe("SCRIPT");
|
|
167
|
+
expect(div!.children[0]!.textContent).toBe("var x = 1;");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should handle style tags", () => {
|
|
171
|
+
const doc = parseHTML("<div></div>");
|
|
172
|
+
const div = doc.querySelector("div");
|
|
173
|
+
|
|
174
|
+
div!.innerHTML = "<style>.class { color: red; }</style>";
|
|
175
|
+
|
|
176
|
+
expect(div!.children.length).toBe(1);
|
|
177
|
+
expect(div!.children[0]!.tagName).toBe("STYLE");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should handle table structure", () => {
|
|
181
|
+
const doc = parseHTML("<div></div>");
|
|
182
|
+
const div = doc.querySelector("div");
|
|
183
|
+
|
|
184
|
+
div!.innerHTML = "<table><tr><td>Cell</td></tr></table>";
|
|
185
|
+
|
|
186
|
+
const table = div!.querySelector("table");
|
|
187
|
+
expect(table).not.toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should handle lists", () => {
|
|
191
|
+
const doc = parseHTML("<div></div>");
|
|
192
|
+
const div = doc.querySelector("div");
|
|
193
|
+
|
|
194
|
+
div!.innerHTML = "<ul><li>Item 1</li><li>Item 2</li></ul>";
|
|
195
|
+
|
|
196
|
+
const items = div!.querySelectorAll("li");
|
|
197
|
+
expect(items.length).toBe(2);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("innerHTML special content", () => {
|
|
202
|
+
it("should handle unicode content", () => {
|
|
203
|
+
const doc = parseHTML("<div></div>");
|
|
204
|
+
const div = doc.querySelector("div");
|
|
205
|
+
|
|
206
|
+
div!.innerHTML = "<span>日本語 🎉</span>";
|
|
207
|
+
|
|
208
|
+
expect(div!.textContent).toBe("日本語 🎉");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should handle whitespace-only content (trimmed by parser)", () => {
|
|
212
|
+
const doc = parseHTML("<div></div>");
|
|
213
|
+
const div = doc.querySelector("div");
|
|
214
|
+
|
|
215
|
+
div!.innerHTML = " \n\t ";
|
|
216
|
+
|
|
217
|
+
expect(div!.childNodes.length).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should handle attributes with special characters", () => {
|
|
221
|
+
const doc = parseHTML("<div></div>");
|
|
222
|
+
const div = doc.querySelector("div");
|
|
223
|
+
|
|
224
|
+
div!.innerHTML =
|
|
225
|
+
'<span data-test="value with "quotes"">Content</span>';
|
|
226
|
+
|
|
227
|
+
const span = div!.querySelector("span");
|
|
228
|
+
expect(span).not.toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should handle multiple attributes", () => {
|
|
232
|
+
const doc = parseHTML("<div></div>");
|
|
233
|
+
const div = doc.querySelector("div");
|
|
234
|
+
|
|
235
|
+
div!.innerHTML =
|
|
236
|
+
'<span id="myId" class="myClass" data-value="123">Content</span>';
|
|
237
|
+
|
|
238
|
+
const span = div!.querySelector("span");
|
|
239
|
+
expect(span!.id).toBe("myId");
|
|
240
|
+
expect(span!.className).toBe("myClass");
|
|
241
|
+
expect(span!.getAttribute("data-value")).toBe("123");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("innerHTML getter", () => {
|
|
246
|
+
it("should return empty string for empty element", () => {
|
|
247
|
+
const doc = parseHTML("<div></div>");
|
|
248
|
+
const div = doc.querySelector("div");
|
|
249
|
+
|
|
250
|
+
expect(div!.innerHTML).toBe("");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should return text content for text-only", () => {
|
|
254
|
+
const doc = parseHTML("<div>Hello</div>");
|
|
255
|
+
const div = doc.querySelector("div");
|
|
256
|
+
|
|
257
|
+
expect(div!.innerHTML).toBe("Hello");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("should return serialized HTML for elements", () => {
|
|
261
|
+
const doc = parseHTML("<div><span>Content</span></div>");
|
|
262
|
+
const div = doc.querySelector("div");
|
|
263
|
+
|
|
264
|
+
expect(div!.innerHTML).toContain("<span>");
|
|
265
|
+
expect(div!.innerHTML).toContain("</span>");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("DOM Content - outerHTML setter", () => {
|
|
271
|
+
describe("Basic outerHTML setter functionality", () => {
|
|
272
|
+
it("should replace element with new HTML", () => {
|
|
273
|
+
const doc = parseHTML("<div><p id='test'>Old</p></div>");
|
|
274
|
+
const p = doc.querySelector("#test");
|
|
275
|
+
|
|
276
|
+
p!.outerHTML = "<span>New</span>";
|
|
277
|
+
|
|
278
|
+
const span = doc.querySelector("span");
|
|
279
|
+
expect(span).not.toBeNull();
|
|
280
|
+
expect(span!.textContent).toBe("New");
|
|
281
|
+
expect(doc.querySelector("#test")).toBeNull();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should handle multiple elements", () => {
|
|
285
|
+
const doc = parseHTML("<div><p>Test</p></div>");
|
|
286
|
+
const p = doc.querySelector("p");
|
|
287
|
+
|
|
288
|
+
p!.outerHTML = "<span>One</span><em>Two</em>";
|
|
289
|
+
|
|
290
|
+
expect(doc.querySelector("span")).not.toBeNull();
|
|
291
|
+
expect(doc.querySelector("em")).not.toBeNull();
|
|
292
|
+
expect(doc.querySelector("p")).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should handle text content", () => {
|
|
296
|
+
const doc = parseHTML("<div><p>Test</p></div>");
|
|
297
|
+
const p = doc.querySelector("p");
|
|
298
|
+
|
|
299
|
+
p!.outerHTML = "Just text";
|
|
300
|
+
|
|
301
|
+
const div = doc.querySelector("div");
|
|
302
|
+
expect(div!.childNodes.length).toBe(1);
|
|
303
|
+
expect(div!.childNodes[0]!.nodeType).toBe(3);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should update parent references", () => {
|
|
307
|
+
const doc = parseHTML("<div><p>Old</p></div>");
|
|
308
|
+
const div = doc.querySelector("div");
|
|
309
|
+
const p = doc.querySelector("p");
|
|
310
|
+
|
|
311
|
+
p!.outerHTML = "<span>New</span>";
|
|
312
|
+
|
|
313
|
+
expect(div!.childNodes.length).toBe(1);
|
|
314
|
+
expect(div!.children[0]!.tagName).toBe("SPAN");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("should handle special characters", () => {
|
|
318
|
+
const doc = parseHTML("<div><p>Test</p></div>");
|
|
319
|
+
const p = doc.querySelector("p");
|
|
320
|
+
|
|
321
|
+
p!.outerHTML = "<span>&</span>";
|
|
322
|
+
|
|
323
|
+
const span = doc.querySelector("span");
|
|
324
|
+
expect(span!.innerHTML).toBe("&");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should throw when element has no parent", () => {
|
|
328
|
+
const doc = parseHTML("<div></div>");
|
|
329
|
+
const orphan = doc.createElement("span");
|
|
330
|
+
|
|
331
|
+
expect(() => {
|
|
332
|
+
orphan.outerHTML = "<p>Test</p>";
|
|
333
|
+
}).toThrow();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("outerHTML with siblings", () => {
|
|
338
|
+
it("should preserve previous siblings", () => {
|
|
339
|
+
const doc = parseHTML("<div><span>Before</span><p>Replace</p></div>");
|
|
340
|
+
const p = doc.querySelector("p");
|
|
341
|
+
|
|
342
|
+
p!.outerHTML = "<em>New</em>";
|
|
343
|
+
|
|
344
|
+
const div = doc.querySelector("div");
|
|
345
|
+
expect(div!.children[0]!.tagName).toBe("SPAN");
|
|
346
|
+
expect(div!.children[1]!.tagName).toBe("EM");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should preserve next siblings", () => {
|
|
350
|
+
const doc = parseHTML("<div><p>Replace</p><span>After</span></div>");
|
|
351
|
+
const p = doc.querySelector("p");
|
|
352
|
+
|
|
353
|
+
p!.outerHTML = "<em>New</em>";
|
|
354
|
+
|
|
355
|
+
const div = doc.querySelector("div");
|
|
356
|
+
expect(div!.children[0]!.tagName).toBe("EM");
|
|
357
|
+
expect(div!.children[1]!.tagName).toBe("SPAN");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should update sibling references", () => {
|
|
361
|
+
const doc = parseHTML("<div><span>A</span><p>B</p><span>C</span></div>");
|
|
362
|
+
const p = doc.querySelector("p");
|
|
363
|
+
|
|
364
|
+
p!.outerHTML = "<em>New</em>";
|
|
365
|
+
|
|
366
|
+
const div = doc.querySelector("div");
|
|
367
|
+
const em = doc.querySelector("em");
|
|
368
|
+
expect(div!.children[0]!.nextElementSibling).toBe(em);
|
|
369
|
+
expect(div!.children[2]!.previousElementSibling).toBe(em);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("should handle replacing with multiple elements among siblings", () => {
|
|
373
|
+
const doc = parseHTML("<div><span>A</span><p>B</p><span>C</span></div>");
|
|
374
|
+
const p = doc.querySelector("p");
|
|
375
|
+
|
|
376
|
+
p!.outerHTML = "<em>1</em><em>2</em>";
|
|
377
|
+
|
|
378
|
+
const div = doc.querySelector("div");
|
|
379
|
+
expect(div!.children.length).toBe(4);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("outerHTML with empty content", () => {
|
|
384
|
+
it("should remove element when set to empty string", () => {
|
|
385
|
+
const doc = parseHTML("<div><p>Remove me</p></div>");
|
|
386
|
+
const p = doc.querySelector("p");
|
|
387
|
+
|
|
388
|
+
p!.outerHTML = "";
|
|
389
|
+
|
|
390
|
+
expect(doc.querySelector("p")).toBeNull();
|
|
391
|
+
const div = doc.querySelector("div");
|
|
392
|
+
expect(div!.childNodes.length).toBe(0);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("should remove element and preserve siblings", () => {
|
|
396
|
+
const doc = parseHTML(
|
|
397
|
+
"<div><span>A</span><p>Remove</p><span>B</span></div>",
|
|
398
|
+
);
|
|
399
|
+
const p = doc.querySelector("p");
|
|
400
|
+
|
|
401
|
+
p!.outerHTML = "";
|
|
402
|
+
|
|
403
|
+
const div = doc.querySelector("div");
|
|
404
|
+
expect(div!.children.length).toBe(2);
|
|
405
|
+
expect(div!.children[0]?.nextElementSibling).toBe(
|
|
406
|
+
div!.children[1] ?? null,
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("outerHTML replaced element state", () => {
|
|
412
|
+
it("should detach replaced element", () => {
|
|
413
|
+
const doc = parseHTML("<div><p>Old</p></div>");
|
|
414
|
+
const p = doc.querySelector("p");
|
|
415
|
+
|
|
416
|
+
p!.outerHTML = "<span>New</span>";
|
|
417
|
+
|
|
418
|
+
expect(p!.parentNode).toBe(null);
|
|
419
|
+
expect(p!.previousSibling).toBe(null);
|
|
420
|
+
expect(p!.nextSibling).toBe(null);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should preserve replaced element children", () => {
|
|
424
|
+
const doc = parseHTML("<div><p><span>Child</span></p></div>");
|
|
425
|
+
const p = doc.querySelector("p");
|
|
426
|
+
|
|
427
|
+
p!.outerHTML = "<em>New</em>";
|
|
428
|
+
|
|
429
|
+
expect(p!.childNodes.length).toBe(1);
|
|
430
|
+
expect((p!.childNodes[0] as Element).tagName).toBe("SPAN");
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe("outerHTML getter", () => {
|
|
435
|
+
it("should return serialized element", () => {
|
|
436
|
+
const doc = parseHTML("<div><span>Content</span></div>");
|
|
437
|
+
const span = doc.querySelector("span");
|
|
438
|
+
|
|
439
|
+
expect(span!.outerHTML).toContain("<span>");
|
|
440
|
+
expect(span!.outerHTML).toContain("</span>");
|
|
441
|
+
expect(span!.outerHTML).toContain("Content");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("should include attributes", () => {
|
|
445
|
+
const doc = parseHTML(
|
|
446
|
+
'<div><span id="test" class="myClass">Content</span></div>',
|
|
447
|
+
);
|
|
448
|
+
const span = doc.querySelector("span");
|
|
449
|
+
|
|
450
|
+
expect(span!.outerHTML).toContain('id="test"');
|
|
451
|
+
expect(span!.outerHTML).toContain('class="myClass"');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("should handle void elements", () => {
|
|
455
|
+
const doc = parseHTML("<div><br></div>");
|
|
456
|
+
const br = doc.querySelector("br");
|
|
457
|
+
|
|
458
|
+
expect(br!.outerHTML).toBe("<br>");
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { parseHTML } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("DOM Manipulation - prepend", () => {
|
|
5
|
+
describe("Basic prepend functionality", () => {
|
|
6
|
+
it("should prepend a node to an element", () => {
|
|
7
|
+
const doc = parseHTML("<div><span>Existing</span></div>");
|
|
8
|
+
const div = doc.querySelector("div");
|
|
9
|
+
const newSpan = doc.createElement("span");
|
|
10
|
+
newSpan.textContent = "New";
|
|
11
|
+
|
|
12
|
+
div!.prepend(newSpan);
|
|
13
|
+
|
|
14
|
+
expect(div!.childNodes.length).toBe(2);
|
|
15
|
+
expect(div!.childNodes[0]!.textContent).toBe("New");
|
|
16
|
+
expect(div!.childNodes[1]!.textContent).toBe("Existing");
|
|
17
|
+
expect((newSpan.parentNode as Element).tagName).toBe("DIV");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should prepend multiple nodes", () => {
|
|
21
|
+
const doc = parseHTML("<div><span>Existing</span></div>");
|
|
22
|
+
const div = doc.querySelector("div");
|
|
23
|
+
const span1 = doc.createElement("span");
|
|
24
|
+
span1.textContent = "1";
|
|
25
|
+
const span2 = doc.createElement("span");
|
|
26
|
+
span2.textContent = "2";
|
|
27
|
+
|
|
28
|
+
div!.prepend(span1, span2);
|
|
29
|
+
|
|
30
|
+
expect(div!.childNodes.length).toBe(3);
|
|
31
|
+
expect(div!.childNodes[0]!.textContent).toBe("1");
|
|
32
|
+
expect(div!.childNodes[1]!.textContent).toBe("2");
|
|
33
|
+
expect(div!.childNodes[2]!.textContent).toBe("Existing");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should prepend to empty element", () => {
|
|
37
|
+
const doc = parseHTML("<div></div>");
|
|
38
|
+
const div = doc.querySelector("div");
|
|
39
|
+
const newSpan = doc.createElement("span");
|
|
40
|
+
newSpan.textContent = "First";
|
|
41
|
+
|
|
42
|
+
div!.prepend(newSpan);
|
|
43
|
+
|
|
44
|
+
expect(div!.childNodes.length).toBe(1);
|
|
45
|
+
expect(div!.childNodes[0]!.textContent).toBe("First");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should update firstChild reference", () => {
|
|
49
|
+
const doc = parseHTML("<div><span>Old first</span></div>");
|
|
50
|
+
const div = doc.querySelector("div");
|
|
51
|
+
const newSpan = doc.createElement("span");
|
|
52
|
+
newSpan.textContent = "New first";
|
|
53
|
+
|
|
54
|
+
div!.prepend(newSpan);
|
|
55
|
+
|
|
56
|
+
expect(div!.firstChild).toBe(newSpan);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should update sibling references correctly", () => {
|
|
60
|
+
const doc = parseHTML("<div><span>Existing</span></div>");
|
|
61
|
+
const div = doc.querySelector("div");
|
|
62
|
+
const existingSpan = div!.childNodes[0]!;
|
|
63
|
+
const newSpan = doc.createElement("span");
|
|
64
|
+
|
|
65
|
+
div!.prepend(newSpan);
|
|
66
|
+
|
|
67
|
+
expect(newSpan.nextSibling).toBe(existingSpan);
|
|
68
|
+
expect(existingSpan.previousSibling).toBe(newSpan);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("Prepending strings", () => {
|
|
73
|
+
it("should prepend a string as text node", () => {
|
|
74
|
+
const doc = parseHTML("<div><span>Existing</span></div>");
|
|
75
|
+
const div = doc.querySelector("div");
|
|
76
|
+
|
|
77
|
+
div!.prepend("Hello ");
|
|
78
|
+
|
|
79
|
+
expect(div!.childNodes.length).toBe(2);
|
|
80
|
+
expect(div!.childNodes[0]!.nodeType).toBe(3);
|
|
81
|
+
expect(div!.childNodes[0]!.textContent).toBe("Hello ");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should prepend multiple strings", () => {
|
|
85
|
+
const doc = parseHTML("<div></div>");
|
|
86
|
+
const div = doc.querySelector("div");
|
|
87
|
+
|
|
88
|
+
div!.prepend("A", "B", "C");
|
|
89
|
+
|
|
90
|
+
expect(div!.childNodes.length).toBe(3);
|
|
91
|
+
expect(div!.textContent).toBe("ABC");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should prepend empty string", () => {
|
|
95
|
+
const doc = parseHTML("<div><span>Existing</span></div>");
|
|
96
|
+
const div = doc.querySelector("div");
|
|
97
|
+
|
|
98
|
+
div!.prepend("");
|
|
99
|
+
|
|
100
|
+
expect(div!.childNodes.length).toBe(2);
|
|
101
|
+
expect(div!.childNodes[0]!.nodeType).toBe(3);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should prepend mixed nodes and strings", () => {
|
|
105
|
+
const doc = parseHTML("<div><span>Existing</span></div>");
|
|
106
|
+
const div = doc.querySelector("div");
|
|
107
|
+
const p = doc.createElement("p");
|
|
108
|
+
p.textContent = "P";
|
|
109
|
+
|
|
110
|
+
div!.prepend("Text", p);
|
|
111
|
+
|
|
112
|
+
expect(div!.childNodes.length).toBe(3);
|
|
113
|
+
expect(div!.childNodes[0]!.nodeType).toBe(3);
|
|
114
|
+
expect((div!.childNodes[1] as Element).tagName).toBe("P");
|
|
115
|
+
expect((div!.childNodes[2] as Element).tagName).toBe("SPAN");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should prepend string with HTML special characters", () => {
|
|
119
|
+
const doc = parseHTML("<div></div>");
|
|
120
|
+
const div = doc.querySelector("div");
|
|
121
|
+
|
|
122
|
+
div!.prepend("<b>bold</b> & 'quoted'");
|
|
123
|
+
|
|
124
|
+
expect(div!.textContent).toBe("<b>bold</b> & 'quoted'");
|
|
125
|
+
expect(div!.childNodes[0]!.nodeType).toBe(3);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("Moving nodes (node already has parent)", () => {
|
|
130
|
+
it("should move node from one parent to another", () => {
|
|
131
|
+
const doc = parseHTML("<div><span>Child</span></div><p><em>Em</em></p>");
|
|
132
|
+
const div = doc.querySelector("div");
|
|
133
|
+
const p = doc.querySelector("p");
|
|
134
|
+
const span = doc.querySelector("span");
|
|
135
|
+
|
|
136
|
+
p!.prepend(span!);
|
|
137
|
+
|
|
138
|
+
expect(div!.childNodes.length).toBe(0);
|
|
139
|
+
expect(p!.childNodes.length).toBe(2);
|
|
140
|
+
expect(p!.childNodes[0] ?? null).toBe(span);
|
|
141
|
+
expect(span!.parentNode).toBe(p);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should update original parent sibling references", () => {
|
|
145
|
+
const doc = parseHTML(
|
|
146
|
+
"<div><span>A</span><span>B</span><span>C</span></div><p></p>",
|
|
147
|
+
);
|
|
148
|
+
const div = doc.querySelector("div");
|
|
149
|
+
const p = doc.querySelector("p");
|
|
150
|
+
const spanB = div!.childNodes[1]!;
|
|
151
|
+
|
|
152
|
+
p!.prepend(spanB);
|
|
153
|
+
|
|
154
|
+
expect(div!.childNodes.length).toBe(2);
|
|
155
|
+
expect(div!.childNodes[0]?.nextSibling).toBe(div!.childNodes[1] ?? null);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("Edge cases", () => {
|
|
160
|
+
it("should handle no arguments", () => {
|
|
161
|
+
const doc = parseHTML("<div><span>Existing</span></div>");
|
|
162
|
+
const div = doc.querySelector("div");
|
|
163
|
+
|
|
164
|
+
div!.prepend();
|
|
165
|
+
|
|
166
|
+
expect(div!.childNodes.length).toBe(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should prepend text node", () => {
|
|
170
|
+
const doc = parseHTML("<div><span>Existing</span></div>");
|
|
171
|
+
const div = doc.querySelector("div");
|
|
172
|
+
const textNode = doc.createTextNode("Hello");
|
|
173
|
+
|
|
174
|
+
div!.prepend(textNode);
|
|
175
|
+
|
|
176
|
+
expect(div!.childNodes.length).toBe(2);
|
|
177
|
+
expect(div!.childNodes[0]!).toBe(textNode);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should prepend parsed comment node", () => {
|
|
181
|
+
const doc = parseHTML("<div><!-- comment --></div>");
|
|
182
|
+
const div = doc.querySelector("div");
|
|
183
|
+
const comment = div!.childNodes[0]!;
|
|
184
|
+
const target = parseHTML("<p><span>Existing</span></p>").querySelector(
|
|
185
|
+
"p",
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
target!.prepend(comment);
|
|
189
|
+
|
|
190
|
+
expect(target!.childNodes.length).toBe(2);
|
|
191
|
+
expect(target!.childNodes[0]!.nodeType).toBe(8);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should handle deeply nested structure", () => {
|
|
195
|
+
const doc = parseHTML("<div><span>Deep</span></div>");
|
|
196
|
+
const div = doc.querySelector("div");
|
|
197
|
+
const wrapper = doc.createElement("div");
|
|
198
|
+
const inner = doc.createElement("div");
|
|
199
|
+
const deepest = doc.createElement("span");
|
|
200
|
+
deepest.textContent = "Deepest";
|
|
201
|
+
inner.append(deepest);
|
|
202
|
+
wrapper.append(inner);
|
|
203
|
+
|
|
204
|
+
div!.prepend(wrapper);
|
|
205
|
+
|
|
206
|
+
expect(
|
|
207
|
+
div!.childNodes[0]!.childNodes[0]!.childNodes[0]!.textContent,
|
|
208
|
+
).toBe("Deepest");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should maintain order with many nodes", () => {
|
|
212
|
+
const doc = parseHTML("<div></div>");
|
|
213
|
+
const div = doc.querySelector("div");
|
|
214
|
+
const nodes: any[] = [];
|
|
215
|
+
for (let i = 0; i < 50; i++) {
|
|
216
|
+
nodes.push(String(i));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
div!.prepend(...nodes);
|
|
220
|
+
|
|
221
|
+
expect(div!.childNodes.length).toBe(50);
|
|
222
|
+
expect(div!.childNodes[0]!.textContent).toBe("0");
|
|
223
|
+
expect(div!.childNodes[49]!.textContent).toBe("49");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should handle unicode strings", () => {
|
|
227
|
+
const doc = parseHTML("<div></div>");
|
|
228
|
+
const div = doc.querySelector("div");
|
|
229
|
+
|
|
230
|
+
div!.prepend("🚀 日本語");
|
|
231
|
+
|
|
232
|
+
expect(div!.textContent).toBe("🚀 日本語");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|