Coverage for tdom/processor_test.py: 100%

417 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-31 17:14 +0000

1import datetime 

2import typing as t 

3from dataclasses import dataclass, field 

4from string.templatelib import Interpolation, Template 

5 

6import pytest 

7from markupsafe import Markup 

8 

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

10from .processor import _PLACEHOLDER_PREFIX, _PLACEHOLDER_SUFFIX, 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 

91def test_parse_entities_are_escaped(): 

92 node = html(t"<p>&lt;/p&gt;</p>") 

93 assert node == Element( 

94 "p", 

95 children=[Text("</p>")], 

96 ) 

97 assert str(node) == "<p>&lt;/p&gt;</p>" 

98 

99 

100# -------------------------------------------------------------------------- 

101# Interpolated text content 

102# -------------------------------------------------------------------------- 

103 

104 

105def test_interpolated_text_content(): 

106 name = "Alice" 

107 node = html(t"<p>Hello, {name}!</p>") 

108 assert node == Element("p", children=[Text("Hello, "), Text("Alice"), Text("!")]) 

109 assert str(node) == "<p>Hello, Alice!</p>" 

110 

111 

112def test_escaping_of_interpolated_text_content(): 

113 name = "<Alice & Bob>" 

114 node = html(t"<p>Hello, {name}!</p>") 

115 assert node == Element( 

116 "p", children=[Text("Hello, "), Text("<Alice & Bob>"), Text("!")] 

117 ) 

118 assert str(node) == "<p>Hello, &lt;Alice &amp; Bob&gt;!</p>" 

119 

120 

121class Convertible: 

122 def __str__(self): 

123 return "string" 

124 

125 def __repr__(self): 

126 return "repr" 

127 

128 

129def test_conversions(): 

130 c = Convertible() 

131 assert f"{c!s}" == "string" 

132 assert f"{c!r}" == "repr" 

133 node = html(t"<li>{c!s}</li><li>{c!r}</li><li>{'😊'!a}</li>") 

134 assert node == Fragment( 

135 children=[ 

136 Element("li", children=[Text("string")]), 

137 Element("li", children=[Text("repr")]), 

138 Element("li", children=[Text("'\\U0001f60a'")]), 

139 ], 

140 ) 

141 

142 

143def test_interpolated_in_content_node(): 

144 # https://github.com/t-strings/tdom/issues/68 

145 evil = "</style><script>alert('whoops');</script><style>" 

146 node = html(t"<style>{evil}</style>") 

147 assert node == Element( 

148 "style", 

149 children=[Text("</style><script>alert('whoops');</script><style>")], 

150 ) 

151 assert ( 

152 str(node) 

153 == "<style>&lt;/style&gt;&lt;script&gt;alert(&#39;whoops&#39;);&lt;/script&gt;&lt;style&gt;</style>" 

154 ) 

155 

156 

157def test_interpolated_trusted_in_content_node(): 

158 # https://github.com/t-strings/tdom/issues/68 

159 node = html(t"<script>if (a < b && c > d) { alert('wow'); } </script>") 

160 assert node == Element( 

161 "script", 

162 children=[Text(Markup("if (a < b && c > d) { alert('wow'); }"))], 

163 ) 

164 assert str(node) == ("<script>if (a < b && c > d) { alert('wow'); }</script>") 

165 

166 

167# -------------------------------------------------------------------------- 

168# Interpolated non-text content 

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

170 

171 

172def test_interpolated_false_content(): 

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

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

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

176 

177 

178def test_interpolated_none_content(): 

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

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

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

182 

183 

184def test_interpolated_zero_arg_function(): 

185 def get_value(): 

186 return "dynamic" 

187 

188 node = html(t"<p>The value is {get_value}.</p>") 

189 assert node == Element( 

190 "p", children=[Text("The value is "), Text("dynamic"), Text(".")] 

191 ) 

192 

193 

194def test_interpolated_multi_arg_function_fails(): 

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

196 return a + b 

197 

198 with pytest.raises(TypeError): 

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

200 

201 

202# -------------------------------------------------------------------------- 

203# Raw HTML injection tests 

204# -------------------------------------------------------------------------- 

205 

206 

207def test_raw_html_injection_with_markupsafe(): 

208 raw_content = Markup("<strong>I am bold</strong>") 

209 node = html(t"<div>{raw_content}</div>") 

210 assert node == Element("div", children=[Text(text=raw_content)]) 

211 assert str(node) == "<div><strong>I am bold</strong></div>" 

212 

213 

214def test_raw_html_injection_with_dunder_html_protocol(): 

