Coverage for tdom / parser_test.py: 95%
195 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-12 16:43 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-12 16:43 +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_mixed_literal_content():
20 node = TemplateParser.parse(
21 t"<!DOCTYPE html>"
22 t"<!-- Comment -->"
23 t'<div class="container">'
24 t"Hello, <br class='funky' />world <!-- neato -->!"
25 t"</div>"
26 )
27 assert node == TFragment(
28 children=(
29 TDocumentType("html"),
30 TComment.literal(" Comment "),
31 TElement(
32 "div",
33 attrs=(TLiteralAttribute("class", "container"),),
34 children=(
35 TText.literal("Hello, "),
36 TElement("br", attrs=(TLiteralAttribute("class", "funky"),)),
37 TText.literal("world "),
38 TComment.literal(" neato "),
39 TText.literal("!"),
40 ),
41 ),
42 )
43 )
46#
47# Text
48#
49def test_parse_empty():
50 node = TemplateParser.parse(t"")
51 assert node == TFragment()
54def test_parse_text():
55 node = TemplateParser.parse(t"Hello, world!")
56 assert node == TText.literal("Hello, world!")
59def test_parse_text_multiline():
60 node = TemplateParser.parse(t"""Hello, world!
61 Hello, moon!
62Hello, sun!
63""")
64 assert node == TText.literal("""Hello, world!
65 Hello, moon!
66Hello, sun!
67""")
70def test_parse_text_with_entities():
71 node = TemplateParser.parse(t"a < b")
72 assert node == TText.literal("a < b")
75def test_parse_text_with_template_singleton():
76 greeting = "Hello, World!"
77 node = TemplateParser.parse(t"{greeting}")
78 assert node == TText(ref=TemplateRef(strings=("", ""), i_indexes=(0,)))
81def test_parse_text_with_template():
82 who = "World"
83 node = TemplateParser.parse(t"Hello, {who}!")
84 assert node == TText(ref=TemplateRef(strings=("Hello, ", "!"), i_indexes=(0,)))
87#
88# Elements
89#
90def test_parse_void_element():
91 node = TemplateParser.parse(t"<br>")
92 assert node == TElement("br")
95def test_parse_void_element_self_closed():
96 node = TemplateParser.parse(t"<br />")
97 assert node == TElement("br")
100def test_parse_uppercase_void_element():
101 node = TemplateParser.parse(t"<BR>")
102 assert node == TElement("br")
105def test_parse_standard_element_with_text():
106 node = TemplateParser.parse(t"<div>Hello, world!</div>")
107 assert node == TElement("div", children=(TText.literal("Hello, world!"),))
110def test_parse_nested_elements():
111 node = TemplateParser.parse(t"<div><span>Nested</span> content</div>")
112 assert node == TElement(
113 "div",
114 children=(
115 TElement("span", children=(TText.literal("Nested"),)),
116 TText.literal(" content"),
117 ),
118 )
121def test_parse_element_with_template():
122 who = "World"
123 node = TemplateParser.parse(t"<div>Hello, {who}!</div>")
124 assert node == TElement(
125 "div",
126 children=(TText(ref=TemplateRef(strings=("Hello, ", "!"), i_indexes=(0,))),),
127 )
130def test_parse_element_with_template_singleton():
131 greeting = "Hello, World!"
132 node = TemplateParser.parse(t"<div>{greeting}</div>")
133 assert node == TElement(
134 "div", children=(TText(ref=TemplateRef(strings=("", ""), i_indexes=(0,))),)
135 )
138def test_parse_multiple_voids():
139 node = TemplateParser.parse(t"<br><hr><hr /><hr /><br /><br><br>")
140 assert node == TFragment(
141 children=(
142 TElement("br"),
143 TElement("hr"),
144 TElement("hr"),
145 TElement("hr"),
146 TElement("br"),
147 TElement("br"),
148 TElement("br"),
149 )
150 )
153def test_parse_text_entities():
154 node = TemplateParser.parse(t"<p></p></p>")
155 assert node == TElement(
156 "p",
157 children=(TText.literal("</p>"),),
158 )
161def test_parse_script_tag_content():
162 node = TemplateParser.parse(
163 t"<script>if (a < b && c > d) { alert('wow'); } </script>"
164 )
165 assert node == TElement(
166 "script",
167 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),),
168 )
171def test_parse_script_with_entities():
172 # The <script> tag (and <style>) tag uses the CDATA content model.
173 node = TemplateParser.parse(t"<script>var x = 'a & b';</script>")
174 assert node == TElement(
175 "script",
176 children=(TText.literal("var x = 'a & b';"),),
177 ), "Entities SHOULD NOT be evaluated in scripts."
180def test_parse_textarea_tag_content():
181 node = TemplateParser.parse(
182 t"<textarea>if (a < b && c > d) { alert('wow'); } </textarea>"
183 )
184 assert node == TElement(
185 "textarea",
186 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),),
187 )
190def test_parse_textarea_with_entities():
191 # The <textarea> (and <title>) tag uses the RCDATA content model.
192 node = TemplateParser.parse(t"<textarea>var x = 'a & b';</textarea>")
193 assert node == TElement(
194 "textarea",
195 children=(TText.literal("var x = 'a & b';"),),
196 ), "Entities SHOULD be evaluated in textarea/title."
199def test_parse_title_unusual():
200 node = TemplateParser.parse(t"<title>My & Awesome <Site></title>")
201 assert node == TElement(
202 "title",
203 children=(TText.literal("My & Awesome <Site>"),),
204 )
207def test_parse_mismatched_tags():
208 with pytest.raises(ValueError):
209 _ = TemplateParser.parse(t"<div><span>Mismatched</div></span>")
212def test_parse_unclosed_tag():
213 with pytest.raises(ValueError):
214 _ = TemplateParser.parse(t"<div>Unclosed")
217def test_parse_unexpected_closing_tag():
218 with pytest.raises(ValueError):
219 _ = TemplateParser.parse(t"Unopened</div>")
222def test_self_closing_tags():
223 node = TemplateParser.parse(t"<div/><p></p>")
224 assert node == TFragment(
225 children=(
226 TElement("div"),
227 TElement("p"),
228 )
229 )
232def test_nested_self_closing_tags():
233 node = TemplateParser.parse(t"<div><br><div /><br></div>")
234 assert node == TElement(
235 "div", children=(TElement("br"), TElement("div"), TElement("br"))
236 )
237 node = TemplateParser.parse(t"<div><div /></div>")
238 assert node == TElement("div", children=(TElement("div"),))
241def test_self_closing_tags_unexpected_closing_tag():
242 with pytest.raises(ValueError):
243 _ = TemplateParser.parse(t"<div /></div>")
246def test_self_closing_void_tags_unexpected_closing_tag():
247 with pytest.raises(ValueError):
248 _ = TemplateParser.parse(t"<input /></input>")
251#
252# Attributes
253#
254def test_literal_attrs():
255 node = TemplateParser.parse(
256 (
257 t"<a"
258 t" id=example_link" # no quotes allowed without spaces
259 t" autofocus" # bare / boolean
260 t' title=""' # empty attribute
261 t' href="https://example.com" target="_blank"'
262 t">Link</a>"
263 )
264 )
265 assert node == TElement(
266 "a",
267 attrs=(
268 TLiteralAttribute("id", "example_link"),
269 TLiteralAttribute("autofocus", None),
270 TLiteralAttribute("title", ""),
271 TLiteralAttribute("href", "https://example.com"),
272 TLiteralAttribute("target", "_blank"),
273 ),
274 children=(TText.literal("Link"),),
275 )
278def test_literal_attr_entities():
279 node = TemplateParser.parse(t'<a title="<">Link</a>')
280 assert node == TElement(
281 "a",
282 attrs=(TLiteralAttribute("title", "<"),),
283 children=(TText.literal("Link"),),
284 )
287def test_literal_attr_order():
288 node = TemplateParser.parse(t'<a title="a" href="b" title="c"></a>')
289 assert isinstance(node, TElement)
290 assert node.attrs == (
291 TLiteralAttribute("title", "a"),
292 TLiteralAttribute("href", "b"),
293 TLiteralAttribute("title", "c"), # dupe IS allowed
294 )
297def test_interpolated_attr():
298 value1 = 42
299 value2 = 99
300 node = TemplateParser.parse(t'<div value1="{value1}" value2={value2} />')
301 assert node == TElement(
302 "div",
303 attrs=(
304 TInterpolatedAttribute("value1", 0),
305 TInterpolatedAttribute("value2", 1),
306 ),
307 children=(),
308 )
311def test_templated_attr():
312 value1 = 42
313 value2 = 99
314 node = TemplateParser.parse(
315 t'<div value1="{value1}-burrito" value2="neato-{value2}-wow" />'
316 )
317 value1_ref = TemplateRef(strings=("", "-burrito"), i_indexes=(0,))
318 value2_ref = TemplateRef(strings=("neato-", "-wow"), i_indexes=(1,))
319 assert node == TElement(
320 "div",
321 attrs=(
322 TTemplatedAttribute("value1", value1_ref),
323 TTemplatedAttribute("value2", value2_ref),
324 ),
325 children=(),
326 )
329def test_spread_attr():
330 spread_attrs = {}
331 node = TemplateParser.parse(t"<div {spread_attrs} />")
332 assert node == TElement(
333 "div",
334 attrs=(TSpreadAttribute(i_index=0),),
335 children=(),
336 )
339def test_templated_attribute_name_error():
340 with pytest.raises(ValueError):
341 attr_name = "some-attr"
342 _ = TemplateParser.parse(t'<div {attr_name}="value" />')
345def test_templated_attribute_name_and_value_error():
346 with pytest.raises(ValueError):
347 attr_name = "some-attr"
348 value = "value"
349 _ = TemplateParser.parse(t'<div {attr_name}="{value}" />')
352def test_adjacent_spread_attrs_error():
353 with pytest.raises(ValueError):
354 attrs1 = {}
355 attrs2 = {}
356 _ = TemplateParser.parse(t"<div {attrs1}{attrs2} />")
359#
360# Comments
361#
362def test_parse_comment():
363 node = TemplateParser.parse(t"<!-- This is a comment -->")
364 assert node == TComment.literal(" This is a comment ")
367def test_parse_comment_interpolation():
368 text = "comment"
369 node = TemplateParser.parse(t"<!-- This is a {text} -->")
370 assert node == TComment(
371 ref=TemplateRef(strings=(" This is a ", " "), i_indexes=(0,))
372 )
375#
376# Doctypes
377#
378def test_parse_doctype():
379 node = TemplateParser.parse(t"<!DOCTYPE html>")
380 assert node == TDocumentType("html")
383def test_parse_doctype_interpolation_error():
384 extra = "SYSTEM"
385 with pytest.raises(ValueError):
386 _ = TemplateParser.parse(t"<!DOCTYPE html {extra}>")
389def test_unsupported_decl_error():
390 with pytest.raises(NotImplementedError):
391 _ = TemplateParser.parse(t"<!doctype-alt html500>") # Unknown declaration
392 with pytest.raises(NotImplementedError):
393 _ = TemplateParser.parse(t"<!doctype>") # missing DTD
396#
397# Components.
398#
399def test_component_element_with_children():
400 def Component(children):
401 return t"{children}"
403 node = TemplateParser.parse(t"<{Component}><div>Hello, World!</div></{Component}>")
404 assert node == TComponent(
405 start_i_index=0,
406 end_i_index=1,
407 children=(TElement("div", children=(TText.literal("Hello, World!"),)),),
408 )
411def test_component_element_self_closing():
412 def Component():
413 pass
415 node = TemplateParser.parse(t"<{Component} />")
416 assert node == TComponent(start_i_index=0)
419def test_component_element_with_closing_tag():
420 def Component():
421 pass
423 node = TemplateParser.parse(t"<{Component}></{Component}>")
424 assert node == TComponent(start_i_index=0, end_i_index=1)
427def test_component_element_special_case_mismatched_closing_tag_still_parses():
428 def Component1():
429 pass
431 def Component2():
432 pass
434 node = TemplateParser.parse(t"<{Component1}></{Component2}>")
435 assert node == TComponent(start_i_index=0, end_i_index=1)
438def test_component_element_invalid_closing_tag():
439 def Component():
440 pass
442 with pytest.raises(ValueError):
443 _ = TemplateParser.parse(t"<{Component}></div>")
446def test_component_element_invalid_opening_tag():
447 def Component():
448 pass
450 with pytest.raises(ValueError):
451 _ = TemplateParser.parse(t"<div></{Component}>")
454def test_adjacent_start_component_tag_error():
455 def Component():
456 pass
458 with pytest.raises(ValueError):
459 _ = TemplateParser.parse(t"<{Component}{Component}></{Component}>")
462def test_adjacent_end_component_tag_error():
463 def Component():
464 pass
466 with pytest.raises(ValueError):
467 _ = TemplateParser.parse(t"<{Component}></{Component}{Component}>")