@tkeron/html-parser 0.1.0
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 +24 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/bun.lock +29 -0
- package/index.ts +18 -0
- package/package.json +25 -0
- package/src/css-selector.ts +172 -0
- package/src/dom-simulator.ts +592 -0
- package/src/dom-types.ts +78 -0
- package/src/parser.ts +355 -0
- package/src/tokenizer.ts +413 -0
- package/tests/advanced.test.ts +487 -0
- package/tests/api-integration.test.ts +114 -0
- package/tests/dom-extended.test.ts +173 -0
- package/tests/dom.test.ts +482 -0
- package/tests/google-dom.test.ts +118 -0
- package/tests/google-homepage.txt +13 -0
- package/tests/official/README.md +87 -0
- package/tests/official/acid/acid-tests.test.ts +309 -0
- package/tests/official/final-output/final-output.test.ts +361 -0
- package/tests/official/html5lib/tokenizer-utils.ts +204 -0
- package/tests/official/html5lib/tokenizer.test.ts +184 -0
- package/tests/official/html5lib/tree-construction-utils.ts +208 -0
- package/tests/official/html5lib/tree-construction.test.ts +250 -0
- package/tests/official/validator/validator-tests.test.ts +237 -0
- package/tests/official/validator-nu/validator-nu.test.ts +335 -0
- package/tests/official/whatwg/whatwg-tests.test.ts +205 -0
- package/tests/official/wpt/wpt-tests.test.ts +409 -0
- package/tests/parser.test.ts +642 -0
- package/tests/selectors.test.ts +65 -0
- package/tests/test-page-0.txt +362 -0
- package/tests/tokenizer.test.ts +666 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { parseHTML } from '../index';
|
|
3
|
+
import {
|
|
4
|
+
setInnerHTML
|
|
5
|
+
} from '../src/dom-simulator';
|
|
6
|
+
|
|
7
|
+
describe('DOM Extended Functionality', () => {
|
|
8
|
+
describe('innerHTML and outerHTML', () => {
|
|
9
|
+
it('should generate correct innerHTML for simple elements', () => {
|
|
10
|
+
const doc = parseHTML('<div>Hello World</div>') as Document;
|
|
11
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
12
|
+
|
|
13
|
+
expect(div.innerHTML).toBe('Hello World');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should generate correct innerHTML for nested elements', () => {
|
|
17
|
+
const doc = parseHTML('<div><p>Hello</p><span>World</span></div>') as Document;
|
|
18
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
19
|
+
|
|
20
|
+
expect(div.innerHTML).toBe('<p>Hello</p><span>World</span>');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should generate correct outerHTML for elements', () => {
|
|
24
|
+
const doc = parseHTML('<div class="test">Hello</div>') as Document;
|
|
25
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
26
|
+
|
|
27
|
+
expect(div.outerHTML).toBe('<div class="test">Hello</div>');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should generate correct outerHTML for elements with multiple attributes', () => {
|
|
31
|
+
const doc = parseHTML('<input type="text" name="username" value="test">') as Document;
|
|
32
|
+
const input = doc.childNodes[0] as HTMLElement;
|
|
33
|
+
|
|
34
|
+
expect(input.outerHTML).toContain('type="text"');
|
|
35
|
+
expect(input.outerHTML).toContain('name="username"');
|
|
36
|
+
expect(input.outerHTML).toContain('value="test"');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle comments in innerHTML', () => {
|
|
40
|
+
const doc = parseHTML('<div><!-- comment -->text</div>') as Document;
|
|
41
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
42
|
+
|
|
43
|
+
expect(div.innerHTML).toBe('<!-- comment -->text');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('textContent property', () => {
|
|
48
|
+
it('should provide textContent on elements', () => {
|
|
49
|
+
const doc = parseHTML('<div>Hello <span>World</span></div>') as Document;
|
|
50
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
51
|
+
|
|
52
|
+
expect(div.textContent).toBe('Hello World');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should provide textContent for deeply nested elements', () => {
|
|
56
|
+
const doc = parseHTML('<div><p><em>Hello</em> <strong>Beautiful</strong></p> <span>World</span></div>') as Document;
|
|
57
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
58
|
+
|
|
59
|
+
expect(div.textContent).toBe('Hello Beautiful World');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should ignore comments in textContent', () => {
|
|
63
|
+
const doc = parseHTML('<div>Hello <!-- comment --> World</div>') as Document;
|
|
64
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
65
|
+
|
|
66
|
+
expect(div.textContent).toBe('Hello World');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('element navigation properties', () => {
|
|
71
|
+
it('should provide parentElement property', () => {
|
|
72
|
+
const doc = parseHTML('<div><p>Hello</p></div>') as Document;
|
|
73
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
74
|
+
const p = div.children[0];
|
|
75
|
+
|
|
76
|
+
expect(p).toBeDefined();
|
|
77
|
+
expect(p?.parentElement).toBe(div);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should provide firstElementChild and lastElementChild', () => {
|
|
81
|
+
const doc = parseHTML('<div><span>First</span><p>Second</p><em>Last</em></div>') as Document;
|
|
82
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
83
|
+
|
|
84
|
+
expect(div.firstElementChild?.tagName).toBe('SPAN');
|
|
85
|
+
expect(div.lastElementChild?.tagName).toBe('EM');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should provide nextElementSibling and previousElementSibling', () => {
|
|
89
|
+
const doc = parseHTML('<div><span>First</span><p>Second</p><em>Last</em></div>') as Document;
|
|
90
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
91
|
+
const span = div.children[0];
|
|
92
|
+
const p = div.children[1];
|
|
93
|
+
const em = div.children[2];
|
|
94
|
+
|
|
95
|
+
expect(span).toBeDefined();
|
|
96
|
+
expect(p).toBeDefined();
|
|
97
|
+
expect(em).toBeDefined();
|
|
98
|
+
|
|
99
|
+
if (span && p && em) {
|
|
100
|
+
expect(span.nextElementSibling).toBe(p);
|
|
101
|
+
expect(p.previousElementSibling).toBe(span);
|
|
102
|
+
expect(p.nextElementSibling).toBe(em);
|
|
103
|
+
expect(em.previousElementSibling).toBe(p);
|
|
104
|
+
|
|
105
|
+
expect(span.previousElementSibling).toBeNull();
|
|
106
|
+
expect(em.nextElementSibling).toBeNull();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('setInnerHTML functionality', () => {
|
|
112
|
+
it('should clear existing content when setting innerHTML', () => {
|
|
113
|
+
const doc = parseHTML('<div><p>Old content</p></div>') as Document;
|
|
114
|
+
const div = doc.childNodes[0] as HTMLElement;
|
|
115
|
+
|
|
116
|
+
setInnerHTML(div, 'New content');
|
|
117
|
+
|
|
118
|
+
expect(div.innerHTML).toBe('New content');
|
|
119
|
+
expect(div.children.length).toBe(0);
|
|
120
|
+
expect(div.childNodes.length).toBe(1);
|
|
121
|
+
expect(div.childNodes[0]?.nodeType).toBe(3);
|
|
122
|
+
expect(div.childNodes[0]?.textContent).toBe('New content');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Document body property type validation', () => {
|
|
127
|
+
it('should have body property with HTMLElement type', () => {
|
|
128
|
+
const doc = parseHTML('<html><body><p>Content</p></body></html>') as Document;
|
|
129
|
+
|
|
130
|
+
expect(doc.body).toBeTruthy();
|
|
131
|
+
expect(doc.body?.tagName).toBe('BODY');
|
|
132
|
+
expect(doc.body?.innerHTML).toBe('<p>Content</p>');
|
|
133
|
+
expect(doc.body?.textContent).toBe('Content');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should have head property with HTMLElement type', () => {
|
|
137
|
+
const doc = parseHTML('<html><head><title>Test</title></head><body></body></html>') as Document;
|
|
138
|
+
|
|
139
|
+
expect(doc.head).toBeTruthy();
|
|
140
|
+
expect(doc.head?.tagName).toBe('HEAD');
|
|
141
|
+
expect(doc.head?.innerHTML).toBe('<title>Test</title>');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should have documentElement property with HTMLElement type', () => {
|
|
145
|
+
const doc = parseHTML('<html><head></head><body></body></html>') as Document;
|
|
146
|
+
|
|
147
|
+
expect(doc.documentElement).toBeTruthy();
|
|
148
|
+
expect(doc.documentElement?.tagName).toBe('HTML');
|
|
149
|
+
expect(doc.documentElement?.children.length).toBe(2);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('DOM mutation and manipulation', () => {
|
|
154
|
+
it("should append an element and update innerHTML accordingly", () => {
|
|
155
|
+
const doc = parseHTML('<html><head></head><body></body></html>');
|
|
156
|
+
|
|
157
|
+
const body = doc.querySelector("body");
|
|
158
|
+
|
|
159
|
+
const h1 = doc.createElement("h1");
|
|
160
|
+
|
|
161
|
+
h1.textContent = "Hello World";
|
|
162
|
+
|
|
163
|
+
body?.appendChild(h1);
|
|
164
|
+
|
|
165
|
+
const innerHTML = body?.innerHTML;
|
|
166
|
+
|
|
167
|
+
expect(innerHTML).toBe('<h1>Hello World</h1>');
|
|
168
|
+
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
});
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { parseHTML } from "../index";
|
|
3
|
+
import {
|
|
4
|
+
NodeType,
|
|
5
|
+
getTextContent,
|
|
6
|
+
getAttribute,
|
|
7
|
+
hasAttribute,
|
|
8
|
+
setAttribute,
|
|
9
|
+
removeAttribute,
|
|
10
|
+
} from "../src/dom-simulator";
|
|
11
|
+
import { parse } from "../src/parser";
|
|
12
|
+
|
|
13
|
+
describe("DOM Simulator - Phase 1: Structure and Conversion", () => {
|
|
14
|
+
describe("parseHTML basic functionality", () => {
|
|
15
|
+
it("should return a Document object", () => {
|
|
16
|
+
const doc = parseHTML("<p>Hello</p>");
|
|
17
|
+
expect(doc.nodeType).toBe(NodeType.DOCUMENT_NODE);
|
|
18
|
+
expect(doc.nodeName).toBe("#document");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should parse simple HTML elements", () => {
|
|
22
|
+
const doc = parseHTML("<p>Hello World</p>");
|
|
23
|
+
|
|
24
|
+
expect(doc.childNodes.length).toBe(1);
|
|
25
|
+
const paragraph = doc.childNodes[0]!;
|
|
26
|
+
|
|
27
|
+
expect(paragraph.nodeType).toBe(NodeType.ELEMENT_NODE);
|
|
28
|
+
expect(paragraph.nodeName).toBe("P");
|
|
29
|
+
expect((paragraph as any).tagName).toBe("P");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should parse text content correctly", () => {
|
|
33
|
+
const doc = parseHTML("<p>Hello World</p>");
|
|
34
|
+
const paragraph = doc.childNodes[0]!;
|
|
35
|
+
|
|
36
|
+
expect(paragraph.childNodes.length).toBe(1);
|
|
37
|
+
const textNode = paragraph.childNodes[0]!;
|
|
38
|
+
|
|
39
|
+
expect(textNode.nodeType).toBe(NodeType.TEXT_NODE);
|
|
40
|
+
expect(textNode.nodeName).toBe("#text");
|
|
41
|
+
expect(textNode.nodeValue).toBe("Hello World");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should parse nested elements", () => {
|
|
45
|
+
const doc = parseHTML("<div><p>Hello</p><span>World</span></div>");
|
|
46
|
+
|
|
47
|
+
const div = doc.childNodes[0]!;
|
|
48
|
+
expect(div.nodeName).toBe("DIV");
|
|
49
|
+
expect(div.childNodes.length).toBe(2);
|
|
50
|
+
|
|
51
|
+
const p = div.childNodes[0]!;
|
|
52
|
+
const span = div.childNodes[1]!;
|
|
53
|
+
|
|
54
|
+
expect(p.nodeName).toBe("P");
|
|
55
|
+
expect(span.nodeName).toBe("SPAN");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should handle attributes correctly", () => {
|
|
59
|
+
const doc = parseHTML('<p id="test" class="highlight">Content</p>');
|
|
60
|
+
const paragraph = doc.childNodes[0]! as any;
|
|
61
|
+
|
|
62
|
+
expect(paragraph.attributes.id).toBe("test");
|
|
63
|
+
expect(paragraph.attributes.class).toBe("highlight");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should parse comments", () => {
|
|
67
|
+
const doc = parseHTML("<!-- This is a comment --><p>Hello</p>");
|
|
68
|
+
|
|
69
|
+
expect(doc.childNodes.length).toBe(2);
|
|
70
|
+
const comment = doc.childNodes[0]!;
|
|
71
|
+
|
|
72
|
+
expect(comment.nodeType).toBe(NodeType.COMMENT_NODE);
|
|
73
|
+
expect(comment.nodeName).toBe("#comment");
|
|
74
|
+
expect(comment.nodeValue).toBe(" This is a comment ");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should set parent-child relationships correctly", () => {
|
|
78
|
+
const doc = parseHTML("<div><p>Hello</p></div>");
|
|
79
|
+
|
|
80
|
+
const div = doc.childNodes[0]!;
|
|
81
|
+
const p = div.childNodes[0]!;
|
|
82
|
+
|
|
83
|
+
expect(p.parentNode).toBe(<any>div);
|
|
84
|
+
expect(div.parentNode).toBe(doc);
|
|
85
|
+
expect(div.firstChild).toBe(p);
|
|
86
|
+
expect(div.lastChild).toBe(p);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should set sibling relationships correctly", () => {
|
|
90
|
+
const doc = parseHTML(
|
|
91
|
+
"<div><p>First</p><span>Second</span><em>Third</em></div>"
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const div = doc.childNodes[0]!;
|
|
95
|
+
const p = div.childNodes[0]!;
|
|
96
|
+
const span = div.childNodes[1]!;
|
|
97
|
+
const em = div.childNodes[2]!;
|
|
98
|
+
|
|
99
|
+
expect(p.nextSibling).toBe(span);
|
|
100
|
+
expect(span.previousSibling).toBe(p);
|
|
101
|
+
expect(span.nextSibling).toBe(em);
|
|
102
|
+
expect(em.previousSibling).toBe(span);
|
|
103
|
+
|
|
104
|
+
expect(p.previousSibling).toBeNull();
|
|
105
|
+
expect(em.nextSibling).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should handle self-closing elements", () => {
|
|
109
|
+
const doc = parseHTML("<p>Before<br/>After</p>");
|
|
110
|
+
|
|
111
|
+
const p = doc.childNodes[0]!;
|
|
112
|
+
expect(p.childNodes.length).toBe(3);
|
|
113
|
+
|
|
114
|
+
const br = p.childNodes[1]!;
|
|
115
|
+
expect(br.nodeName).toBe("BR");
|
|
116
|
+
expect(br.childNodes.length).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should handle empty elements", () => {
|
|
120
|
+
const doc = parseHTML("<div></div>");
|
|
121
|
+
|
|
122
|
+
const div = doc.childNodes[0]!;
|
|
123
|
+
expect(div.childNodes.length).toBe(0);
|
|
124
|
+
expect(div.firstChild).toBeNull();
|
|
125
|
+
expect(div.lastChild).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("Document special properties", () => {
|
|
130
|
+
it("should identify documentElement (html)", () => {
|
|
131
|
+
const doc = parseHTML("<html><head></head><body></body></html>");
|
|
132
|
+
|
|
133
|
+
expect(doc.documentElement).toBeTruthy();
|
|
134
|
+
expect((doc.documentElement as any)?.tagName).toBe("HTML");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should identify body element", () => {
|
|
138
|
+
const doc = parseHTML("<html><body><p>Content</p></body></html>");
|
|
139
|
+
|
|
140
|
+
expect(doc.body).toBeTruthy();
|
|
141
|
+
expect((doc.body as any)?.tagName).toBe("BODY");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should identify head element", () => {
|
|
145
|
+
const doc = parseHTML("<html><head><title>Test</title></head></html>");
|
|
146
|
+
|
|
147
|
+
expect(doc.head).toBeTruthy();
|
|
148
|
+
expect((doc.head as any)?.tagName).toBe("HEAD");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("DOM Simulator - Phase 2: Navigation and Attributes", () => {
|
|
154
|
+
describe("getTextContent", () => {
|
|
155
|
+
it("should get text content from a simple text node", () => {
|
|
156
|
+
const doc = parseHTML("<p>Hello World</p>");
|
|
157
|
+
const p = doc.childNodes[0]!;
|
|
158
|
+
const textNode = p.childNodes[0]!;
|
|
159
|
+
|
|
160
|
+
expect(getTextContent(textNode)).toBe("Hello World");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should get text content from an element with text", () => {
|
|
164
|
+
const doc = parseHTML("<p>Hello World</p>");
|
|
165
|
+
const p = doc.childNodes[0]!;
|
|
166
|
+
|
|
167
|
+
expect(getTextContent(p)).toBe("Hello World");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should get concatenated text from nested elements", () => {
|
|
171
|
+
const doc = parseHTML("<div>Hello <span>beautiful</span> world</div>");
|
|
172
|
+
const div = doc.childNodes[0]!;
|
|
173
|
+
|
|
174
|
+
expect(getTextContent(div)).toBe("Hello beautiful world");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should get text from deeply nested elements", () => {
|
|
178
|
+
const doc = parseHTML(
|
|
179
|
+
"<div>Start <p>Middle <em>Deep <strong>Deeper</strong></em></p> End</div>"
|
|
180
|
+
);
|
|
181
|
+
const div = doc.childNodes[0]!;
|
|
182
|
+
|
|
183
|
+
expect(getTextContent(div)).toBe("Start Middle Deep Deeper End");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should return empty string for elements with no text", () => {
|
|
187
|
+
const doc = parseHTML("<div></div>");
|
|
188
|
+
const div = doc.childNodes[0]!;
|
|
189
|
+
|
|
190
|
+
expect(getTextContent(div)).toBe("");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should ignore comments when getting text content", () => {
|
|
194
|
+
const doc = parseHTML("<div>Before<!-- comment -->After</div>");
|
|
195
|
+
const div = doc.childNodes[0]!;
|
|
196
|
+
|
|
197
|
+
expect(getTextContent(div)).toBe("BeforeAfter");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should handle mixed content with self-closing elements", () => {
|
|
201
|
+
const doc = parseHTML("<p>Before<br/>After</p>");
|
|
202
|
+
const p = doc.childNodes[0]!;
|
|
203
|
+
|
|
204
|
+
expect(getTextContent(p)).toBe("BeforeAfter");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("Attribute functions", () => {
|
|
209
|
+
it("should get existing attributes", () => {
|
|
210
|
+
const doc = parseHTML(
|
|
211
|
+
'<div id="test" class="highlight" data-value="123">Content</div>'
|
|
212
|
+
);
|
|
213
|
+
const div = doc.childNodes[0]! as any;
|
|
214
|
+
|
|
215
|
+
expect(getAttribute(div, "id")).toBe("test");
|
|
216
|
+
expect(getAttribute(div, "class")).toBe("highlight");
|
|
217
|
+
expect(getAttribute(div, "data-value")).toBe("123");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should return null for non-existing attributes", () => {
|
|
221
|
+
const doc = parseHTML('<div id="test">Content</div>');
|
|
222
|
+
const div = doc.childNodes[0]! as any;
|
|
223
|
+
|
|
224
|
+
expect(getAttribute(div, "nonexistent")).toBeNull();
|
|
225
|
+
expect(getAttribute(div, "class")).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should check if attributes exist", () => {
|
|
229
|
+
const doc = parseHTML('<div id="test" class="highlight">Content</div>');
|
|
230
|
+
const div = doc.childNodes[0]! as any;
|
|
231
|
+
|
|
232
|
+
expect(hasAttribute(div, "id")).toBe(true);
|
|
233
|
+
expect(hasAttribute(div, "class")).toBe(true);
|
|
234
|
+
expect(hasAttribute(div, "nonexistent")).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should set new attributes", () => {
|
|
238
|
+
const doc = parseHTML("<div>Content</div>");
|
|
239
|
+
const div = doc.childNodes[0]! as any;
|
|
240
|
+
|
|
241
|
+
setAttribute(div, "id", "new-id");
|
|
242
|
+
setAttribute(div, "class", "new-class");
|
|
243
|
+
|
|
244
|
+
expect(getAttribute(div, "id")).toBe("new-id");
|
|
245
|
+
expect(getAttribute(div, "class")).toBe("new-class");
|
|
246
|
+
expect(hasAttribute(div, "id")).toBe(true);
|
|
247
|
+
expect(hasAttribute(div, "class")).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should update existing attributes", () => {
|
|
251
|
+
const doc = parseHTML('<div id="old-id" class="old-class">Content</div>');
|
|
252
|
+
const div = doc.childNodes[0]! as any;
|
|
253
|
+
|
|
254
|
+
setAttribute(div, "id", "new-id");
|
|
255
|
+
setAttribute(div, "class", "new-class");
|
|
256
|
+
|
|
257
|
+
expect(getAttribute(div, "id")).toBe("new-id");
|
|
258
|
+
expect(getAttribute(div, "class")).toBe("new-class");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should remove attributes", () => {
|
|
262
|
+
const doc = parseHTML(
|
|
263
|
+
'<div id="test" class="highlight" data-value="123">Content</div>'
|
|
264
|
+
);
|
|
265
|
+
const div = doc.childNodes[0]! as any;
|
|
266
|
+
|
|
267
|
+
removeAttribute(div, "class");
|
|
268
|
+
removeAttribute(div, "data-value");
|
|
269
|
+
|
|
270
|
+
expect(getAttribute(div, "id")).toBe("test");
|
|
271
|
+
expect(getAttribute(div, "class")).toBeNull();
|
|
272
|
+
expect(getAttribute(div, "data-value")).toBeNull();
|
|
273
|
+
expect(hasAttribute(div, "class")).toBe(false);
|
|
274
|
+
expect(hasAttribute(div, "data-value")).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should handle removing non-existing attributes gracefully", () => {
|
|
278
|
+
const doc = parseHTML('<div id="test">Content</div>');
|
|
279
|
+
const div = doc.childNodes[0]! as any;
|
|
280
|
+
|
|
281
|
+
removeAttribute(div, "nonexistent");
|
|
282
|
+
|
|
283
|
+
expect(getAttribute(div, "id")).toBe("test");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("DOM extra tests", () => {
|
|
289
|
+
const smallDocument = `
|
|
290
|
+
<!DOCTYPE html>
|
|
291
|
+
<html lang="en">
|
|
292
|
+
<head>
|
|
293
|
+
<title>Sample Page</title>
|
|
294
|
+
<meta charset="UTF-8">
|
|
295
|
+
</head>
|
|
296
|
+
<body>
|
|
297
|
+
<header id="main-header" class="site-header">
|
|
298
|
+
<h1>Welcome</h1>
|
|
299
|
+
</header>
|
|
300
|
+
<main>
|
|
301
|
+
<section>
|
|
302
|
+
<p>First paragraph.</p>
|
|
303
|
+
<p>Second <strong>paragraph</strong> with <em>formatting</em>.</p>
|
|
304
|
+
</section>
|
|
305
|
+
<img src="image.jpg" alt="Sample Image">
|
|
306
|
+
<!-- Footer note -->
|
|
307
|
+
</main>
|
|
308
|
+
<footer>
|
|
309
|
+
<p>Contact: <a href="mailto:test@example.com">Email us</a></p>
|
|
310
|
+
</footer>
|
|
311
|
+
</body>
|
|
312
|
+
</html>
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
it("should parse a simple HTML document and perform common DOM operations", () => {
|
|
316
|
+
const doc = parseHTML(`<!DOCTYPE html>
|
|
317
|
+
<html>
|
|
318
|
+
<head>
|
|
319
|
+
<title>Test Document</title>
|
|
320
|
+
</head>
|
|
321
|
+
<body>
|
|
322
|
+
<h1>Welcome to the Test Document</h1>
|
|
323
|
+
<p>This is a paragraph in the test document.</p>
|
|
324
|
+
</body>
|
|
325
|
+
</html>`);
|
|
326
|
+
|
|
327
|
+
expect(doc.body).toHaveProperty("querySelector");
|
|
328
|
+
|
|
329
|
+
const h1 = doc.body?.querySelector("h1");
|
|
330
|
+
|
|
331
|
+
expect(h1).toBeTruthy();
|
|
332
|
+
expect(h1?.tagName).toBe("H1");
|
|
333
|
+
expect(h1?.textContent).toBe("Welcome to the Test Document");
|
|
334
|
+
expect(h1?.innerHTML).toBe("Welcome to the Test Document");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should correctly parse nested elements and maintain DOM structure", () => {
|
|
338
|
+
const doc = parseHTML(`<div><p><span>Hello</span> World</p></div>`);
|
|
339
|
+
const div = doc.body?.querySelector("div");
|
|
340
|
+
const p = div?.querySelector("p");
|
|
341
|
+
const span = p?.querySelector("span");
|
|
342
|
+
|
|
343
|
+
expect(div).toBeTruthy();
|
|
344
|
+
expect(p).toBeTruthy();
|
|
345
|
+
expect(span).toBeTruthy();
|
|
346
|
+
expect(span?.textContent).toBe("Hello");
|
|
347
|
+
expect(p?.textContent).toBe("Hello World");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should create a new Document", () => {
|
|
351
|
+
const doc = parseHTML();
|
|
352
|
+
expect(doc).toBeTruthy();
|
|
353
|
+
expect(doc.nodeType).toBe(NodeType.DOCUMENT_NODE);
|
|
354
|
+
expect(doc.nodeName).toBe("#document");
|
|
355
|
+
expect(doc.childNodes.length).toBe(0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should parse document structure and identify main elements", () => {
|
|
359
|
+
const doc = parseHTML(smallDocument);
|
|
360
|
+
|
|
361
|
+
expect(doc.nodeType).toBe(NodeType.DOCUMENT_NODE);
|
|
362
|
+
expect(doc.documentElement?.nodeName).toBe("HTML");
|
|
363
|
+
expect(doc.head?.nodeName).toBe("HEAD");
|
|
364
|
+
expect(doc.body?.nodeName).toBe("BODY");
|
|
365
|
+
|
|
366
|
+
expect(doc.head?.querySelector("title")?.textContent).toBe("Sample Page");
|
|
367
|
+
expect(doc.head?.querySelector("meta")?.getAttribute("charset")).toBe(
|
|
368
|
+
"UTF-8"
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("should query and navigate DOM elements correctly", () => {
|
|
373
|
+
const doc = parseHTML(smallDocument);
|
|
374
|
+
|
|
375
|
+
const header = doc.body?.querySelector("#main-header")!;
|
|
376
|
+
expect(header.nodeName).toBe("HEADER");
|
|
377
|
+
expect(header.getAttribute("class")).toBe("site-header");
|
|
378
|
+
expect(header.querySelector("h1")?.textContent).toBe("Welcome");
|
|
379
|
+
|
|
380
|
+
const section = doc.body?.querySelector("section")!;
|
|
381
|
+
const paragraphs = section.querySelectorAll("p");
|
|
382
|
+
expect(paragraphs.length).toBe(2);
|
|
383
|
+
expect(paragraphs[0]?.textContent).toBe("First paragraph.");
|
|
384
|
+
expect(paragraphs[1]?.textContent).toBe(
|
|
385
|
+
"Second paragraph with formatting."
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const strong = section.querySelector("strong")!;
|
|
389
|
+
expect(strong.textContent).toBe("paragraph");
|
|
390
|
+
expect(strong.parentNode?.nodeName).toBe("P");
|
|
391
|
+
|
|
392
|
+
const em = section.querySelector("em")!;
|
|
393
|
+
expect(em.textContent).toBe("formatting");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("should handle different node types and attributes", () => {
|
|
397
|
+
const doc = parseHTML(smallDocument);
|
|
398
|
+
|
|
399
|
+
const img = doc.body?.querySelector("img")!;
|
|
400
|
+
expect(img.nodeName).toBe("IMG");
|
|
401
|
+
expect(img.getAttribute("src")).toBe("image.jpg");
|
|
402
|
+
expect(img.getAttribute("alt")).toBe("Sample Image");
|
|
403
|
+
|
|
404
|
+
const main = doc.body?.querySelector("main")!;
|
|
405
|
+
const commentNode = (main.childNodes as any).find(
|
|
406
|
+
(n: any) => n.nodeType === NodeType.COMMENT_NODE
|
|
407
|
+
);
|
|
408
|
+
expect(commentNode).toBeTruthy();
|
|
409
|
+
expect(commentNode?.nodeValue?.trim()).toBe("Footer note");
|
|
410
|
+
|
|
411
|
+
const footerLink = doc.body?.querySelector("footer a")!;
|
|
412
|
+
expect(footerLink.getAttribute("href")).toBe("mailto:test@example.com");
|
|
413
|
+
expect(footerLink.textContent).toBe("Email us");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should support DOM manipulation and traversal operations", () => {
|
|
417
|
+
const doc = parseHTML(smallDocument);
|
|
418
|
+
const section = doc.body?.querySelector("section")!;
|
|
419
|
+
const paragraphs = section.querySelectorAll("p");
|
|
420
|
+
const header = doc.body?.querySelector("#main-header")!;
|
|
421
|
+
|
|
422
|
+
const clonedFooter = (doc.body?.querySelector("footer") as any).cloneNode(
|
|
423
|
+
true
|
|
424
|
+
);
|
|
425
|
+
expect(clonedFooter.nodeName).toBe("FOOTER");
|
|
426
|
+
expect(clonedFooter.querySelector("a")?.textContent).toBe("Email us");
|
|
427
|
+
|
|
428
|
+
const bodyText = getTextContent(doc.body!);
|
|
429
|
+
expect(bodyText.includes("Welcome")).toBe(true);
|
|
430
|
+
expect(bodyText.includes("First paragraph.")).toBe(true);
|
|
431
|
+
expect(bodyText.includes("Second paragraph with formatting")).toBe(true);
|
|
432
|
+
expect(bodyText.includes("Email us")).toBe(true);
|
|
433
|
+
|
|
434
|
+
header.setAttribute("data-role", "banner");
|
|
435
|
+
expect(header.getAttribute("data-role")).toBe("banner");
|
|
436
|
+
header.removeAttribute("class");
|
|
437
|
+
expect(header.hasAttribute("class")).toBe(false);
|
|
438
|
+
|
|
439
|
+
const firstP = paragraphs[0]!;
|
|
440
|
+
const secondP = paragraphs[1]!;
|
|
441
|
+
|
|
442
|
+
expect(firstP.nextElementSibling === secondP).toBe(true);
|
|
443
|
+
expect(secondP.previousElementSibling === firstP).toBe(true);
|
|
444
|
+
|
|
445
|
+
expect(firstP.nextSibling?.nodeType).toBe(NodeType.TEXT_NODE);
|
|
446
|
+
expect(secondP.previousSibling?.nodeType).toBe(NodeType.TEXT_NODE);
|
|
447
|
+
|
|
448
|
+
const compareNodes = (node1: any, node2: any) => {
|
|
449
|
+
if (!node1 || !node2) return node1 === node2;
|
|
450
|
+
return (
|
|
451
|
+
node1.nodeName === node2.nodeName &&
|
|
452
|
+
node1.nodeType === node2.nodeType &&
|
|
453
|
+
node1.textContent === node2.textContent
|
|
454
|
+
);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
expect(compareNodes(firstP.nextElementSibling, secondP)).toBe(true);
|
|
458
|
+
expect(compareNodes(secondP.previousElementSibling, firstP)).toBe(true);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should support dynamic HTML injection and DOM manipulation workflows", () => {
|
|
462
|
+
const doc = parseHTML("<div id='container'></div>");
|
|
463
|
+
const container = doc.getElementById("container")!;
|
|
464
|
+
|
|
465
|
+
expect(container).toBeTruthy();
|
|
466
|
+
|
|
467
|
+
container.innerHTML = `
|
|
468
|
+
<h2>Dynamic Content</h2>
|
|
469
|
+
<p>This is a dynamically added paragraph.</p>
|
|
470
|
+
<ul>
|
|
471
|
+
<li>Item 1</li>
|
|
472
|
+
<li>Item 2</li>
|
|
473
|
+
</ul>
|
|
474
|
+
`;
|
|
475
|
+
|
|
476
|
+
expect(container.querySelector("h2")?.textContent).toBe("Dynamic Content");
|
|
477
|
+
expect(container.querySelector("p")?.textContent).toBe(
|
|
478
|
+
"This is a dynamically added paragraph."
|
|
479
|
+
);
|
|
480
|
+
expect(container.querySelectorAll("li").length).toBe(2);
|
|
481
|
+
});
|
|
482
|
+
});
|