215 class SafeContent: 

216 def __init__(self, text): 

217 self._text = text 

218 

219 def __html__(self): 

220 # In a real app, this would come from a sanitizer or trusted source 

221 return f"<em>{self._text}</em>" 

222 

223 content = SafeContent("emphasized") 

224 node = html(t"<p>Here is some {content}.</p>") 

225 assert node == Element( 

226 "p", 

227 children=[ 

228 Text("Here is some "), 

229 Text(Markup("<em>emphasized</em>")), 

230 Text("."), 

231 ], 

232 ) 

233 assert str(node) == "<p>Here is some <em>emphasized</em>.</p>" 

234 

235 

236def test_raw_html_injection_with_format_spec(): 

237 raw_content = "<u>underlined</u>" 

238 node = html(t"<p>This is {raw_content:safe} text.</p>") 

239 assert node == Element( 

240 "p", 

241 children=[ 

242 Text("This is "), 

243 Text(Markup(raw_content)), 

244 Text(" text."), 

245 ], 

246 ) 

247 assert str(node) == "<p>This is <u>underlined</u> text.</p>" 

248 

249 

250def test_raw_html_injection_with_markupsafe_unsafe_format_spec(): 

251 supposedly_safe = Markup("<i>italic</i>") 

252 node = html(t"<p>This is {supposedly_safe:unsafe} text.</p>") 

253 assert node == Element( 

254 "p", 

255 children=[ 

256 Text("This is "), 

257 Text(str(supposedly_safe)), 

258 Text(" text."), 

259 ], 

260 ) 

261 assert str(node) == "<p>This is &lt;i&gt;italic&lt;/i&gt; text.</p>" 

262 

263 

264# -------------------------------------------------------------------------- 

265# Conditional rendering and control flow 

266# -------------------------------------------------------------------------- 

267 

268 

269def test_conditional_rendering_with_if_else(): 

270 is_logged_in = True 

271 user_profile = t"<span>Welcome, User!</span>" 

272 login_prompt = t"<a href='/login'>Please log in</a>" 

273 node = html(t"<div>{user_profile if is_logged_in else login_prompt}</div>") 

274 

275 assert node == Element( 

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

277 ) 

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

279 

280 is_logged_in = False 

281 node = html(t"<div>{user_profile if is_logged_in else login_prompt}</div>") 

282 assert str(node) == '<div><a href="/login">Please log in</a></div>' 

283 

284 

285def test_conditional_rendering_with_and(): 

286 show_warning = True 

287 warning_message = t'<div class="warning">Warning!</div>' 

288 node = html(t"<main>{show_warning and warning_message}</main>") 

289 

290 assert node == Element( 

291 "main", 

292 children=[ 

293 Element("div", attrs={"class": "warning"}, children=[Text("Warning!")]), 

294 ], 

295 ) 

296 assert str(node) == '<main><div class="warning">Warning!</div></main>' 

297 

298 show_warning = False 

299 node = html(t"<main>{show_warning and warning_message}</main>") 

300 # Assuming False renders nothing 

301 assert str(node) == "<main></main>" 

302 

303 

304# -------------------------------------------------------------------------- 

305# Interpolated nesting of templates and elements 

306# -------------------------------------------------------------------------- 

307 

308 

309def test_interpolated_template_content(): 

310 child = t"<span>Child</span>" 

311 node = html(t"<div>{child}</div>") 

312 assert node == Element("div", children=[html(child)]) 

313 assert str(node) == "<div><span>Child</span></div>" 

314 

315 

316def test_interpolated_element_content(): 

317 child = html(t"<span>Child</span>") 

318 node = html(t"<div>{child}</div>") 

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

320 assert str(node) == "<div><span>Child</span></div>" 

321 

322 

323def test_interpolated_nonstring_content(): 

324 number = 42 

325 node = html(t"<p>The answer is {number}.</p>") 

326 assert node == Element( 

327 "p", children=[Text("The answer is "), Text("42"), Text(".")] 

328 ) 

329 assert str(node) == "<p>The answer is 42.</p>" 

330 

331 

332def test_list_items(): 

333 items = ["Apple", "Banana", "Cherry"] 

334 node = html(t"<ul>{[t'<li>{item}</li>' for item in items]}</ul>") 

335 assert node == Element( 

336 "ul", 

337 children=[ 

338 Element("li", children=[Text("Apple")]), 

339 Element("li", children=[Text("Banana")]), 

340 Element("li", children=[Text("Cherry")]), 

341 ], 

342 ) 

