Simple HTML Generation https://minihtml.trendels.name/
at main 268 lines 6.6 kB view raw
1from textwrap import dedent 2 3from pytest import raises as assert_raises 4 5from minihtml import Element, Fragment, Slots, component, fragment 6from minihtml.tags import ( 7 body, 8 div, 9 h2, 10 head, 11 html, 12 img, 13 main, 14 p, 15 script, 16 style, 17 title, 18) 19 20 21def test_basic_component(): 22 @component() 23 def my_component(slots: Slots, name: str) -> Element: 24 return div(name=name) 25 26 with div["container"] as elem: 27 my_component(name="component-name") 28 29 assert str(elem) == dedent("""\ 30 <div class="container"> 31 <div name="component-name"></div> 32 </div>""") 33 34 35def test_component_slot(): 36 @component() 37 def my_component(slots: Slots, name: str) -> Element: 38 with div(name=name) as elem: 39 slots.slot() 40 return elem 41 42 with div["container"] as elem: 43 with my_component(name="component-name"): 44 p("slot content") 45 46 assert str(elem) == dedent("""\ 47 <div class="container"> 48 <div name="component-name"> 49 <p>slot content</p> 50 </div> 51 </div>""") 52 53 54def test_named_slots(): 55 @component(slots=("head", "main")) 56 def my_component(slots: Slots) -> Element: 57 with html as elem: 58 with head: 59 slots.slot("head") 60 with body: 61 with main: 62 slots.slot("main") 63 return elem 64 65 with my_component() as comp: 66 with comp.slot("head"): 67 title("My website") 68 with comp.slot("main"): 69 p("My article") 70 71 assert str(comp) == dedent("""\ 72 <html> 73 <head> 74 <title>My website</title> 75 </head> 76 <body> 77 <main> 78 <p>My article</p> 79 </main> 80 </body> 81 </html>""") 82 83 84def test_component_without_default_raises_if_default_slot_is_used(): 85 @component(slots=["one"]) 86 def my_component(slots: Slots) -> Element: 87 with div as elem: 88 slots.slot("one") 89 return elem 90 91 with assert_raises(KeyError): 92 with my_component(): 93 p("default slot content") 94 95 96def test_named_slots_with_default(): 97 @component(slots=("head", "main"), default="main") 98 def my_component(slots: Slots) -> Element: 99 with html as elem: 100 with head: 101 slots.slot("head") 102 with body: 103 with main: 104 slots.slot() 105 return elem 106 107 with my_component() as comp1: 108 with comp1.slot("head"): 109 title("My website") 110 p("My article") 111 112 assert str(comp1) == dedent("""\ 113 <html> 114 <head> 115 <title>My website</title> 116 </head> 117 <body> 118 <main> 119 <p>My article</p> 120 </main> 121 </body> 122 </html>""") 123 124 with my_component() as comp2: 125 with comp2.slot("head"): 126 title("My website") 127 with comp2.slot("main"): 128 p("My article") 129 130 assert str(comp1) == str(comp2) 131 132 133def test_slot_is_filled(): 134 @component(slots=("icon", "main"), default="main") 135 def my_component(slots: Slots) -> Element: 136 with div["my-component"] as elem: 137 if slots.is_filled("icon"): 138 with div["icon"]: 139 slots.slot("icon") 140 if slots.is_filled(): 141 with div["main"]: 142 slots.slot() 143 return elem 144 145 with my_component() as comp1: 146 p("My article") 147 148 assert str(comp1) == dedent("""\ 149 <div class="my-component"> 150 <div class="main"> 151 <p>My article</p> 152 </div> 153 </div>""") 154 155 with my_component() as comp2: 156 with comp2.slot("icon"): 157 img(src="icon.png") 158 159 assert str(comp2) == dedent("""\ 160 <div class="my-component"> 161 <div class="icon"><img src="icon.png"></div> 162 </div>""") 163 164 165def test_slots_with_default_content(): 166 @component(slots=("title", "content"), default="content") 167 def my_component(slots: Slots) -> Fragment: 168 with fragment() as f: 169 with slots.slot("title"): 170 h2("Default title") 171 with slots.slot(): 172 p("Default content") 173 return f 174 175 comp1 = my_component() 176 177 with my_component() as comp2: 178 with comp2.slot("title"): 179 h2("My title") 180 p("My content") 181 182 assert str(comp1) == dedent("""\ 183 <h2>Default title</h2> 184 <p>Default content</p>""") 185 186 assert str(comp2) == dedent("""\ 187 <h2>My title</h2> 188 <p>My content</p>""") 189 190 191def test_default_slot_must_be_a_valid_slot_name(): 192 with assert_raises(ValueError, match="Can't set default without slots: 'x'"): 193 component(default="x") 194 195 with assert_raises( 196 ValueError, match="Invalid default: 'x'. Available slots: 'a', 'b'" 197 ): 198 component(slots=("a", "b"), default="x") 199 200 201def test_stringifying_component_has_no_side_effect(): 202 @component() 203 def my_component(slots: Slots) -> Element: 204 with div["my-component"] as elem: 205 slots.slot() 206 return elem 207 208 c1 = my_component() 209 210 with div as elem: 211 str(c1) 212 213 assert str(elem) == "<div></div>" 214 215 216def test_nested_components(): 217 @component() 218 def inner(slots: Slots) -> Element: 219 with div["inner"] as elem: 220 slots.slot() 221 return elem 222 223 @component() 224 def outer(slots: Slots) -> Element: 225 with div["outer"] as elem: 226 with inner(): 227 slots.slot() 228 return elem 229 230 with div["container"] as elem: 231 with outer(): 232 p("content") 233 234 assert str(elem) == dedent("""\ 235 <div class="container"> 236 <div class="outer"> 237 <div class="inner"> 238 <p>content</p> 239 </div> 240 </div> 241 </div>""") 242 243 244def test_component_style_and_script_has_no_effect_outside_of_template_context(): 245 @component( 246 style=style(".my-component { background: #ccc }"), 247 script=script("// script goes here"), 248 ) 249 def my_component(slots: Slots) -> Element: 250 return div["my-component"] 251 252 assert str(my_component()) == '<div class="my-component"></div>' 253 254 255def test_passing_component_as_argument(): 256 @component() 257 def my_component(slots: Slots) -> Element: 258 return div["my-component"] 259 260 with div["#container"] as elem: 261 div(my_component()) 262 263 assert str(elem) == dedent("""\ 264 <div id="container"> 265 <div> 266 <div class="my-component"></div> 267 </div> 268 </div>""")