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

1import datetime 

2import typing as t 

3from dataclasses import dataclass, field 

4from string.templatelib import Template 

5 

6import pytest 

7from markupsafe import Markup 

8 

9from .nodes import Element, Fragment, Node, Text 

10from .processor import html 

11 

12# -------------------------------------------------------------------------- 

13# Basic HTML parsing tests 

14# -------------------------------------------------------------------------- 

15 

16 

17def test_parse_empty(): 

18 node = html(t"") 

19 assert node == Text("") 

20 assert str(node) == "" 

21 

22 

23def test_parse_text(): 

24 node = html(t"Hello, world!") 

25 assert node == Text("Hello, world!") 

26 assert str(node) == "Hello, world!" 

27 

28 

29def test_parse_void_element(): 

30 node = html(t"<br>") 

31 assert node == Element("br") 

32 assert str(node) == "<br />" 

33 

34 

35def test_parse_void_element_self_closed(): 

36 node = html(t"<br />") 

37 assert node == Element("br") 

38 assert str(node) == "<br />" 

39 

40 

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 />' 

54 

55 

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>" 

65 

66 

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>' 

77 

78 

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>" 

89 

90 

91# -------------------------------------------------------------------------- 

92# Interpolated text content 

93# -------------------------------------------------------------------------- 

94 

95 

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>" 

101 

102 

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, &lt;Alice &amp; Bob&gt;!</p>" 

110 

111 

112class Convertible: 

113 def __str__(self): 

114 return "string" 

115 

116 def __repr__(self): 

117 return "repr" 

118 

119 

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 ) 

132 

133 

134# -------------------------------------------------------------------------- 

135# Interpolated non-text content 

136# -------------------------------------------------------------------------- 

137 

138 

139def test_interpolated_false_content(): 

140 node = html(t"<div>{False}</div>") 

141 assert node == Element("div", children=[]) 

142 assert str(node) == "<div></div>" 

143 

144 

145def test_interpolated_none_content(): 

146 node = html(t"<div>{None}</div>") 

147 assert node == Element("div", children=[]) 

148 assert str(node) == "<div></div>" 

149 

150 

151def test_interpolated_zero_arg_function(): 

152 def get_value(): 

153 return "dynamic" 

154 

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 ) 

159 

160 

161def test_interpolated_multi_arg_function_fails(): 

162 def add(a, b): # pragma: no cover 

163 return a + b 

164 

165 with pytest.raises(TypeError): 

166 _ = html(t"<p>The sum is {add}.</p>") 

167 

168 

169# -------------------------------------------------------------------------- 

170# Raw HTML injection tests 

171# -------------------------------------------------------------------------- 

172 

173 

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>" 

179 

180 

181def test_raw_html_injection_with_dunder_html_protocol(): 

182 class SafeContent: 

183 def __init__(self, text): 

184 self._text = text 

185 

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>" 

189 

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>" 

201 

202 

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>" 

215 

216 

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 &lt;i&gt;italic&lt;/i&gt; text.</p>" 

229 

230 

231# -------------------------------------------------------------------------- 

232# Conditional rendering and control flow 

233# -------------------------------------------------------------------------- 

234 

235 

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>") 

241 

242 assert node == Element( 

243 "div", children=[Element("span", children=[Text("Welcome, User!")])] 

244 ) 

245 assert str(node) == "<div><span>Welcome, User!</span></div>" 

246 

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>' 

250 

251 

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>") 

256 

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>' 

264 

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>" 

269 

270 

271# -------------------------------------------------------------------------- 

272# Interpolated nesting of templates and elements 

273# -------------------------------------------------------------------------- 

274 

275 

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>" 

281 

282 

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>" 

288 

289 

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>" 

297 

298 

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>" 

311 

312 

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 ) 

357 

358 

359# -------------------------------------------------------------------------- 

360# Interpolated attribute content 

361# -------------------------------------------------------------------------- 

362 

363 

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>' 

371 

372 

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=&#34;test&#34;&amp;lang=en">Link</a>' 

384 ) 

385 

386 

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>' 

392 

393 

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>" 

401 

402 

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>" 

409 

410 

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>' 

420 

421 

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 ) 

435 

436 

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 ) 

450 

451 

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>' 

461 

462 

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>' 

472 

473 

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 ) 

486 

487 

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 ) 

500 

501 

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 ) 

514 

515 

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>' 

525 

526 

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>") 

531 

532 

533# -------------------------------------------------------------------------- 

534# Function component interpolation tests 

535# -------------------------------------------------------------------------- 

536 

537 

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>" 

551 

552 

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 ) 

571 

572 

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 ) 

594 

595 

596def test_invalid_component_invocation(): 

597 with pytest.raises(TypeError): 

598 _ = html(t"<{FunctionComponent}>Missing props</{FunctionComponent}>") 

599 

600 

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>" 

611 

612 

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 ) 

630 

631 

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>" 

639 

640 

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 ) 

656 

657 

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 ) 

671 

672 

673def ColumnsComponent() -> Template: 

674 return t"""<td>Column 1</td><td>Column 2</td>""" 

675 

676 

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>" 

694 

695 

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}>" 

701 

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 ) 

719 

720 

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'}") 

725 

726 node = html(t"<{Header} />") 

727 assert node == Text("Hello World") 

728 assert str(node) == "Hello World" 

729 

730 

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>") 

736 

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>" 

747 

748 

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></>") 

752 

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>" 

763 

764 

765@dataclass 

766class ClassComponent: 

767 """Example class-based component.""" 

768 

769 user_name: str 

770 image_url: str 

771 homepage: str = "#" 

772 children: t.Iterable[Node] = field(default_factory=list) 

773 

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 ) 

784 

785 

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 ) 

815 

816 

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 ) 

848 

849 

850@dataclass 

851class ClassComponentNoChildren: 

852 """Example class-based component that does not ask for children.""" 

853 

854 user_name: str 

855 image_url: str 

856 homepage: str = "#" 

857 

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 ) 

868 

869 

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 ) 

899 

900 

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!" 

917 

918 

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!" 

933 

934 

935def test_component_non_callable_fails(): 

936 with pytest.raises(TypeError): 

937 _ = html(t"<{'not a function'} />") 

938 

939 

940def RequiresPositional(whoops: int, /) -> Template: # pragma: no cover 

941 return t"<p>Positional arg: {whoops}</p>" 

942 

943 

944def test_component_requiring_positional_arg_fails(): 

945 with pytest.raises(TypeError): 

946 _ = html(t"<{RequiresPositional} />")