343 assert str(node) == "<ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>" 

344 

345 

346def test_nested_list_items(): 

347 # TODO XXX this is a pretty abusrd test case; clean it up when refactoring 

348 outer = ["fruit", "more fruit"] 

349 inner = ["apple", "banana", "cherry"] 

350 inner_items = [t"<li>{item}</li>" for item in inner] 

351 outer_items = [t"<li>{category}<ul>{inner_items}</ul></li>" for category in outer] 

352 node = html(t"<ul>{outer_items}</ul>") 

353 assert node == Element( 

354 "ul", 

355 children=[ 

356 Element( 

357 "li", 

358 children=[ 

359 Text("fruit"), 

360 Element( 

361 "ul", 

362 children=[ 

363 Element("li", children=[Text("apple")]), 

364 Element("li", children=[Text("banana")]), 

365 Element("li", children=[Text("cherry")]), 

366 ], 

367 ), 

368 ], 

369 ), 

370 Element( 

371 "li", 

372 children=[ 

373 Text("more fruit"), 

374 Element( 

375 "ul", 

376 children=[ 

377 Element("li", children=[Text("apple")]), 

378 Element("li", children=[Text("banana")]), 

379 Element("li", children=[Text("cherry")]), 

380 ], 

381 ), 

382 ], 

383 ), 

384 ], 

385 ) 

386 assert ( 

387 str(node) 

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

389 ) 

390 

391 

392# -------------------------------------------------------------------------- 

393# Interpolated attribute content 

394# -------------------------------------------------------------------------- 

395 

396 

397def test_interpolated_attribute_value(): 

398 url = "https://example.com/" 

399 node = html(t'<a href="{url}">Link</a>') 

400 assert node == Element( 

401 "a", attrs={"href": "https://example.com/"}, children=[Text("Link")] 

402 ) 

403 assert str(node) == '<a href="https://example.com/">Link</a>' 

404 

405 

406def test_escaping_of_interpolated_attribute_value(): 

407 url = 'https://example.com/?q="test"&lang=en' 

408 node = html(t'<a href="{url}">Link</a>') 

409 assert node == Element( 

410 "a", 

411 attrs={"href": Markup('https://example.com/?q="test"&lang=en')}, 

412 children=[Text("Link")], 

413 ) 

414 assert ( 

415 str(node) 

416 == '<a href="https://example.com/?q=&#34;test&#34;&amp;lang=en">Link</a>' 

417 ) 

418 

419 

420def test_interpolated_unquoted_attribute_value(): 

421 id = "roquefort" 

422 node = html(t"<div id={id}>Cheese</div>") 

423 assert node == Element("div", attrs={"id": "roquefort"}, children=[Text("Cheese")]) 

424 assert str(node) == '<div id="roquefort">Cheese</div>' 

425 

426 

427def test_interpolated_attribute_value_true(): 

428 disabled = True 

429 node = html(t"<button disabled={disabled}>Click me</button>") 

430 assert node == Element( 

431 "button", attrs={"disabled": None}, children=[Text("Click me")] 

432 ) 

433 assert str(node) == "<button disabled>Click me</button>" 

434 

435 

436def test_interpolated_attribute_value_falsy(): 

437 disabled = False 

438 crumpled = None 

439 node = html(t"<button disabled={disabled} crumpled={crumpled}>Click me</button>") 

440 assert node == Element("button", attrs={}, children=[Text("Click me")]) 

441 assert str(node) == "<button>Click me</button>" 

442 

443 

444def test_interpolated_attribute_spread_dict(): 

445 attrs = {"href": "https://example.com/", "target": "_blank"} 

446 node = html(t"<a {attrs}>Link</a>") 

447 assert node == Element( 

448 "a", 

449 attrs={"href": "https://example.com/", "target": "_blank"}, 

450 children=[Text("Link")], 

451 ) 

452 assert str(node) == '<a href="https://example.com/" target="_blank">Link</a>' 

453 

454 

455def test_interpolated_mixed_attribute_values_and_spread_dict(): 

456 attrs = {"href": "https://example.com/", "id": "link1"} 

457 target = "_blank" 

458 node = html(t'<a {attrs} target="{target}">Link</a>') 

459 assert node == Element( 

460 "a", 

461 attrs={"href": "https://example.com/", "id": "link1", "target": "_blank"}, 

462 children=[Text("Link")], 

463 ) 

464 assert ( 

465 str(node) 

466 == '<a href="https://example.com/" id="link1" target="_blank">Link</a>' 

467 ) 

468 

469 

