@tkeron/html-parser 0.1.5 → 0.1.7

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.
@@ -62,4 +62,67 @@ describe('CSS Selectors', () => {
62
62
  expect(nonExistent).toBeNull();
63
63
  });
64
64
  });
65
- });
65
+
66
+ describe('Element.matches', () => {
67
+ it('should match by tag name', () => {
68
+ const p = querySelector(doc, 'p');
69
+ expect(p?.matches('p')).toBe(true);
70
+ expect(p?.matches('div')).toBe(false);
71
+ });
72
+
73
+ it('should match by id', () => {
74
+ const intro = querySelector(doc, '#intro');
75
+ expect(intro?.matches('#intro')).toBe(true);
76
+ expect(intro?.matches('#other')).toBe(false);
77
+ });
78
+
79
+ it('should match by class', () => {
80
+ const first = querySelector(doc, '.first');
81
+ expect(first?.matches('.first')).toBe(true);
82
+ expect(first?.matches('.second')).toBe(false);
83
+ });
84
+
85
+ it('should match by multiple classes', () => {
86
+ const doc2 = parseHTML('<div class="foo bar baz">Test</div>');
87
+ const div = doc2.querySelector('div');
88
+ expect(div?.matches('.foo')).toBe(true);
89
+ expect(div?.matches('.bar')).toBe(true);
90
+ expect(div?.matches('.foo.bar')).toBe(true);
91
+ expect(div?.matches('.foo.baz')).toBe(true);
92
+ expect(div?.matches('.foo.bar.baz')).toBe(true);
93
+ expect(div?.matches('.foo.missing')).toBe(false);
94
+ });
95
+
96
+ it('should match by attribute', () => {
97
+ const intro = querySelector(doc, '#intro');
98
+ expect(intro?.matches('[id]')).toBe(true);
99
+ expect(intro?.matches('[id="intro"]')).toBe(true);
100
+ expect(intro?.matches('[class]')).toBe(true);
101
+ expect(intro?.matches('[title]')).toBe(false);
102
+ });
103
+
104
+ it('should match complex selectors', () => {
105
+ const intro = querySelector(doc, '#intro');
106
+ expect(intro?.matches('p#intro')).toBe(true);
107
+ expect(intro?.matches('p.first')).toBe(true);
108
+ expect(intro?.matches('div#intro')).toBe(false);
109
+ });
110
+
111
+ it('should match descendant selectors', () => {
112
+ const span = querySelector(doc, 'span');
113
+ expect(span?.matches('p span')).toBe(true);
114
+ expect(span?.matches('body span')).toBe(true);
115
+ expect(span?.matches('div span')).toBe(false);
116
+ });
117
+
118
+ it('should return false for invalid selector', () => {
119
+ const p = querySelector(doc, 'p');
120
+ expect(p?.matches('')).toBe(false);
121
+ });
122
+
123
+ it('should work with universal selector', () => {
124
+ const p = querySelector(doc, 'p');
125
+ expect(p?.matches('*')).toBe(true);
126
+ });
127
+ });
128
+ });
@@ -662,5 +662,91 @@ describe('HTML Tokenizer', () => {
662
662
  tokens.some(token => token.value === 'span');
663
663
  expect(hasValidElements).toBe(true);
664
664
  });
