Simple HTML Generation https://minihtml.trendels.name/
1from textwrap import dedent
2
3import pytest
4from pytest import raises as assert_raises
5
6from minihtml import CircularReferenceError, make_prototype, safe, text
7
8div = make_prototype("div")
9span = make_prototype("span", inline=True)
10img = make_prototype("img", inline=True, empty=True, omit_end_tag=True)
11iframe = make_prototype("iframe", empty=True, omit_end_tag=False)
12
13
14def test_prototype_repr():
15 assert repr(div) == "<PrototypeNonEmpty div>"
16 assert repr(span) == "<PrototypeNonEmpty span (inline)>"
17 assert repr(img) == "<PrototypeEmpty img (inline, omit_end_tag)>"
18 assert repr(iframe) == "<PrototypeEmpty iframe>"
19
20
21def test_element_repr():
22 assert repr(div()) == "<ElementNonEmpty div>"
23 assert repr(img()) == "<ElementEmpty img>"
24
25
26def test_render_bare_elements():
27 assert str(div()) == "<div></div>"
28 assert str(img()) == "<img>"
29 assert str(iframe()) == "<iframe></iframe>"
30
31
32def test_positional_args_are_children():
33 assert str(span(img())) == "<span><img></span>"
34 assert str(span("hello")) == "<span>hello</span>"
35
36
37def test_keyword_args_are_attributes():
38 assert str(div(title="hello")) == '<div title="hello"></div>'
39 assert str(img(src="hello.png")) == '<img src="hello.png">'
40
41
42@pytest.mark.parametrize(
43 ("kwarg", "attribute"),
44 [
45 ("foo", "foo"),
46 ("foo_bar", "foo-bar"),
47 ("foo__bar", "foo--bar"),
48 ("_foo", "-foo"),
49 ("__foo", "--foo"),
50 ("foo_", "foo"),
51 ("foo__", "foo"),
52 ("_", "_"),
53 ],
54)
55def test_attribute_name_mangling(kwarg: str, attribute: str):
56 assert str(div(**{kwarg: "test"})) == f'<div {attribute}="test"></div>'
57 assert str(img(**{kwarg: "test"})) == f'<img {attribute}="test">'
58
59
60def test_boolean_attributes():
61 assert str(div(enabled=True)) == "<div enabled></div>"
62 assert str(div(enabled=False)) == "<div></div>"
63 assert str(div(_=True)) == "<div _></div>"
64 assert str(div(_=False)) == "<div></div>"
65
66 assert str(img(enabled=True)) == "<img enabled>"
67 assert str(img(enabled=False)) == "<img>"
68 assert str(img(_=True)) == "<img _>"
69 assert str(img(_=False)) == "<img>"
70
71
72def test_attribute_values_are_escaped():
73 assert (
74 str(div(title='hello"<world>&'))
75 == '<div title="hello"<world>&"></div>'
76 )
77
78
79@pytest.mark.parametrize(
80 "name",
81 [
82 "",
83 " ",
84 '"',
85 "'",
86 "/",
87 "=",
88 ">",
89 "a a",
90 'a"a',
91 "a'a",
92 "a/a",
93 "a>a",
94 ],
95)
96def test_attribute_names_are_validated(name: str):
97 with assert_raises(ValueError, match=f"Invalid attribute name: {name!r}"):
98 div(**{name: "test"})
99
100 with assert_raises(ValueError, match=f"Invalid attribute name: {name!r}"):
101 img(**{name: "test"})
102
103
104def test_text_contend_is_escaped():
105 assert str(div('hello"<world>&')) == '<div>hello"<world>&</div>'
106
107
108def test_indexing_adds_class_names():
109 assert str(div["green"]) == '<div class="green"></div>'
110 assert str(div["green supergreen"]) == '<div class="green supergreen"></div>'
111 assert str(div["green"]["supergreen"]) == '<div class="green supergreen"></div>'
112 assert str(img["myclass"]) == '<img class="myclass">'
113 assert str(img["myclass"]["otherclass"]) == '<img class="myclass otherclass">'
114
115
116def test_indexing_sets_hashtag_id():
117 assert str(div["#blue"]) == '<div id="blue"></div>'
118 assert str(div["green #blue"]) == '<div id="blue" class="green"></div>'
119 assert str(div["green #blue #red"]) == '<div id="red" class="green"></div>'
120 assert str(img["#my-id"]) == '<img id="my-id">'
121
122
123def test_calling_prototype_in_element_context_adds_child_element():
124 with div as elem:
125 span("hello")
126
127 assert str(elem) == "<div><span>hello</span></div>"
128
129 with div["myclass"] as elem:
130 span("hello")(" again")
131
132 assert str(elem) == '<div class="myclass"><span>hello again</span></div>'
133
134 with div["myclass"] as elem:
135 img(src="hello.png")
136
137 assert str(elem) == '<div class="myclass"><img src="hello.png"></div>'
138
139
140def test_elements_used_as_positional_args_are_not_added_twice():
141 with div as elem:
142 div["nested"](span("hello"))
143
144 assert str(elem) == dedent("""\
145 <div>
146 <div class="nested"><span>hello</span></div>
147 </div>""")
148
149 with div["myclass"] as elem:
150 div["nested"](span("hello")(" again"))
151
152 assert str(elem) == dedent("""\
153 <div class="myclass">
154 <div class="nested"><span>hello again</span></div>
155 </div>""")
156
157
158def test_context_managers_can_be_nested():
159 with div["outer"] as elem:
160 with div, div["inner"]:
161 span("hello")
162
163 assert str(elem) == dedent("""\
164 <div class="outer">
165 <div>
166 <div class="inner"><span>hello</span></div>
167 </div>
168 </div>""")
169
170
171def test_context_manager_with_text_content():
172 with div as elem:
173 text("hello")
174 safe("<!-- this is a comment -->")
175
176 assert str(elem) == "<div>hello<!-- this is a comment --></div>"
177
178
179def test_circular_reference_raises_error_when_rendering():
180 elem = div()
181 elem(elem)
182
183 with assert_raises(CircularReferenceError):
184 str(elem)
185
186 with div() as elem2:
187 div(elem2)
188
189 with assert_raises(CircularReferenceError):
190 str(elem2)