470def test_multiple_attribute_spread_dicts(): 

471 attrs1 = {"href": "https://example.com/", "id": "overwrtten"} 

472 attrs2 = {"target": "_blank", "id": "link1"} 

473 node = html(t"<a {attrs1} {attrs2}>Link</a>") 

474 assert node == Element( 

475 "a", 

476 attrs={"href": "https://example.com/", "id": "link1", "target": "_blank"}, 

477 children=[Text("Link")], 

478 ) 

479 assert ( 

480 str(node) 

481 == '<a href="https://example.com/" id="link1" target="_blank">Link</a>' 

482 ) 

483 

484 

485def test_interpolated_class_attribute(): 

486 classes = ["btn", "btn-primary", False and "disabled", None, {"active": True}] 

487 node = html(t'<button class="{classes}">Click me</button>') 

488 assert node == Element( 

489 "button", 

490 attrs={"class": "btn btn-primary active"}, 

491 children=[Text("Click me")], 

492 ) 

493 assert str(node) == '<button class="btn btn-primary active">Click me</button>' 

494 

495 

496def test_interpolated_class_attribute_with_multiple_placeholders(): 

497 classes1 = ["btn", "btn-primary"] 

498 classes2 = [False and "disabled", None, {"active": True}] 

499 node = html(t'<button class="{classes1} {classes2}">Click me</button>') 

500 # CONSIDER: Is this what we want? Currently, when we have multiple 

501 # placeholders in a single attribute, we treat it as a string attribute. 

502 assert node == Element( 

503 "button", 

504 attrs={"class": "['btn', 'btn-primary'] [False, None, {'active': True}]"}, 

505 children=[Text("Click me")], 

506 ) 

507 

508 

509def test_interpolated_attribute_spread_with_class_attribute(): 

510 attrs = {"id": "button1", "class": ["btn", "btn-primary"]} 

511 node = html(t"<button {attrs}>Click me</button>") 

512 assert node == Element( 

513 "button", 

514 attrs={"id": "button1", "class": "btn btn-primary"}, 

515 children=[Text("Click me")], 

516 ) 

517 assert str(node) == '<button id="button1" class="btn btn-primary">Click me</button>' 

518 

519 

520def test_interpolated_attribute_value_embedded_placeholder(): 

521 slug = "item42" 

522 node = html(t"<div data-id='prefix-{slug}'></div>") 

523 assert node == Element( 

524 "div", 

525 attrs={"data-id": "prefix-item42"}, 

526 children=[], 

527 ) 

528 assert str(node) == '<div data-id="prefix-item42"></div>' 

529 

530 

531def test_interpolated_attribute_value_with_static_prefix_and_suffix(): 

532 counter = 3 

533 node = html(t'<div data-id="item-{counter}-suffix"></div>') 

534 assert node == Element( 

535 "div", 

536 attrs={"data-id": "item-3-suffix"}, 

537 children=[], 

538 ) 

539 assert str(node) == '<div data-id="item-3-suffix"></div>' 

540 

541 

542def test_attribute_value_empty_string(): 

543 node = html(t'<div data-id=""></div>') 

544 assert node == Element( 

545 "div", 

546 attrs={"data-id": ""}, 

547 children=[], 

548 ) 

549 

550 

551def test_interpolated_attribute_value_multiple_placeholders(): 

552 start = 1 

553 end = 5 

554 node = html(t'<div data-range="{start}-{end}"></div>') 

555 assert node == Element( 

556 "div", 

557 attrs={"data-range": "1-5"}, 

558 children=[], 

559 ) 

560 assert str(node) == '<div data-range="1-5"></div>' 

561 

562 

563def test_interpolated_attribute_value_tricky_multiple_placeholders(): 

564 start = "start" 

565 end = "end" 

566 node = html(t'<div data-range="{start}5-and-{end}12"></div>') 

567 assert node == Element( 

568 "div", 

569 attrs={"data-range": "start5-and-end12"}, 

570 children=[], 

571 ) 

572 assert str(node) == '<div data-range="start5-and-end12"></div>' 

573 

574 

575def test_placeholder_collision_avoidance(): 

576 # This test is to ensure that our placeholder detection avoids collisions 

577 # even with content that might look like a placeholder. 

578 tricky = "123" 

579 template = Template( 

580 '<div data-tricky="', 

581 _PLACEHOLDER_PREFIX, 

582 Interpolation(tricky, "tricky"), 

583 _PLACEHOLDER_SUFFIX, 

584 '"></div>', 

585 ) 

586 node = html(template) 

