Coverage for tdom / parser_test.py: 96%
143 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-17 23:32 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-17 23:32 +0000
1import pytest
3from .parser import TemplateParser
4from .placeholders import TemplateRef
5from .tnodes import (
6 TComment,
7 TComponent,
8 TDocumentType,
9 TElement,
10 TFragment,
11 TInterpolatedAttribute,
12 TLiteralAttribute,
13 TSpreadAttribute,
14 TTemplatedAttribute,
15 TText,
16)
19def test_parse_empty():
20 node = TemplateParser.parse(t"")
21 assert node == TFragment()
24def test_parse_text():
25 node = TemplateParser.parse(t"Hello, world!")
26 assert node == TText.literal("Hello, world!")
29def test_parse_text_with_entities():
30 node = TemplateParser.parse(t"Panini's")
31 assert node == TText.literal("Panini's")
34def test_parse_void_element():
35 node = TemplateParser.parse(t"<br>")
36 assert node == TElement("br")
39def test_parse_void_element_self_closed():
40 node = TemplateParser.parse(t"<br />")
41 assert node == TElement("br")
44def test_parse_uppercase_void_element():
45 node = TemplateParser.parse(t"<BR>")
46 assert node == TElement("br")
49def test_parse_standard_element_with_text():
50 node = TemplateParser.parse(t"<div>Hello, world!</div>")
51 assert node == TElement("div", children=(TText.literal("Hello, world!"),))
54def test_parse_nested_elements():
55 node = TemplateParser.parse(t"<div><span>Nested</span> content</div>")
56 assert node == TElement(
57 "div",
58 children=(
59 TElement("span", children=(TText.literal("Nested"),)),
60 TText.literal(" content"),
61 ),
62 )
65def test_parse_element_with_attributes():
66 node = TemplateParser.parse(
67 t'<a href="https://example.com" target="_blank">Link</a>'
68 )
69 assert node == TElement(
70 "a",
71 attrs=(
72 TLiteralAttribute("href", "https://example.com"),
73 TLiteralAttribute("target", "_blank"),
74 ),
75 children=(TText.literal("Link"),),
76 )
79def test_parse_element_attribute_order():
80 node = TemplateParser.parse(t'<a title="a" href="b" title="c"></a>')
81 assert isinstance(node, TElement)
82 assert node.attrs == (
83 TLiteralAttribute("title", "a"),
84 TLiteralAttribute("href", "b"),
85 TLiteralAttribute("title", "c"),
86 )
89def test_parse_comment():
90 node = TemplateParser.parse(t"<!-- This is a comment -->")
91 assert node == TComment.literal(" This is a comment ")
94def test_parse_doctype():
95 node = TemplateParser.parse(t"<!DOCTYPE html>")
96 assert node == TDocumentType("html")
99def test_parse_multiple_voids():
100 node = TemplateParser.parse(t"<br><hr><hr /><hr /><br /><br><br>")
101 assert node == TFragment(
102 children=(
103 TElement("br"),
104 TElement("hr"),
105 TElement("hr"),
106 TElement("hr"),
107 TElement("br"),
108 TElement("br"),
109 TElement("br"),
110 )
111 )
114def test_parse_mixed_content():
115 node = TemplateParser.parse(
116 t'<!DOCTYPE html><!-- Comment --><div class="container">'
117 t"Hello, <br class='funky' />world <!-- neato -->!</div>"
118 )
119 assert node == TFragment(
120 children=(
121 TDocumentType("html"),
122 TComment.literal(" Comment "),
123 TElement(
124 "div",
125 attrs=(TLiteralAttribute("class", "container"),),
126 children=(
127 TText.literal("Hello, "),
128 TElement("br", attrs=(TLiteralAttribute("class", "funky"),)),
129 TText.literal("world "),
130 TComment.literal(" neato "),
131 TText.literal("!"),
132 ),
133 ),
134 )
135 )
138def test_parse_entities_are_escaped():
139 node = TemplateParser.parse(t"<p></p></p>")
140 assert node == TElement(
141 "p",
142 children=(TText.literal("</p>"),),
143 )
146def test_parse_script_tag_content():
147 node = TemplateParser.parse(
148 t"<script>if (a < b && c > d) { alert('wow'); } </script>"
149 )
150 assert node == TElement(
151 "script",
152 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),),
153 )
156def test_parse_script_with_entities():
157 # The <script> tag (and <style>) tag uses the CDATA content model.
158 node = TemplateParser.parse(t"<script>var x = 'a & b';</script>")
159 assert node == TElement(
160 "script",
161 children=(TText.literal("var x = 'a & b';"),),
162 )
165def test_parse_textarea_tag_content():
166 node = TemplateParser.parse(
167 t"<textarea>if (a < b && c > d) { alert('wow'); } </textarea>"
168 )
169 assert node == TElement(
170 "textarea",
171 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),),
172 )
175def test_parse_textarea_with_entities():
176 # The <textarea> (and <title>) tag uses the RCDATA content model.
177 node = TemplateParser.parse(t"<textarea>var x = 'a & b';</textarea>")
178 assert node == TElement(
179 "textarea",
180 children=(TText.literal("var x = 'a & b';"),),
181 )
184def test_parse_title_unusual():
185 node = TemplateParser.parse(t"<title>My & Awesome <Site></title>")
186 assert node == TElement(
187 "title",
188 children=(TText.literal("My & Awesome <Site>"),),
189 )
192def test_parse_mismatched_tags():
193 with pytest.raises(ValueError):
194 _ = TemplateParser.parse(t"<div><span>Mismatched</div></span>")
197def test_parse_unclosed_tag():
198 with pytest.raises(ValueError):
199 _ = TemplateParser.parse(t"<div>Unclosed")
202def test_parse_unexpected_closing_tag():
203 with pytest.raises(ValueError):
204 _ = TemplateParser.parse(t"Unopened</div>")
207def test_self_closing_tags():
208 node = TemplateParser.parse(t"<div/><p></p>")
209 assert node == TFragment(
210 children=(
211 TElement("div"),
212 TElement("p"),
213 )
214 )
217def test_nested_self_closing_tags():
218 node = TemplateParser.parse(t"<div><br><div /><br></div>")
219 assert node == TElement(
220 "div", children=(TElement("br"), TElement("div"), TElement("br"))
221 )
222 node = TemplateParser.parse(t"<div><div /></div>")
223 assert node == TElement("div", children=(TElement("div"),))
226def test_self_closing_tags_unexpected_closing_tag():
227 with pytest.raises(ValueError):
228 _ = TemplateParser.parse(t"<div /></div>")
231def test_self_closing_void_tags_unexpected_closing_tag():
232 with pytest.raises(ValueError):
233 _ = TemplateParser.parse(t"<input /></input>")
236def test_literal_attributes():
237 node = TemplateParser.parse(t'<input type="text" disabled />')
238 assert node == TElement(
239 "input",
240 attrs=(
241 TLiteralAttribute("type", "text"),
242 TLiteralAttribute("disabled", None),
243 ),
244 )
247def test_interpolated_attributes():
248 value1 = 42
249 value2 = 99
250 node = TemplateParser.parse(t'<div value1="{value1}" value2={value2} />')
251 assert node == TElement(
252 "div",
253 attrs=(
254 TInterpolatedAttribute("value1", 0),
255 TInterpolatedAttribute("value2", 1),
256 ),
257 children=(),
258 )
261def test_templated_attributes():
262 value1 = 42
263 value2 = 99
264 node = TemplateParser.parse(
265 t'<div value1="{value1}-burrito" value2="neato-{value2}-wow" />'
266 )
267 value1_ref = TemplateRef(strings=("", "-burrito"), i_indexes=(0,))
268 value2_ref = TemplateRef(strings=("neato-", "-wow"), i_indexes=(1,))
269 assert node == TElement(
270 "div",
271 attrs=(
272 TTemplatedAttribute("value1", value1_ref),
273 TTemplatedAttribute("value2", value2_ref),
274 ),
275 children=(),
276 )
279def test_templated_attribute_name_error():
280 with pytest.raises(ValueError):
281 attr_name = "some-attr"
282 _ = TemplateParser.parse(t'<div {attr_name}="value" />')
285def test_templated_attribute_name_and_value_error():
286 with pytest.raises(ValueError):
287 attr_name = "some-attr"
288 value = "value"
289 _ = TemplateParser.parse(t'<div {attr_name}="{value}" />')
292def test_spread_attribute():
293 props = "doesnt-matter-the-type"
294 node = TemplateParser.parse(t"<div {props} />")
295 assert node == TElement(
296 "div",
297 attrs=(TSpreadAttribute(i_index=0),),
298 children=(),
299 )
302def test_component_element_self_closing():
303 def Component():
304 pass
306 node = TemplateParser.parse(t"<{Component} />")
307 assert node == TComponent(start_i_index=0)
310def test_component_element_with_closing_tag():
311 def Component():
312 pass
314 node = TemplateParser.parse(t"<{Component}></{Component}>")
315 assert node == TComponent(start_i_index=0, end_i_index=1)
318def test_component_element_special_case_mismatched_closing_tag_still_parses():
319 def Component1():
320 pass
322 def Component2():
323 pass
325 node = TemplateParser.parse(t"<{Component1}></{Component2}>")
326 assert node == TComponent(start_i_index=0, end_i_index=1)
329def test_component_element_invalid_closing_tag():
330 def Component():
331 pass
333 with pytest.raises(ValueError):
334 _ = TemplateParser.parse(t"<{Component}></div>")
337def test_component_element_invalid_opening_tag():
338 def Component():
339 pass
341 with pytest.raises(ValueError):
342 _ = TemplateParser.parse(t"<div></{Component}>")