665
+
666
+ it('should handle empty angle brackets <>', () => {
667
+ const html = '<>text<div>content</div>';
668
+ const tokens = tokenize(html);
669
+
670
+ // Should skip the invalid <> and continue parsing
671
+ expect(tokens[tokens.length - 1]!.type).toBe(TokenType.EOF);
672
+ const divToken = tokens.find(t => t.value === 'div');
673
+ expect(divToken).toBeDefined();
674
+ });
675
+
676
+ it('should handle angle bracket with only space < >', () => {
677
+ const html = '< >text<p>paragraph</p>';
678
+ const tokens = tokenize(html);
679
+
680
+ expect(tokens[tokens.length - 1]!.type).toBe(TokenType.EOF);
681
+ const pToken = tokens.find(t => t.value === 'p');
682
+ expect(pToken).toBeDefined();
683
+ });
684
+
685
+ it('should handle tag with no valid name', () => {
686
+ const html = '<123>text</123><div>ok</div>';
687
+ const tokens = tokenize(html);
688
+
689
+ // Tags starting with numbers are invalid, should be treated as text
690
+ expect(tokens[tokens.length - 1]!.type).toBe(TokenType.EOF);
691
+ const divToken = tokens.find(t => t.value === 'div');
692
+ expect(divToken).toBeDefined();
693
+ });
694
+ });
695
+
696
+ describe('Entity Edge Cases', () => {
697
+ it('should handle entity without semicolon with valid prefix', () => {
698
+ // &nbsp followed by other text (no semicolon) should decode &nbsp
699
+ const tokens = tokenize('<div>&nbsptext</div>');
700
+
701
+ const textToken = tokens.find(t => t.type === TokenType.TEXT);
702
+ expect(textToken).toBeDefined();
703
+ // Should decode &nbsp (non-breaking space) and keep "text"
704
+ expect(textToken!.value).toContain('text');
705
+ });
706
+
707
+ it('should handle entity without semicolon - lt prefix', () => {
708
+ const tokens = tokenize('<div>&ltvalue</div>');
709
+
710
+ const textToken = tokens.find(t => t.type === TokenType.TEXT);
711
+ expect(textToken).toBeDefined();
712
+ // &lt should decode to < and "value" should follow
713
+ expect(textToken!.value).toBe('<value');
714
+ });
715
+
716
+ it('should handle entity without semicolon - gt prefix', () => {
717
+ const tokens = tokenize('<div>&gtvalue</div>');
718
+
719
+ const textToken = tokens.find(t => t.type === TokenType.TEXT);
720
+ expect(textToken).toBeDefined();
721
+ // &gt should decode to > and "value" should follow
722
+ expect(textToken!.value).toBe('>value');
723
+ });
724
+
725
+ it('should handle entity without semicolon - amp prefix', () => {
726
+ const tokens = tokenize('<div>&ampvalue</div>');
727
+
728
+ const textToken = tokens.find(t => t.type === TokenType.TEXT);
729
+ expect(textToken).toBeDefined();
730
+ // &amp should decode to & and "value" should follow
731
+ expect(textToken!.value).toBe('&value');
732
+ });
733
+
734
+ it('should handle unknown entity gracefully', () => {
735
+ const tokens = tokenize('<div>&unknownentity;</div>');
736
+
737
+ const textToken = tokens.find(t => t.type === TokenType.TEXT);
738
+ expect(textToken).toBeDefined();
739
+ // Unknown entity should be kept as-is
740
+ expect(textToken!.value).toBe('&unknownentity;');
741
+ });
742
+
743
+ it('should handle partial entity name with no matching prefix', () => {
744
+ const tokens = tokenize('<div>&xyz</div>');
745
+
746
+ const textToken = tokens.find(t => t.type === TokenType.TEXT);
747
+ expect(textToken).toBeDefined();
748
+ // No valid entity prefix, keep as-is
749
+ expect(textToken!.value).toBe('&xyz');
750
+ });
665
751
  })
666
752
  });