587 assert node == Element( 

588 "div", 

589 attrs={"data-tricky": _PLACEHOLDER_PREFIX + tricky + _PLACEHOLDER_SUFFIX}, 

590 children=[], 

591 ) 

592 assert ( 

593 str(node) 

594 == f'<div data-tricky="{_PLACEHOLDER_PREFIX}{tricky}{_PLACEHOLDER_SUFFIX}"></div>' 

595 ) 

596 

597 

598def test_interpolated_attribute_value_multiple_placeholders_no_quotes(): 

599 start = 1 

600 end = 5 

601 node = html(t"<div data-range={start}-{end}></div>") 

602 assert node == Element( 

603 "div", 

604 attrs={"data-range": "1-5"}, 

605 children=[], 

606 ) 

607 assert str(node) == '<div data-range="1-5"></div>' 

608 

609 

610def test_interpolated_data_attributes(): 

611 data = {"user-id": 123, "role": "admin", "wild": True} 

612 node = html(t"<div data={data}>User Info</div>") 

613 assert node == Element( 

614 "div", 

615 attrs={"data-user-id": "123", "data-role": "admin", "data-wild": None}, 

616 children=[Text("User Info")], 

617 ) 

618 assert ( 

619 str(node) 

620 == '<div data-user-id="123" data-role="admin" data-wild>User Info</div>' 

621 ) 

622 

623 

624def test_interpolated_data_attribute_multiple_placeholders(): 

625 confusing = {"user-id": "user-123"} 

626 placeholders = {"role": "admin"} 

627 with pytest.raises(TypeError): 

628 _ = html(t'<div data="{confusing} {placeholders}">User Info</div>') 

629 

630 

631def test_interpolated_aria_attributes(): 

632 aria = {"label": "Close", "hidden": True, "another": False, "more": None} 

633 node = html(t"<button aria={aria}>X</button>") 

634 assert node == Element( 

635 "button", 

636 attrs={"aria-label": "Close", "aria-hidden": "true", "aria-another": "false"}, 

637 children=[Text("X")], 

638 ) 

639 assert ( 

640 str(node) 

641 == '<button aria-label="Close" aria-hidden="true" aria-another="false">X</button>' 

642 ) 

643 

644 

645def test_interpolated_aria_attribute_multiple_placeholders(): 

646 confusing = {"label": "Close"} 

647 placeholders = {"hidden": True} 

648 with pytest.raises(TypeError): 

649 _ = html(t'<button aria="{confusing} {placeholders}">X</button>') 

650 

651 

652def test_interpolated_style_attribute(): 

653 styles = {"color": "red", "font-weight": "bold", "font-size": "16px"} 

654 node = html(t"<p style={styles}>Warning!</p>") 

655 assert node == Element( 

656 "p", 

657 attrs={"style": "color: red; font-weight: bold; font-size: 16px"}, 

658 children=[Text("Warning!")], 

659 ) 

660 assert ( 

661 str(node) 

662 == '<p style="color: red; font-weight: bold; font-size: 16px">Warning!</p>' 

663 ) 

664 

665 

666def test_interpolated_style_attribute_multiple_placeholders(): 

667 styles1 = {"color": "red"} 

668 styles2 = {"font-weight": "bold"} 

669 node = html(t"<p style='{styles1} {styles2}'>Warning!</p>") 

670 # CONSIDER: Is this what we want? Currently, when we have multiple 

671 # placeholders in a single attribute, we treat it as a string attribute. 

672 assert node == Element( 

673 "p", 

674 attrs={"style": "{'color': 'red'} {'font-weight': 'bold'}"}, 

675 children=[Text("Warning!")], 

676 ) 

677 

678 

679def test_style_attribute_str(): 

680 styles = "color: red; font-weight: bold;" 

681 node = html(t"<p style={styles}>Warning!</p>") 

682 assert node == Element( 

683 "p", 

684 attrs={"style": "color: red; font-weight: bold;"}, 

685 children=[Text("Warning!")], 

686 ) 

687 assert str(node) == '<p style="color: red; font-weight: bold;">Warning!</p>' 

688 

689 

690def test_style_attribute_non_str_non_dict(): 

691 with pytest.raises(TypeError): 

692 styles = [1, 2] 

693 _ = html(t"<p style={styles}>Warning!</p>") 

694 

695 

696# -------------------------------------------------------------------------- 

697# Function component interpolation tests 

698# -------------------------------------------------------------------------- 

699 

700 

