Coverage for tdom/processor_test.py: 100%
347 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-17 19:54 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-17 19:54 +0000
1import datetime
2import typing as t
3from dataclasses import dataclass, field
4from string.templatelib import Template
6import pytest
7from markupsafe import Markup
9from .nodes import Element, Fragment, Node, Text
10from .processor import html
12# --------------------------------------------------------------------------
13# Basic HTML parsing tests
14# --------------------------------------------------------------------------
17def test_parse_empty():
18 node = html(t"")
19 assert node == Text("")
20 assert str(node) == ""
23def test_parse_text():
24 node = html(t"Hello, world!")
25 assert node == Text("Hello, world!")
26 assert str(node) == "Hello, world!"
29def test_parse_void_element():
30 node = html(t"<br>")
31 assert node == Element("br")
32 assert str(node) == "<br />"
35def test_parse_void_element_self_closed():
36 node = html(t"<br />")
37 assert node == Element("br")
38 assert str(node) == "<br />"
41def test_parse_chain_of_void_elements():
42 # Make sure our handling of CPython issue #69445 is reasonable.
43 node = html(t"<br><hr><img src='image.png' /><br /><hr>")
44 assert node == Fragment(
45 children=[
46 Element("br"),
47 Element("hr"),
48 Element("img", attrs={"src": "image.png"}),
49 Element("br"),
50 Element("hr"),
51 ],
52 )
53 assert str(node) == '<br /><hr /><img src="image.png" /><br /><hr />'
56def test_parse_element_with_text():
57 node = html(t"<p>Hello, world!</p>")
58 assert node == Element(
59 "p",
60 children=[
61 Text("Hello, world!"),
62 ],
63 )
64 assert str(node) == "<p>Hello, world!</p>"
67def test_parse_element_with_attributes():
68 node = html(t'<a href="https://example.com" target="_blank">Link</a>')
69 assert node == Element(
70 "a",
71 attrs={"href": "https://example.com", "target": "_blank"},
72 children=[
73 Text("Link"),
74 ],
75 )
76 assert str(node) == '<a href="https://example.com" target="_blank">Link</a>'
79def test_parse_nested_elements():
80 node = html(t"<div><p>Hello</p><p>World</p></div>")
81 assert node == Element(
82 "div",
83 children=[
84 Element("p", children=[Text("Hello")]),
85 Element("p", children=[Text("World")]),
86 ],
87 )
88 assert str(node) == "<div><p>Hello</p><p>World</p></div>"
91# --------------------------------------------------------------------------
92# Interpolated text content
93# --------------------------------------------------------------------------
96def test_interpolated_text_content():
97 name = "Alice"
98 node = html(t"<p>Hello, {name}!</p>")
99 assert node == Element("p", children=[Text("Hello, "), Text("Alice"), Text("!")])
100 assert str(node) == "<p>Hello, Alice!</p>"
103def test_escaping_of_interpolated_text_content():
104 name = "<Alice & Bob>"
105 node = html(t"<p>Hello, {name}!</p>")
106 assert node == Element(
107 "p", children=[Text("Hello, "), Text("<Alice & Bob>"), Text("!")]
108 )
109 assert str(node) == "<p>Hello, <Alice & Bob>!</p>"
112class Convertible:
113 def __str__(self):
114 return "string"
116 def __repr__(self):
117 return "repr"
120def test_conversions():
121 c = Convertible()
122 assert f"{c!s}" == "string"
123 assert f"{c!r}" == "repr"
124 node = html(t"<li>{c!s}</li><li>{c!r}</li><li>{'😊'!a}</li>")
125 assert node == Fragment(
126 children=[
127 Element("li", children=[Text("string")]),
128 Element("li", children=[Text("repr")]),
129 Element("li", children=[Text("'\\U0001f60a'")]),
130 ],
131 )
134# --------------------------------------------------------------------------
135# Interpolated non-text content
136# --------------------------------------------------------------------------
139def test_interpolated_false_content():
140 node = html(t"<div>{False}</div>")
141 assert node == Element("div", children=[])
142 assert str(node) == "<div></div>"
145def test_interpolated_none_content():
146 node = html(t"<div>{None}</div>")
147 assert node == Element("div", children=[])
148 assert str(node) == "<div></div>"
151def test_interpolated_zero_arg_function():
152 def get_value():
153 return "dynamic"
155 node = html(t"<p>The value is {get_value}.</p>")
156 assert node == Element(
157 "p", children=[Text("The value is "), Text("dynamic"), Text(".")]
158 )
161def test_interpolated_multi_arg_function_fails():
162 def add(a, b): # pragma: no cover
163 return a + b
165 with pytest.raises(TypeError):
166 _ = html(t"<p>The sum is {add}.</p>")
169# --------------------------------------------------------------------------
170# Raw HTML injection tests
171# --------------------------------------------------------------------------
174def test_raw_html_injection_with_markupsafe():
175 raw_content = Markup("<strong>I am bold</strong>")
176 node = html(t"<div>{raw_content}</div>")
177 assert node == Element("div", children=[Text(text=raw_content)])
178 assert str(node) == "<div><strong>I am bold</strong></div>"
181def test_raw_html_injection_with_dunder_html_protocol():
182 class SafeContent:
183 def __init__(self, text):
184 self._text = text
186 def __html__(self):
187 # In a real app, this would come from a sanitizer or trusted source
188 return f"<em>{self._text}</em>"
190 content = SafeContent("emphasized")
191 node = html(t"<p>Here is some {content}.</p>")
192 assert node == Element(
193 "p",
194 children=[
195 Text("Here is some "),
196 Text(Markup("<em>emphasized</em>")),
197 Text("."),
198 ],
199 )
200 assert str(node) == "<p>Here is some <em>emphasized</em>.</p>"
203def test_raw_html_injection_with_format_spec():
204 raw_content = "<u>underlined</u>"
205 node = html(t"<p>This is {raw_content:safe} text.</p>")
206 assert node == Element(
207 "p",
208 children=[
209 Text("This is "),
210 Text(Markup(raw_content)),
211 Text(" text."),
212 ],
213 )
214 assert str(node) == "<p>This is <u>underlined</u> text.</p>"
217def test_raw_html_injection_with_markupsafe_unsafe_format_spec():
218 supposedly_safe = Markup("<i>italic</i>")
219 node = html(t"<p>This is {supposedly_safe:unsafe} text.</p>")
220 assert node == Element(
221 "p",
222 children=[
223 Text("This is "),
224 Text(supposedly_safe),
225 Text(" text."),
226 ],
227 )
228 assert str(node) == "<p>This is <i>italic</i> text.</p>"
231# --------------------------------------------------------------------------
232# Conditional rendering and control flow
233# --------------------------------------------------------------------------
236def test_conditional_rendering_with_if_else():
237 is_logged_in = True
238 user_profile = t"<span>Welcome, User!</span>"
239 login_prompt = t"<a href='/login'>Please log in</a>"
240 node = html(t"<div>{user_profile if is_logged_in else login_prompt}</div>")
242 assert node == Element(
243 "div", children=[Element("span", children=[Text("Welcome, User!")])]
244 )
245 assert str(node) == "<div><span>Welcome, User!</span></div>"
247 is_logged_in = False
248 node = html(t"<div>{user_profile if is_logged_in else login_prompt}</div>")
249 assert str(node) == '<div><a href="/login">Please log in</a></div>'
252def test_conditional_rendering_with_and():
253 show_warning = True
254 warning_message = t'<div class="warning">Warning!</div>'
255 node = html(t"<main>{show_warning and warning_message}</main>")
257 assert node == Element(
258 "main",
259 children=[
260 Element("div", attrs={"class": "warning"}, children=[Text("Warning!")]),
261 ],
262 )
263 assert str(node) == '<main><div class="warning">Warning!</div></main>'
265 show_warning = False
266 node = html(t"<main>{show_warning and warning_message}</main>")
267 # Assuming False renders nothing
268 assert str(node) == "<main></main>"
271# --------------------------------------------------------------------------
272# Interpolated nesting of templates and elements
273# --------------------------------------------------------------------------
276def test_interpolated_template_content():
277 child = t"<span>Child</span>"
278 node = html(t"<div>{child}</div>")
279 assert node == Element("div", children=[html(child)])
280 assert str(node) == "<div><span>Child</span></div>"
283def test_interpolated_element_content():
284 child = html(t"<span>Child</span>")
285 node = html(t"<div>{child}</div>")
286 assert node == Element("div", children=[child])
287 assert str(node) == "<div><span>Child</span></div>"
290def test_interpolated_nonstring_content():
291 number = 42
292 node = html(t"<p>The answer is {number}.</p>")
293 assert node == Element(
294 "p", children=[Text("The answer is "), Text("42"), Text(".")]
295 )
296 assert str(node) == "<p>The answer is 42.</p>"
299def test_list_items():
300 items = ["Apple", "Banana", "Cherry"]
301 node = html(t"<ul>{[t'<li>{item}</li>' for item in items]}</ul>")
302 assert node == Element(
303 "ul",
304 children=[
305 Element("li", children=[Text("Apple")]),
306 Element("li", children=[Text("Banana")]),
307 Element("li", children=[Text("Cherry")]),
308 ],
309 )
310 assert str(node) == "<ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>"
313def test_nested_list_items():
314 # TODO XXX this is a pretty abusrd test case; clean it up when refactoring
315 outer = ["fruit", "more fruit"]
316 inner = ["apple", "banana", "cherry"]
317 inner_items = [t"<li>{item}</li>" for item in inner]
318 outer_items = [t"<li>{category}<ul>{inner_items}</ul></li>" for category in outer]
319 node = html(t"<ul>{outer_items}</ul>")
320 assert node == Element(
321 "ul",
322 children=[
323 Element(
324 "li",
325 children=[
326 Text("fruit"),
327 Element(
328 "ul",
329 children=[
330 Element("li", children=[Text("apple")]),
331 Element("li", children=[Text("banana")]),
332 Element("li", children=[Text("cherry")]),
333 ],
334 ),
335 ],
336 ),
337 Element(
338 "li",
339 children=[
340 Text("more fruit"),
341 Element(
342 "ul",
343 children=[
344 Element("li", children=[Text("apple")]),
345 Element("li", children=[Text("banana")]),
346 Element("li", children=[Text("cherry")]),
347 ],
348 ),
349 ],
350 ),
351 ],
352 )
353 assert (
354 str(node)
355 == "<ul><li>fruit<ul><li>apple</li><li>banana</li><li>cherry</li></ul></li><li>more fruit<ul><li>apple</li><li>banana</li><li>cherry</li></ul></li></ul>"
356 )
359# --------------------------------------------------------------------------
360# Interpolated attribute content
361# --------------------------------------------------------------------------
364def test_interpolated_attribute_value():
365 url = "https://example.com/"
366 node = html(t'<a href="{url}">Link</a>')
367 assert node == Element(
368 "a", attrs={"href": "https://example.com/"}, children=[Text("Link")]
369 )
370 assert str(node) == '<a href="https://example.com/">Link</a>'
373def test_escaping_of_interpolated_attribute_value():
374 url = 'https://example.com/?q="test"&lang=en'
375 node = html(t'<a href="{url}">Link</a>')
376 assert node == Element(
377 "a",
378 attrs={"href": Markup('https://example.com/?q="test"&lang=en')},
379 children=[Text("Link")],
380 )
381 assert (
382 str(node)
383 == '<a href="https://example.com/?q="test"&lang=en">Link</a>'
384 )
387def test_interpolated_unquoted_attribute_value():
388 id = "roquefort"
389 node = html(t"<div id={id}>Cheese</div>")
390 assert node == Element("div", attrs={"id": "roquefort"}, children=[Text("Cheese")])
391 assert str(node) == '<div id="roquefort">Cheese</div>'
394def test_interpolated_attribute_value_true():
395 disabled = True
396 node = html(t"<button disabled={disabled}>Click me</button>")
397 assert node == Element(
398 "button", attrs={"disabled": None}, children=[Text("Click me")]
399 )
400 assert str(node) == "<button disabled>Click me</button>"
403def test_interpolated_attribute_value_falsy():
404 disabled = False
405 crumpled = None
406 node = html(t"<button disabled={disabled} crumpled={crumpled}>Click me</button>")
407 assert node == Element("button", attrs={}, children=[Text("Click me")])
408 assert str(node) == "<button>Click me</button>"
411def test_interpolated_attribute_spread_dict():
412 attrs = {"href": "https://example.com/", "target": "_blank"}
413 node = html(t"<a {attrs}>Link</a>")
414 assert node == Element(
415 "a",
416 attrs={"href": "https://example.com/", "target": "_blank"},
417 children=[Text("Link")],
418 )
419 assert str(node) == '<a href="https://example.com/" target="_blank">Link</a>'
422def test_interpolated_mixed_attribute_values_and_spread_dict():
423 attrs = {"href": "https://example.com/", "id": "link1"}
424 target = "_blank"
425 node = html(t'<a {attrs} target="{target}">Link</a>')
426 assert node == Element(
427 "a",
428 attrs={"href": "https://example.com/", "id": "link1", "target": "_blank"},
429 children=[Text("Link")],
430 )
431 assert (
432 str(node)
433 == '<a href="https://example.com/" id="link1" target="_blank">Link</a>'
434 )
437def test_multiple_attribute_spread_dicts():
438 attrs1 = {"href": "https://example.com/", "id": "overwrtten"}
439 attrs2 = {"target": "_blank", "id": "link1"}
440 node = html(t"<a {attrs1} {attrs2}>Link</a>")
441 assert node == Element(
442 "a",
443 attrs={"href": "https://example.com/", "id": "link1", "target": "_blank"},
444 children=[Text("Link")],
445 )
446 assert (
447 str(node)
448 == '<a href="https://example.com/" id="link1" target="_blank">Link</a>'
449 )
452def test_interpolated_class_attribute():
453 classes = ["btn", "btn-primary", False and "disabled", None, {"active": True}]
454 node = html(t'<button class="{classes}">Click me</button>')
455 assert node == Element(
456 "button",
457 attrs={"class": "btn btn-primary active"},
458 children=[Text("Click me")],
459 )
460 assert str(node) == '<button class="btn btn-primary active">Click me</button>'
463def test_interpolated_attribute_spread_with_class_attribute():
464 attrs = {"id": "button1", "class": ["btn", "btn-primary"]}
465 node = html(t"<button {attrs}>Click me</button>")
466 assert node == Element(
467 "button",
468 attrs={"id": "button1", "class": "btn btn-primary"},
469 children=[Text("Click me")],
470 )
471 assert str(node) == '<button id="button1" class="btn btn-primary">Click me</button>'
474def test_interpolated_data_attributes():
475 data = {"user-id": 123, "role": "admin", "wild": True}
476 node = html(t"<div data={data}>User Info</div>")
477 assert node == Element(
478 "div",
479 attrs={"data-user-id": "123", "data-role": "admin", "data-wild": None},
480 children=[Text("User Info")],
481 )
482 assert (
483 str(node)
484 == '<div data-user-id="123" data-role="admin" data-wild>User Info</div>'
485 )
488def test_interpolated_aria_attributes():
489 aria = {"label": "Close", "hidden": True, "another": False, "more": None}
490 node = html(t"<button aria={aria}>X</button>")
491 assert node == Element(
492 "button",
493 attrs={"aria-label": "Close", "aria-hidden": "true", "aria-another": "false"},
494 children=[Text("X")],
495 )
496 assert (
497 str(node)
498 == '<button aria-label="Close" aria-hidden="true" aria-another="false">X</button>'
499 )
502def test_interpolated_style_attribute():
503 styles = {"color": "red", "font-weight": "bold", "font-size": "16px"}
504 node = html(t"<p style={styles}>Warning!</p>")
505 assert node == Element(
506 "p",
507 attrs={"style": "color: red; font-weight: bold; font-size: 16px"},
508 children=[Text("Warning!")],
509 )
510 assert (
511 str(node)
512 == '<p style="color: red; font-weight: bold; font-size: 16px">Warning!</p>'
513 )
516def test_style_attribute_str():
517 styles = "color: red; font-weight: bold;"
518 node = html(t"<p style={styles}>Warning!</p>")
519 assert node == Element(
520 "p",
521 attrs={"style": "color: red; font-weight: bold;"},
522 children=[Text("Warning!")],
523 )
524 assert str(node) == '<p style="color: red; font-weight: bold;">Warning!</p>'
527def test_style_attribute_non_str_non_dict():
528 with pytest.raises(TypeError):
529 styles = [1, 2]
530 _ = html(t"<p style={styles}>Warning!</p>")
533# --------------------------------------------------------------------------
534# Function component interpolation tests
535# --------------------------------------------------------------------------
538def FunctionComponent(
539 children: t.Iterable[Node], first: str, second: int, third_arg: str, **attrs: t.Any
540) -> Template:
541 # Ensure type correctness of props at runtime for testing purposes
542 assert isinstance(first, str)
543 assert isinstance(second, int)
544 assert isinstance(third_arg, str)
545 new_attrs = {
546 "id": third_arg,
547 "data": {"first": first, "second": second},
548 **attrs,
549 }
550 return t"<div {new_attrs}>Component: {children}</div>"
553def test_interpolated_template_component():
554 node = html(
555 t'<{FunctionComponent} first=1 second={99} third-arg="comp1" class="my-comp">Hello, Component!</{FunctionComponent}>'
556 )
557 assert node == Element(
558 "div",
559 attrs={
560 "id": "comp1",
561 "data-first": "1",
562 "data-second": "99",
563 "class": "my-comp",
564 },
565 children=[Text("Component: "), Text("Hello, Component!")],
566 )
567 assert (
568 str(node)
569 == '<div id="comp1" data-first="1" data-second="99" class="my-comp">Component: Hello, Component!</div>'
570 )
573def test_interpolated_template_component_no_children_provided():
574 """Same test, but the caller didn't provide any children."""
575 node = html(
576 t'<{FunctionComponent} first=1 second={99} third-arg="comp1" class="my-comp" />'
577 )
578 assert node == Element(
579 "div",
580 attrs={
581 "id": "comp1",
582 "data-first": "1",
583 "data-second": "99",
584 "class": "my-comp",
585 },
586 children=[
587 Text("Component: "),
588 ],
589 )
590 assert (
591 str(node)
592 == '<div id="comp1" data-first="1" data-second="99" class="my-comp">Component: </div>'
593 )
596def test_invalid_component_invocation():
597 with pytest.raises(TypeError):
598 _ = html(t"<{FunctionComponent}>Missing props</{FunctionComponent}>")
601def FunctionComponentNoChildren(first: str, second: int, third_arg: str) -> Template:
602 # Ensure type correctness of props at runtime for testing purposes
603 assert isinstance(first, str)
604 assert isinstance(second, int)
605 assert isinstance(third_arg, str)
606 new_attrs = {
607 "id": third_arg,
608 "data": {"first": first, "second": second},
609 }
610 return t"<div {new_attrs}>Component: ignore children</div>"
613def test_interpolated_template_component_ignore_children():
614 node = html(
615 t'<{FunctionComponentNoChildren} first=1 second={99} third-arg="comp1">Hello, Component!</{FunctionComponentNoChildren}>'
616 )
617 assert node == Element(
618 "div",
619 attrs={
620 "id": "comp1",
621 "data-first": "1",
622 "data-second": "99",
623 },
624 children=[Text(text="Component: ignore children")],
625 )
626 assert (
627 str(node)
628 == '<div id="comp1" data-first="1" data-second="99">Component: ignore children</div>'
629 )
632def FunctionComponentKeywordArgs(first: str, **attrs: t.Any) -> Template:
633 # Ensure type correctness of props at runtime for testing purposes
634 assert isinstance(first, str)
635 assert "children" in attrs
636 _ = attrs.pop("children")
637 new_attrs = {"data-first": first, **attrs}
638 return t"<div {new_attrs}>Component with kwargs</div>"
641def test_children_always_passed_via_kwargs():
642 node = html(
643 t'<{FunctionComponentKeywordArgs} first="value" extra="info">Child content</{FunctionComponentKeywordArgs}>'
644 )
645 assert node == Element(
646 "div",
647 attrs={
648 "data-first": "value",
649 "extra": "info",
650 },
651 children=[Text("Component with kwargs")],
652 )
653 assert (
654 str(node) == '<div data-first="value" extra="info">Component with kwargs</div>'
655 )
658def test_children_always_passed_via_kwargs_even_when_empty():
659 node = html(t'<{FunctionComponentKeywordArgs} first="value" extra="info" />')
660 assert node == Element(
661 "div",
662 attrs={
663 "data-first": "value",
664 "extra": "info",
665 },
666 children=[Text("Component with kwargs")],
667 )
668 assert (
669 str(node) == '<div data-first="value" extra="info">Component with kwargs</div>'
670 )
673def ColumnsComponent() -> Template:
674 return t"""<td>Column 1</td><td>Column 2</td>"""
677def test_fragment_from_component():
678 # This test assumes that if a component returns a template that parses
679 # into multiple root elements, they are treated as a fragment.
680 node = html(t"<table><tr><{ColumnsComponent} /></tr></table>")
681 assert node == Element(
682 "table",
683 children=[
684 Element(
685 "tr",
686 children=[
687 Element("td", children=[Text("Column 1")]),
688 Element("td", children=[Text("Column 2")]),
689 ],
690 ),
691 ],
692 )
693 assert str(node) == "<table><tr><td>Column 1</td><td>Column 2</td></tr></table>"
696def test_component_passed_as_attr_value():
697 def Wrapper(
698 children: t.Iterable[Node], sub_component: t.Callable, **attrs: t.Any
699 ) -> Template:
700 return t"<{sub_component} {attrs}>{children}</{sub_component}>"
702 node = html(
703 t'<{Wrapper} sub-component={FunctionComponent} class="wrapped" first=1 second={99} third-arg="comp1"><p>Inside wrapper</p></{Wrapper}>'
704 )
705 assert node == Element(
706 "div",
707 attrs={
708 "id": "comp1",
709 "data-first": "1",
710 "data-second": "99",
711 "class": "wrapped",
712 },
713 children=[Text("Component: "), Element("p", children=[Text("Inside wrapper")])],
714 )
715 assert (
716 str(node)
717 == '<div id="comp1" data-first="1" data-second="99" class="wrapped">Component: <p>Inside wrapper</p></div>'
718 )
721def test_nested_component_gh23():
722 # See https://github.com/t-strings/tdom/issues/23 for context
723 def Header():
724 return html(t"{'Hello World'}")
726 node = html(t"<{Header} />")
727 assert node == Text("Hello World")
728 assert str(node) == "Hello World"
731def test_component_returning_iterable():
732 def Items() -> t.Iterable:
733 for i in range(2):
734 yield t"<li>Item {i + 1}</li>"
735 yield html(t"<li>Item {3}</li>")
737 node = html(t"<ul><{Items} /></ul>")
738 assert node == Element(
739 "ul",
740 children=[
741 Element("li", children=[Text("Item "), Text("1")]),
742 Element("li", children=[Text("Item "), Text("2")]),
743 Element("li", children=[Text("Item "), Text("3")]),
744 ],
745 )
746 assert str(node) == "<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>"
749def test_component_returning_explicit_fragment():
750 def Items() -> Node:
751 return html(t"<><li>Item {1}</li><li>Item {2}</li><li>Item {3}</li></>")
753 node = html(t"<ul><{Items} /></ul>")
754 assert node == Element(
755 "ul",
756 children=[
757 Element("li", children=[Text("Item "), Text("1")]),
758 Element("li", children=[Text("Item "), Text("2")]),
759 Element("li", children=[Text("Item "), Text("3")]),
760 ],
761 )
762 assert str(node) == "<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>"
765@dataclass
766class ClassComponent:
767 """Example class-based component."""
769 user_name: str
770 image_url: str
771 homepage: str = "#"
772 children: t.Iterable[Node] = field(default_factory=list)
774 def __call__(self) -> Node:
775 return html(
776 t"<div class='avatar'>"
777 t"<a href={self.homepage}>"
778 t"<img src='{self.image_url}' alt='{f'Avatar of {self.user_name}'}' />"
779 t"</a>"
780 t"<span>{self.user_name}</span>"
781 t"{self.children}"
782 t"</div>",
783 )
786def test_class_component_implicit_invocation():
787 node = html(
788 t"<{ClassComponent} user-name='Alice' image-url='https://example.com/alice.png'>Fun times!</{ClassComponent}>"
789 )
790 assert node == Element(
791 "div",
792 attrs={"class": "avatar"},
793 children=[
794 Element(
795 "a",
796 attrs={"href": "#"},
797 children=[
798 Element(
799 "img",
800 attrs={
801 "src": "https://example.com/alice.png",
802 "alt": "Avatar of Alice",
803 },
804 )
805 ],
806 ),
807 Element("span", children=[Text("Alice")]),
808 Text("Fun times!"),
809 ],
810 )
811 assert (
812 str(node)
813 == '<div class="avatar"><a href="#"><img src="https://example.com/alice.png" alt="Avatar of Alice" /></a><span>Alice</span>Fun times!</div>'
814 )
817def test_class_component_direct_invocation():
818 avatar = ClassComponent(
819 user_name="Alice",
820 image_url="https://example.com/alice.png",
821 homepage="https://example.com/users/alice",
822 )
823 node = html(t"<{avatar} />")
824 assert node == Element(
825 "div",
826 attrs={"class": "avatar"},
827 children=[
828 Element(
829 "a",
830 attrs={"href": "https://example.com/users/alice"},
831 children=[
832 Element(
833 "img",
834 attrs={
835 "src": "https://example.com/alice.png",
836 "alt": "Avatar of Alice",
837 },
838 )
839 ],
840 ),
841 Element("span", children=[Text("Alice")]),
842 ],
843 )
844 assert (
845 str(node)
846 == '<div class="avatar"><a href="https://example.com/users/alice"><img src="https://example.com/alice.png" alt="Avatar of Alice" /></a><span>Alice</span></div>'
847 )
850@dataclass
851class ClassComponentNoChildren:
852 """Example class-based component that does not ask for children."""
854 user_name: str
855 image_url: str
856 homepage: str = "#"
858 def __call__(self) -> Node:
859 return html(
860 t"<div class='avatar'>"
861 t"<a href={self.homepage}>"
862 t"<img src='{self.image_url}' alt='{f'Avatar of {self.user_name}'}' />"
863 t"</a>"
864 t"<span>{self.user_name}</span>"
865 t"ignore children"
866 t"</div>",
867 )
870def test_class_component_implicit_invocation_ignore_children():
871 node = html(
872 t"<{ClassComponentNoChildren} user-name='Alice' image-url='https://example.com/alice.png'>Fun times!</{ClassComponentNoChildren}>"
873 )
874 assert node == Element(
875 "div",
876 attrs={"class": "avatar"},
877 children=[
878 Element(
879 "a",
880 attrs={"href": "#"},
881 children=[
882 Element(
883 "img",
884 attrs={
885 "src": "https://example.com/alice.png",
886 "alt": "Avatar of Alice",
887 },
888 )
889 ],
890 ),
891 Element("span", children=[Text("Alice")]),
892 Text("ignore children"),
893 ],
894 )
895 assert (
896 str(node)
897 == '<div class="avatar"><a href="#"><img src="https://example.com/alice.png" alt="Avatar of Alice" /></a><span>Alice</span>ignore children</div>'
898 )
901def AttributeTypeComponent(
902 data_int: int,
903 data_true: bool,
904 data_false: bool,
905 data_none: None,
906 data_float: float,
907 data_dt: datetime.datetime,
908) -> Template:
909 """Component to test that we don't incorrectly convert attribute types."""
910 assert isinstance(data_int, int)
911 assert data_true is True
912 assert data_false is False
913 assert data_none is None
914 assert isinstance(data_float, float)
915 assert isinstance(data_dt, datetime.datetime)
916 return t"Looks good!"
919def test_attribute_type_component():
920 an_int: int = 42
921 a_true: bool = True
922 a_false: bool = False
923 a_none: None = None
924 a_float: float = 3.14
925 a_dt: datetime.datetime = datetime.datetime(2024, 1, 1, 12, 0, 0)
926 node = html(
927 t"<{AttributeTypeComponent} data-int={an_int} data-true={a_true} "
928 t"data-false={a_false} data-none={a_none} data-float={a_float} "
929 t"data-dt={a_dt} />"
930 )
931 assert node == Text("Looks good!")
932 assert str(node) == "Looks good!"
935def test_component_non_callable_fails():
936 with pytest.raises(TypeError):
937 _ = html(t"<{'not a function'} />")
940def RequiresPositional(whoops: int, /) -> Template: # pragma: no cover
941 return t"<p>Positional arg: {whoops}</p>"
944def test_component_requiring_positional_arg_fails():
945 with pytest.raises(TypeError):
946 _ = html(t"<{RequiresPositional} />")