@@ -0,0 +1,471 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { parseHTML } from "../index";
3
+
4
+ /**
5
+ * Test suite for HTML void elements serialization
6
+ *
7
+ * Void elements should NOT have closing tags according to HTML spec:
8
+ * https://html.spec.whatwg.org/multipage/syntax.html#void-elements
9
+ *
10
+ * List: area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
11
+ */
12
+
13
+ const VOID_ELEMENTS = [
14
+ "area",
15
+ "base",
16
+ "br",
17
+ "col",
18
+ "embed",
19
+ "hr",
20
+ "img",
21
+ "input",
22
+ "link",
23
+ "meta",
24
+ "source",
25
+ "track",
26
+ "wbr",
27
+ ];
28
+
29
+ describe("Void Elements - outerHTML serialization", () => {
30
+ describe("Individual void elements without attributes", () => {
31
+ it("should serialize <br> without closing tag", () => {
32
+ const doc = parseHTML("<html><body><br></body></html>");
33
+ const br = doc.querySelector("br");
34
+ expect(br).not.toBeNull();
35
+ expect(br!.outerHTML).toBe("<br>");
36
+ });
37
+
38
+ it("should serialize <hr> without closing tag", () => {
39
+ const doc = parseHTML("<html><body><hr></body></html>");
40
+ const hr = doc.querySelector("hr");
41
+ expect(hr).not.toBeNull();
42
+ expect(hr!.outerHTML).toBe("<hr>");
43
+ });
44
+
45
+ it("should serialize <wbr> without closing tag", () => {
46
+ const doc = parseHTML("<html><body><wbr></body></html>");
47
+ const wbr = doc.querySelector("wbr");
48
+ expect(wbr).not.toBeNull();
49
+ expect(wbr!.outerHTML).toBe("<wbr>");
50
+ });
51
+ });
52
+
53
+ describe("Individual void elements with attributes", () => {
54
+ it("should serialize <img> with attributes without closing tag", () => {
55
+ const doc = parseHTML('<html><body><img src="test.jpg" alt="test image"></body></html>');
56
+ const img = doc.querySelector("img");
57
+ expect(img).not.toBeNull();
58
+ expect(img!.outerHTML).toBe('<img src="test.jpg" alt="test image">');
59
+ });
60
+
61
+ it("should serialize <input> with type attribute without closing tag", () => {
62
+ const doc = parseHTML('<html><body><input type="text" name="username"></body></html>');
63
+ const input = doc.querySelector("input");
64
+ expect(input).not.toBeNull();
65
+ expect(input!.outerHTML).toBe('<input type="text" name="username">');
66
+ });
67
+
68
+ it("should serialize <meta> with attributes without closing tag", () => {
69
+ const doc = parseHTML('<html><head><meta charset="utf-8"></head><body></body></html>');
70
+ const meta = doc.querySelector("meta");
71
+ expect(meta).not.toBeNull();
72
+ expect(meta!.outerHTML).toBe('<meta charset="utf-8">');
73
+ });
74
+
75
+ it("should serialize <link> with attributes without closing tag", () => {
76
+ const doc = parseHTML('<html><head><link rel="stylesheet" href="style.css"></head><body></body></html>');
77
+ const link = doc.querySelector("link");
78
+ expect(link).not.toBeNull();
79
+ expect(link!.outerHTML).toBe('<link rel="stylesheet" href="style.css">');
80
+ });
81
+
82
+ it("should serialize <base> with href without closing tag", () => {
83
+ const doc = parseHTML('<html><head><base href="https://example.com/"></head><body></body></html>');
84
+ const base = doc.querySelector("base");
85
+ expect(base).not.toBeNull();
86
+ expect(base!.outerHTML).toBe('<base href="https://example.com/">');
87
+ });
88
+
89
+ it("should serialize <col> with attributes without closing tag", () => {
90
+ const doc = parseHTML('<html><body><table><colgroup><col span="2" style="background:red"></colgroup></table></body></html>');
91
+ const col = doc.querySelector("col");
92
+ expect(col).not.toBeNull();
93
+ expect(col!.outerHTML).toBe('<col span="2" style="background:red">');
94
+ });
95
+
96
+ it("should serialize <embed> with attributes without closing tag", () => {
97
+ const doc = parseHTML('<html><body><embed src="video.swf" type="application/x-shockwave-flash"></body></html>');
98
+ const embed = doc.querySelector("embed");
99
+ expect(embed).not.toBeNull();
100
+ expect(embed!.outerHTML).toBe('<embed src="video.swf" type="application/x-shockwave-flash">');
101
+ });
102
+
103
+ it("should serialize <source> with attributes without closing tag", () => {
104
+ const doc = parseHTML('<html><body><video><source src="video.mp4" type="video/mp4"></video></body></html>');
105
+ const source = doc.querySelector("source");
106
+ expect(source).not.toBeNull();
107
+ expect(source!.outerHTML).toBe('<source src="video.mp4" type="video/mp4">');
108
+ });
109
+
110
+ it("should serialize <track> with attributes without closing tag", () => {
111
+ const doc = parseHTML('<html><body><video><track kind="subtitles" src="subs.vtt" srclang="en"></video></body></html>');
112
+ const track = doc.querySelector("track");
113
+ expect(track).not.toBeNull();
114
+ expect(track!.outerHTML).toBe('<track kind="subtitles" src="subs.vtt" srclang="en">');
115
+ });
116
+
117
+ it("should serialize <area> with attributes without closing tag", () => {
118
+ const doc = parseHTML('<html><body><map name="test"><area shape="rect" coords="0,0,100,100" href="link.html"></map></body></html>');
119
+ const area = doc.querySelector("area");
120
+ expect(area).not.toBeNull();
121
+ expect(area!.outerHTML).toBe('<area shape="rect" coords="0,0,100,100" href="link.html">');
122
+ });
123
+ });
124
+
125
+ describe("All void elements - comprehensive test", () => {
126
+ VOID_ELEMENTS.forEach((tagName) => {
127
+ it(`should serialize <${tagName}> without closing tag`, () => {
128
+ const doc = parseHTML(`<html><body><${tagName}></body></html>`);
129
+ const element = doc.querySelector(tagName);
130
+ expect(element).not.toBeNull();
131
+ expect(element!.outerHTML).toBe(`<${tagName}>`);
132
+ expect(element!.outerHTML).not.toContain(`</${tagName}>`);
133
+ });
134
+ });
135
+ });
136
+
137
+ describe("Multiple void elements in same document", () => {
138
+ it("should serialize multiple void elements correctly", () => {
139
+ const doc = parseHTML('<html><body><img src="test.jpg"><br><input type="text"></body></html>');
140
+
141
+ const img = doc.querySelector("img");
142
+ const br = doc.querySelector("br");
143
+ const input = doc.querySelector("input");
144
+
145
+ expect(img!.outerHTML).toBe('<img src="test.jpg">');
146
+ expect(br!.outerHTML).toBe("<br>");
147
+ expect(input!.outerHTML).toBe('<input type="text">');
148
+ });
149
+
150
+ it("should serialize document with multiple void elements without closing tags", () => {
151
+ const html = '<html><body><img src="test.jpg"><br><input type="text"></body></html>';
152
+ const doc = parseHTML(html);
153
+ const outerHTML = doc.documentElement.outerHTML;
154
+
155
+ expect(outerHTML).not.toContain("</img>");
156
+ expect(outerHTML).not.toContain("</br>");
157
+ expect(outerHTML).not.toContain("</input>");
158
+ });
159
+ });
160
+
161
+ describe("Void elements in head section", () => {
162
+ it("should serialize head void elements without closing tags", () => {
163
+ const html = `<html>
164
+ <head>
165
+ <meta charset="utf-8">
166
+ <meta name="viewport" content="width=device-width">
167
+ <link rel="stylesheet" href="style.css">
168
+ <base href="https://example.com/">
169
+ </head>
170
+ <body></body>
171
+ </html>`;
172
+ const doc = parseHTML(html);
173
+
174
+ const metas = doc.querySelectorAll("meta");
175
+ const link = doc.querySelector("link");
176
+ const base = doc.querySelector("base");
177
+
178
+ metas.forEach((meta: any) => {
179
+ expect(meta.outerHTML).not.toContain("</meta>");
180
+ });
181
+ expect(link!.outerHTML).not.toContain("</link>");
182
+ expect(base!.outerHTML).not.toContain("</base>");
183
+ });
184
+ });
185
+
186
+ describe("Void elements created with createElement", () => {
187
+ it("should serialize dynamically created <img> without closing tag", () => {
188
+ const doc = parseHTML("<html><body></body></html>");
189
+ const img = doc.createElement("img");
190
+ img.setAttribute("src", "dynamic.jpg");
191
+ expect(img.outerHTML).toBe('<img src="dynamic.jpg">');
192
+ });
193
+
194
+ it("should serialize dynamically created <br> without closing tag", () => {
195
+ const doc = parseHTML("<html><body></body></html>");
196
+ const br = doc.createElement("br");
197
+ expect(br.outerHTML).toBe("<br>");
198
+ });
199
+
200
+ it("should serialize dynamically created <input> without closing tag", () => {
201
+ const doc = parseHTML("<html><body></body></html>");
202
+ const input = doc.createElement("input");
203
+ input.setAttribute("type", "password");
204
+ input.setAttribute("name", "secret");
205
+ expect(input.outerHTML).toBe('<input type="password" name="secret">');
206
+ });
207
+
208
+ it("should serialize dynamically created <meta> without closing tag", () => {
209
+ const doc = parseHTML("<html><body></body></html>");
210
+ const meta = doc.createElement("meta");
211
+ meta.setAttribute("name", "description");
212
+ meta.setAttribute("content", "Test page");
213
+ expect(meta.outerHTML).toBe('<meta name="description" content="Test page">');
214
+ });
215
+
216
+ it("should serialize dynamically created <hr> without closing tag", () => {
217
+ const doc = parseHTML("<html><body></body></html>");
218
+ const hr = doc.createElement("hr");
219
+ expect(hr.outerHTML).toBe("<hr>");
220
+ });
221
+
222
+ VOID_ELEMENTS.forEach((tagName) => {
223
+ it(`should serialize dynamically created <${tagName}> without closing tag`, () => {
224
+ const doc = parseHTML("<html><body></body></html>");
225
+ const element = doc.createElement(tagName);
226
+ expect(element.outerHTML).toBe(`<${tagName}>`);
227
+ expect(element.outerHTML).not.toContain(`</${tagName}>`);
228
+ });
229
+ });
230
+ });
231
+
232
+ describe("Void elements with XHTML-style syntax", () => {
233
+ it("should handle <br /> and serialize without closing tag", () => {
234
+ const doc = parseHTML("<html><body><br /></body></html>");
235
+ const br = doc.querySelector("br");
236
+ expect(br).not.toBeNull();
237
+ expect(br!.outerHTML).toBe("<br>");
238
+ expect(br!.outerHTML).not.toContain("</br>");
239
+ });
240
+
241
+ it("should handle <img /> and serialize without closing tag", () => {
242
+ const doc = parseHTML('<html><body><img src="test.jpg" /></body></html>');
243
+ const img = doc.querySelector("img");
244
+ expect(img).not.toBeNull();
245
+ expect(img!.outerHTML).toBe('<img src="test.jpg">');
246
+ expect(img!.outerHTML).not.toContain("</img>");
247
+ });
248
+
249
+ it("should handle <input /> and serialize without closing tag", () => {
250
+ const doc = parseHTML('<html><body><input type="text" /></body></html>');
251
+ const input = doc.querySelector("input");
252
+ expect(input).not.toBeNull();
253
+ expect(input!.outerHTML).toBe('<input type="text">');
254
+ expect(input!.outerHTML).not.toContain("</input>");
255
+ });
256
+ });
257
+
258
+ describe("Non-void elements should have closing tags", () => {
259
+ it("should serialize <div> with closing tag", () => {
260
+ const doc = parseHTML("<html><body><div></div></body></html>");
261
+ const div = doc.querySelector("div");
262
+ expect(div).not.toBeNull();
263
+ expect(div!.outerHTML).toBe("<div></div>");
264
+ });
265
+
266
+ it("should serialize <span> with closing tag", () => {
267
+ const doc = parseHTML("<html><body><span></span></body></html>");
268
+ const span = doc.querySelector("span");
269
+ expect(span).not.toBeNull();
270
+ expect(span!.outerHTML).toBe("<span></span>");
271
+ });
272
+
273
+ it("should serialize <p> with closing tag", () => {
274
+ const doc = parseHTML("<html><body><p></p></body></html>");
275
+ const p = doc.querySelector("p");
276
+ expect(p).not.toBeNull();
277
+ expect(p!.outerHTML).toBe("<p></p>");
278
+ });
279
+
280
+ it("should serialize <script> with closing tag", () => {
281
+ const doc = parseHTML("<html><body><script></script></body></html>");
282
+ const script = doc.querySelector("script");
283
+ expect(script).not.toBeNull();
284
+ expect(script!.outerHTML).toBe("<script></script>");
285
+ });
286
+
287
+ it("should serialize <style> with closing tag", () => {
288
+ const doc = parseHTML("<html><head><style></style></head><body></body></html>");
289
+ const style = doc.querySelector("style");
290
+ expect(style).not.toBeNull();
291
+ expect(style!.outerHTML).toBe("<style></style>");
292
+ });
293
+
294
+ it("should serialize <iframe> with closing tag", () => {
295
+ const doc = parseHTML('<html><body><iframe src="page.html"></iframe></body></html>');
296
+ const iframe = doc.querySelector("iframe");
297
+ expect(iframe).not.toBeNull();
298
+ expect(iframe!.outerHTML).toBe('<iframe src="page.html"></iframe>');
299
+ });
300
+
301
+ it("should serialize <textarea> with closing tag", () => {
302
+ const doc = parseHTML("<html><body><textarea></textarea></body></html>");
303
+ const textarea = doc.querySelector("textarea");
304
+ expect(textarea).not.toBeNull();
305
+ expect(textarea!.outerHTML).toBe("<textarea></textarea>");
306
+ });
307
+
308
+ it("should serialize <video> with closing tag", () => {
309
+ const doc = parseHTML("<html><body><video></video></body></html>");
310
+ const video = doc.querySelector("video");
311
+ expect(video).not.toBeNull();
312
+ expect(video!.outerHTML).toBe("<video></video>");
313
+ });
314
+
315
+ it("should serialize <audio> with closing tag", () => {
316
+ const doc = parseHTML("<html><body><audio></audio></body></html>");
317
+ const audio = doc.querySelector("audio");
318
+ expect(audio).not.toBeNull();
319
+ expect(audio!.outerHTML).toBe("<audio></audio>");
320
+ });
321
+
322
+ it("should serialize <canvas> with closing tag", () => {
323
+ const doc = parseHTML("<html><body><canvas></canvas></body></html>");
324
+ const canvas = doc.querySelector("canvas");
325
+ expect(canvas).not.toBeNull();
326
+ expect(canvas!.outerHTML).toBe("<canvas></canvas>");
327
+ });
328
+ });
329
+
330
+ describe("Void elements with content (should be ignored)", () => {
331
+ it("should not include text content in void element", () => {
332
+ const doc = parseHTML("<html><body><br>text</body></html>");
333
+ const br = doc.querySelector("br");
334
+ expect(br).not.toBeNull();
335
+ expect(br!.outerHTML).toBe("<br>");
336
+ });
337
+
338
+ it("should not include innerHTML content in void element", () => {
339
+ const doc = parseHTML("<html><body><img src=\"test.jpg\"></body></html>");
340
+ const img = doc.querySelector("img");
341
+ expect(img).not.toBeNull();
342
+ expect(img!.innerHTML).toBe("");
343
+ expect(img!.outerHTML).toBe('<img src="test.jpg">');
344
+ });
345
+ });
346
+
347
+ describe("Void elements in nested structures", () => {
348
+ it("should serialize void elements inside multiple nested elements", () => {
349
+ const html = `<html><body>
350
+ <div class="container">
351
+ <form>
352
+ <div class="form-group">
353
+ <input type="text" name="field1">
354
+ <br>
355
+ <input type="password" name="field2">
356
+ </div>
357
+ </form>
358
+ </div>
359
+ </body></html>`;
360
+
361
+ const doc = parseHTML(html);
362
+ const inputs = doc.querySelectorAll("input");
363
+ const br = doc.querySelector("br");
364
+
365
+ expect(inputs.length).toBe(2);
366
+ inputs.forEach((input: any) => {
367
+ expect(input.outerHTML).not.toContain("</input>");
368
+ });
369
+ expect(br!.outerHTML).toBe("<br>");
370
+ });
371
+
372
+ it("should serialize void elements inside tables correctly", () => {
373
+ const html = `<html><body>
374
+ <table>
375
+ <colgroup>
376
+ <col span="1" class="col1">
377
+ <col span="2" class="col2">
378
+ </colgroup>
379
+ <tr><td><img src="icon.png"></td></tr>
380
+ </table>
381
+ </body></html>`;
382
+
383
+ const doc = parseHTML(html);
384
+ const cols = doc.querySelectorAll("col");
385
+ const img = doc.querySelector("img");
386
+
387
+ expect(cols.length).toBe(2);
388
+ cols.forEach((col: any) => {
389
+ expect(col.outerHTML).not.toContain("</col>");
390
+ });
391
+ expect(img!.outerHTML).not.toContain("</img>");
392
+ });
393
+ });
394
+
395
+ describe("Edge cases", () => {
396
+ it("should handle void element with boolean attributes", () => {
397
+ const doc = parseHTML('<html><body><input type="checkbox" checked disabled></body></html>');
398
+ const input = doc.querySelector("input");
399
+ expect(input).not.toBeNull();
400
+ expect(input!.outerHTML).not.toContain("</input>");
401
+ });
402
+
403
+ it("should handle void element with empty attribute value", () => {
404
+ const doc = parseHTML('<html><body><input type="text" value=""></body></html>');
405
+ const input = doc.querySelector("input");
406
+ expect(input).not.toBeNull();
407
+ expect(input!.outerHTML).not.toContain("</input>");
408
+ });
409
+
410
+ it("should handle uppercase void element tag names", () => {
411
+ const doc = parseHTML("<html><body><BR><IMG SRC=\"test.jpg\"></body></html>");
412
+ const br = doc.querySelector("br");
413
+ const img = doc.querySelector("img");
414
+
415
+ expect(br).not.toBeNull();
416
+ expect(img).not.toBeNull();
417
+ expect(br!.outerHTML).not.toContain("</br>");
418
+ expect(br!.outerHTML).not.toContain("</BR>");
419
+ expect(img!.outerHTML).not.toContain("</img>");
420
+ expect(img!.outerHTML).not.toContain("</IMG>");
421
+ });
422
+
423
+ it("should handle mixed case void element tag names", () => {
424
+ const doc = parseHTML("<html><body><Br><ImG src=\"test.jpg\"></body></html>");
425
+ const br = doc.querySelector("br");
426
+ const img = doc.querySelector("img");
427
+
428
+ expect(br).not.toBeNull();
429
+ expect(img).not.toBeNull();
430
+ expect(br!.outerHTML.toLowerCase()).not.toContain("</br>");
431
+ expect(img!.outerHTML.toLowerCase()).not.toContain("</img>");
432
+ });
433
+ });
434
+
435
+ describe("Full document serialization", () => {
436
+ it("should serialize complete document without closing tags on void elements", () => {
437
+ const html = `<html>
438
+ <head>
439
+ <meta charset="utf-8">
440
+ <link rel="stylesheet" href="style.css">
441
+ </head>
442
+ <body>
443
+ <img src="logo.png" alt="Logo">
444
+ <hr>
445
+ <form>
446
+ <input type="text" name="username">
447
+ <br>
448
+ <input type="password" name="password">
449
+ </form>
450
+ </body>
451
+ </html>`;
452
+
453
+ const doc = parseHTML(html);
454
+ const fullHTML = doc.documentElement.outerHTML;
455
+
456
+ // Check no void elements have closing tags
457
+ expect(fullHTML).not.toContain("</meta>");
458
+ expect(fullHTML).not.toContain("</link>");
459
+ expect(fullHTML).not.toContain("</img>");
460
+ expect(fullHTML).not.toContain("</hr>");
461
+ expect(fullHTML).not.toContain("</input>");
462
+ expect(fullHTML).not.toContain("</br>");
463
+
464
+ // Check non-void elements still have closing tags
465
+ expect(fullHTML).toContain("</head>");
466
+ expect(fullHTML).toContain("</body>");
467
+ expect(fullHTML).toContain("</form>");
468
+ expect(fullHTML).toContain("</html>");
469
+ });
470
+ });
471
+ });