701def FunctionComponent( 

702 children: t.Iterable[Node], first: str, second: int, third_arg: str, **attrs: t.Any 

703) -> Template: 

704 # Ensure type correctness of props at runtime for testing purposes 

705 assert isinstance(first, str) 

706 assert isinstance(second, int) 

707 assert isinstance(third_arg, str) 

708 new_attrs = { 

709 "id": third_arg, 

710 "data": {"first": first, "second": second}, 

711 **attrs, 

712 } 

713 return t"<div {new_attrs}>Component: {children}</div>" 

714 

715 

716def test_interpolated_template_component(): 

717 node = html( 

718 t'<{FunctionComponent} first=1 second={99} third-arg="comp1" class="my-comp">Hello, Component!</{FunctionComponent}>' 

719 ) 

720 assert node == Element( 

721 "div", 

722 attrs={ 

723 "id": "comp1", 

724 "data-first": "1", 

725 "data-second": "99", 

726 "class": "my-comp", 

727 }, 

728 children=[Text("Component: "), Text("Hello, Component!")], 

729 ) 

730 assert ( 

731 str(node) 

732 == '<div id="comp1" data-first="1" data-second="99" class="my-comp">Component: Hello, Component!</div>' 

733 ) 

734 

735 

736def test_interpolated_template_component_no_children_provided(): 

737 """Same test, but the caller didn't provide any children.""" 

738 node = html( 

739 t'<{FunctionComponent} first=1 second={99} third-arg="comp1" class="my-comp" />' 

740 ) 

741 assert node == Element( 

742 "div", 

743 attrs={ 

744 "id": "comp1", 

745 "data-first": "1", 

746 "data-second": "99", 

747 "class": "my-comp", 

748 }, 

749 children=[ 

750 Text("Component: "), 

751 ], 

752 ) 

753 assert ( 

754 str(node) 

755 == '<div id="comp1" data-first="1" data-second="99" class="my-comp">Component: </div>' 

756 ) 

757 

758 

759def test_invalid_component_invocation(): 

760 with pytest.raises(TypeError): 

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

762 

763 

764def FunctionComponentNoChildren(first: str, second: int, third_arg: str) -> Template: 

765 # Ensure type correctness of props at runtime for testing purposes 

766 assert isinstance(first, str) 

767 assert isinstance(second, int) 

768 assert isinstance(third_arg, str) 

769 new_attrs = { 

770 "id": third_arg, 

771 "data": {"first": first, "second": second}, 

772 } 

773 return t"<div {new_attrs}>Component: ignore children</div>" 

774 

775 

776def test_interpolated_template_component_ignore_children(): 

777 node = html( 

778 t'<{FunctionComponentNoChildren} first=1 second={99} third-arg="comp1">Hello, Component!</{FunctionComponentNoChildren}>' 

779 ) 

780 assert node == Element( 

781 "div", 

782 attrs={ 

783 "id": "comp1", 

784 "data-first": "1", 

785 "data-second": "99", 

786 }, 

787 children=[Text(text="Component: ignore children")], 

788 ) 

789 assert ( 

790 str(node) 

791 == '<div id="comp1" data-first="1" data-second="99">Component: ignore children</div>' 

792 ) 

793 

794 

795def FunctionComponentKeywordArgs(first: str, **attrs: t.Any) -> Template: 

796 # Ensure type correctness of props at runtime for testing purposes 

797 assert isinstance(first, str) 

798 assert "children" in attrs 

799 _ = attrs.pop("children") 

800 new_attrs = {"data-first": first, **attrs} 

801 return t"<div {new_attrs}>Component with kwargs</div>" 

802 

803 

804def test_children_always_passed_via_kwargs(): 

805 node = html( 

806 t'<{FunctionComponentKeywordArgs} first="value" extra="info">Child content</{FunctionComponentKeywordArgs}>' 

807 ) 

808 assert node == Element( 

809 "div", 

810 attrs={ 

811 "data-first": "value", 

812 "extra": "info", 

813 }, 

814 children=[Text("Component with kwargs")], 

815 ) 

816 assert ( 

817 str(node) == '<div data-first="value" extra="info">Component with kwargs</div>' 

818 ) 

819 

820 

821def test_children_always_passed_via_kwargs_even_when_empty(): 

822 node = html(t'<{FunctionComponentKeywordArgs} first="value" extra="info" />') 

823 assert node == Element( 

824 "div", 

825 attrs={ 

826 "data-first": "value", 

827 "extra": "info", 

828 }, 

829 children=[Text("Component with kwargs")], 

830 ) 

831 assert ( 

832 str(node) == '<div data-first="value" extra="info">Component with kwargs</div>' 

833 ) 

834 

835 

836def ColumnsComponent() -> Template: 

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

838 

839 

840def test_fragment_from_component(): 

841 # This test assumes that if a component returns a template that parses 

842 # into multiple root elements, they are treated as a fragment. 

843 node = html(t"<table><tr><{ColumnsComponent} /></tr></table>") 

844 assert node == Element( 

845 "table", 

846 children=[ 

847 Element( 

848 "tr", 

849 children=[ 

850 Element("td", children=[Text("Column 1")]), 

851 Element("td", children=[Text("Column 2")]), 

852 ], 

853 ), 

854 ], 

855 ) 

856 assert str(node) == "<table><tr><td>Column 1</td><td>Column 2</td></tr></table>" 

857 

858 

859def test_component_passed_as_attr_value(): 

860 def Wrapper( 

861 children: t.Iterable[Node], sub_component: t.Callable, **attrs: t.Any 

862 ) -> Template: 

863 return t"<{sub_component} {attrs}>{children}</{sub_component}>" 

864 

865 node = html( 

866 t'<{Wrapper} sub-component={FunctionComponent} class="wrapped" first=1 second={99} third-arg="comp1"><p>Inside wrapper</p></{Wrapper}>' 

867 ) 

868 assert node == Element( 

869 "div", 

870 attrs={ 

871 "id": "comp1", 

872 "data-first": "1", 

873 "data-second": "99", 

874 "class": "wrapped", 

875 }, 

876 children=[Text("Component: "), Element("p", children=[Text("Inside wrapper")])], 

877 ) 

878 assert ( 

879 str(node) 

880 == '<div id="comp1" data-first="1" data-second="99" class="wrapped">Component: <p>Inside wrapper</p></div>' 

881 ) 

882 

883 

884def test_nested_component_gh23(): 

885 # See https://github.com/t-strings/tdom/issues/23 for context 

886 def Header(): 

887 return html(t"{'Hello World'}") 

888 

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

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

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

892 

893 

894def test_component_returning_iterable(): 

895 def Items() -> t.Iterable: 

896 for i in range(2): 

897 yield t"<li>Item {i + 1}</li>" 

898 yield html(t"<li>Item {3}</li>") 

899 

900 node = html(t"<ul><{Items} /></ul>") 

901 assert node == Element( 

902 "ul", 

903 children=[ 

904 Element("li", children=[Text("Item "), Text("1")]), 

905 Element("li", children=[Text("Item "), Text("2")]), 

906 Element("li", children=[Text("Item "), Text("3")]), 

907 ], 

908 ) 

909 assert str(node) == "<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>" 

910 

911 

912def test_component_returning_explicit_fragment(): 

913 def Items() -> Node: 

914 return html(t"<><li>Item {1}</li><li>Item {2}</li><li>Item {3}</li></>") 

915 

916 node = html(t"<ul><{Items} /></ul>") 

917 assert node == Element( 

918 "ul", 

919 children=[ 

920 Element("li", children=[Text("Item "), Text("1")]), 

921 Element("li", children=[Text("Item "), Text("2")]), 

922 Element("li", children=[Text("Item "), Text("3")]), 

923 ], 

924 ) 

925 assert str(node) == "<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>" 

926 

927 

928@dataclass 

929class ClassComponent: 

930 """Example class-based component.""" 

931 

932 user_name: str 

933 image_url: str 

934 homepage: str = "#" 

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

936 

937 def __call__(self) -> Node: 

938 return html( 

939 t"<div class='avatar'>" 

940 t"<a href={self.homepage}>" 

941 t"<img src='{self.image_url}' alt='{f'Avatar of {self.user_name}'}' />" 

942 t"</a>" 

943 t"<span>{self.user_name}</span>" 

944 t"{self.children}" 

945 t"</div>", 

946 ) 

947 

948 

949def test_class_component_implicit_invocation(): 

950 node = html( 

951 t"<{ClassComponent} user-name='Alice' image-url='https://example.com/alice.png'>Fun times!</{ClassComponent}>" 

952 ) 

953 assert node == Element( 

954 "div", 

955 attrs={"class": "avatar"}, 

956 children=[ 

957 Element( 

958 "a", 

959 attrs={"href": "#"}, 

960 children=[ 

961 Element( 

962 "img", 

963 attrs={ 

964 "src": "https://example.com/alice.png", 

965 "alt": "Avatar of Alice", 

966 }, 

967 ) 

968 ], 

969 ), 

970 Element("span", children=[Text("Alice")]), 

971 Text("Fun times!"), 

972 ], 

973 ) 

974 assert ( 

975 str(node) 

976 == '<div class="avatar"><a href="#"><img src="https://example.com/alice.png" alt="Avatar of Alice" /></a><span>Alice</span>Fun times!</div>' 

977 ) 

978 

979 

980def test_class_component_direct_invocation(): 

981 avatar = ClassComponent( 

982 user_name="Alice", 

983 image_url="https://example.com/alice.png", 

984 homepage="https://example.com/users/alice", 

985 ) 

986 node = html(t"<{avatar} />") 

987 assert node == Element( 

988 "div", 

989 attrs={"class": "avatar"}, 

990 children=[ 

991 Element( 

992 "a", 

993 attrs={"href": "https://example.com/users/alice"}, 

994 children=[ 

995 Element( 

996 "img", 

997 attrs={ 

998 "src": "https://example.com/alice.png", 

999 "alt": "Avatar of Alice", 

1000 }, 

1001 ) 

1002 ], 

1003 ), 

1004 Element("span", children=[Text("Alice")]), 

1005 ], 

1006 ) 

1007 assert ( 

1008 str(node) 

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

1010 ) 

1011 

1012 

1013@dataclass 

1014class ClassComponentNoChildren: 

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

1016 

1017 user_name: str 

1018 image_url: str 

1019 homepage: str = "#" 

1020 

1021 def __call__(self) -> Node: 

1022 return html( 

1023 t"<div class='avatar'>" 

1024 t"<a href={self.homepage}>" 

1025 t"<img src='{self.image_url}' alt='{f'Avatar of {self.user_name}'}' />" 

1026 t"</a>" 

1027 t"<span>{self.user_name}</span>" 

1028 t"ignore children" 

1029 t"</div>", 

1030 ) 

1031 

1032 

1033def test_class_component_implicit_invocation_ignore_children(): 

1034 node = html( 

1035 t"<{ClassComponentNoChildren} user-name='Alice' image-url='https://example.com/alice.png'>Fun times!</{ClassComponentNoChildren}>" 

1036 ) 

1037 assert node == Element( 

1038 "div", 

1039 attrs={"class": "avatar"}, 

1040 children=[ 

1041 Element( 

1042 "a", 

1043 attrs={"href": "#"}, 

1044 children=[ 

1045 Element( 

1046 "img", 

1047 attrs={ 

1048 "src": "https://example.com/alice.png", 

1049 "alt": "Avatar of Alice", 

1050 }, 

1051 ) 

1052 ], 

1053 ), 

1054 Element("span", children=[Text("Alice")]), 

1055 Text("ignore children"), 

1056 ], 

1057 ) 

1058 assert ( 

1059 str(node) 

1060 == '<div class="avatar"><a href="#"><img src="https://example.com/alice.png" alt="Avatar of Alice" /></a><span>Alice</span>ignore children</div>' 

1061 ) 

1062 

1063 

1064def AttributeTypeComponent( 

1065 data_int: int, 

1066 data_true: bool, 

1067 data_false: bool, 

1068 data_none: None, 

1069 data_float: float, 

1070 data_dt: datetime.datetime, 

1071) -> Template: 

1072 """Component to test that we don't incorrectly convert attribute types.""" 

1073 assert isinstance(data_int, int) 

1074 assert data_true is True 

1075 assert data_false is False 

1076 assert data_none is None 

1077 assert isinstance(data_float, float) 

1078 assert isinstance(data_dt, datetime.datetime) 

1079 return t"Looks good!" 

1080 

1081 

1082def test_attribute_type_component(): 

1083 an_int: int = 42 

1084 a_true: bool = True 

1085 a_false: bool = False 

1086 a_none: None = None 

1087 a_float: float = 3.14 

1088 a_dt: datetime.datetime = datetime.datetime(2024, 1, 1, 12, 0, 0) 

1089 node = html( 

1090 t"<{AttributeTypeComponent} data-int={an_int} data-true={a_true} " 

1091 t"data-false={a_false} data-none={a_none} data-float={a_float} " 

1092 t"data-dt={a_dt} />" 

1093 ) 

1094 assert node == Text("Looks good!") 

1095 assert str(node) == "Looks good!" 

1096 

1097 

1098def test_component_non_callable_fails(): 

1099 with pytest.raises(TypeError): 

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

1101 

1102 

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

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

1105 

1106 

1107def test_component_requiring_positional_arg_fails(): 

1108 with pytest.raises(TypeError): 

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