Coverage for tdom / processor_test.py: 99%

483 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-17 23:32 +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 Comment, DocumentType, Element, Fragment, Node, Text 

10from .placeholders import _PLACEHOLDER_PREFIX, _PLACEHOLDER_SUFFIX 

11from .processor import html 

12 

13# -------------------------------------------------------------------------- 

14# Basic HTML parsing tests 

15# -------------------------------------------------------------------------- 

16 

17 

18def test_parse_empty(): 

19 node = html(t"") 

20 assert node == Fragment(children=[]) 

21 assert str(node) == "" 

22 

23 

24def test_parse_text(): 

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

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

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

28 

29 

30def test_parse_comment(): 

31 node = html(t"<!--This is a comment-->") 

32 assert node == Comment("This is a comment") 

33 assert str(node) == "<!--This is a comment-->" 

34 

35 

36def test_parse_document_type(): 

37 node = html(t"<!doctype html>") 

38 assert node == DocumentType("html") 

39 assert str(node) == "<!DOCTYPE html>" 

40 

41 

42def test_parse_void_element(): 

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

44 assert node == Element("br") 

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

46 

47 

48def test_parse_void_element_self_closed(): 

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

50 assert node == Element("br") 

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

52 

53 

54def test_parse_chain_of_void_elements(): 

55 # Make sure our handling of CPython issue #69445 is reasonable. 

56 node = html(t"<br><hr><img src='image.png' /><br /><hr>") 

57 assert node == Fragment( 

58 children=[ 

59 Element("br"), 

60 Element("hr"), 

61 Element("img", attrs={"src": "image.png"}), 

62 Element("br"), 

63 Element("hr"), 

64 ], 

65 ) 

66 assert str(node) == '<br /><hr /><img src="image.png" /><br /><hr />' 

67 

68 

69def test_static_boolean_attr_retained(): 

70 # Make sure a boolean attribute (bare attribute) is not omitted. 

71 node = html(t"<input disabled>") 

72 assert node == Element("input", {"disabled": None}) 

73 assert str(node) == "<input disabled />" 

74 

75 

76def test_parse_element_with_text(): 

77 node = html(t"<p>Hello, world!</p>") 

78 assert node == Element( 

79 "p", 

80 children=[ 

81 Text("Hello, world!"), 

82 ], 

83 ) 

84 assert str(node) == "<p>Hello, world!</p>" 

85 

86 

87def test_parse_element_with_attributes(): 

88 node = html(t'<a href="https://example.com" target="_blank">Link</a>') 

89 assert node == Element( 

90 "a", 

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

92 children=[ 

93 Text("Link"), 

94 ], 

95 ) 

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

97 

98 

99def test_parse_nested_elements(): 

100 node = html(t"<div><p>Hello</p><p>World</p></div>") 

101 assert node == Element( 

102 "div", 

103 children=[ 

104 Element("p", children=[Text("Hello")]), 

105 Element("p", children=[Text("World")]), 

106 ], 

107 ) 

108 assert str(node) == "<div><p>Hello</p><p>World</p></div>" 

109 

110 

111def test_parse_entities_are_escaped(): 

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

113 assert node == Element( 

114 "p", 

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

116 ) 

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

118 

119 

120# -------------------------------------------------------------------------- 

121# Interpolated text content 

122# -------------------------------------------------------------------------- 

123 

124 

125def test_interpolated_text_content(): 

126 name = "Alice" 

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

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

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

130 

131 

132def test_escaping_of_interpolated_text_content(): 

133 name = "<Alice & Bob>" 

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

135 assert node == Element( 

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

137 ) 

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

139 

140 

141class Convertible: 

142 def __str__(self): 

143 return "string" 

144 

145 def __repr__(self): 

146 return "repr" 

147 

148 

149def test_conversions(): 

150 c = Convertible() 

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

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

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

154 assert node == Fragment( 

155 children=[ 

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

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

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

159 ], 

160 ) 

161 

162 

163def test_interpolated_in_content_node(): 

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

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

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

167 assert node == Element( 

168 "style", 

169 children=[ 

170 Text("</style><script>alert('whoops');</script><style>"), 

171 Text("</style><script>alert('whoops');</script><style>"), 

172 ], 

173 ) 

174 LT = "&lt;" 

175 assert ( 

176 str(node) 

177 == f"<style>{LT}/style><script>alert('whoops');</script><style>{LT}/style><script>alert('whoops');</script><style></style>" 

178 ) 

179 

180 

181def test_interpolated_trusted_in_content_node(): 

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

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

184 assert node == Element( 

185 "script", 

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

187 ) 

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

189 

190 

191# -------------------------------------------------------------------------- 

192# Interpolated non-text content 

193# -------------------------------------------------------------------------- 

194 

195 

196def test_interpolated_false_content(): 

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

198 assert node == Element("div") 

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

200 

201 

202def test_interpolated_none_content(): 

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

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

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

206 

207 

208def test_interpolated_zero_arg_function(): 

209 def get_value(): 

210 return "dynamic" 

211 

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

213 assert node == Element( 

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

215 ) 

216 

217 

218def test_interpolated_multi_arg_function_fails(): 

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

220 return a + b 

221 

222 with pytest.raises(TypeError): 

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

224 

225 

226# -------------------------------------------------------------------------- 

227# Raw HTML injection tests 

228# -------------------------------------------------------------------------- 

229 

230 

231def test_raw_html_injection_with_markupsafe(): 

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

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

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

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

236 

237 

238def test_raw_html_injection_with_dunder_html_protocol(): 

239 class SafeContent: 

240 def __init__(self, text): 

241 self._text = text 

242 

243 def __html__(self): 

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

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

246 

247 content = SafeContent("emphasized") 

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

249 assert node == Element( 

250 "p", 

251 children=[ 

252 Text("Here is some "), 

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

254 Text("."), 

255 ], 

256 ) 

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

258 

259 

260def test_raw_html_injection_with_format_spec(): 

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

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

263 assert node == Element( 

264 "p", 

265 children=[ 

266 Text("This is "), 

267 Text(Markup(raw_content)), 

268 Text(" text."), 

269 ], 

270 ) 

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

272 

273 

274def test_raw_html_injection_with_markupsafe_unsafe_format_spec(): 

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

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

277 assert node == Element( 

278 "p", 

279 children=[ 

280 Text("This is "), 

281 Text(str(supposedly_safe)), 

282 Text(" text."), 

283 ], 

284 ) 

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

286 

287 

288# -------------------------------------------------------------------------- 

289# Conditional rendering and control flow 

290# -------------------------------------------------------------------------- 

291 

292 

293def test_conditional_rendering_with_if_else(): 

294 is_logged_in = True 

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

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

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

298 

299 assert node == Element( 

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

301 ) 

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

303 

304 is_logged_in = False 

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

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

307 

308 

309def test_conditional_rendering_with_and(): 

310 show_warning = True 

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

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

313 

314 assert node == Element( 

315 "main", 

316 children=[ 

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

318 ], 

319 ) 

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

321 

322 show_warning = False 

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

324 # Assuming False renders nothing 

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

326 

327 

328# -------------------------------------------------------------------------- 

329# Interpolated nesting of templates and elements 

330# -------------------------------------------------------------------------- 

331 

332 

333def test_interpolated_template_content(): 

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

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

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

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

338 

339 

340def test_interpolated_element_content(): 

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

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

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

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

345 

346 

347def test_interpolated_nonstring_content(): 

348 number = 42 

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

350 assert node == Element( 

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

352 ) 

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

354 

355 

356def test_list_items(): 

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

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

359 assert node == Element( 

360 "ul", 

361 children=[ 

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

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

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

365 ], 

366 ) 

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

368 

369 

370def test_nested_list_items(): 

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

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

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

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

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

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

377 assert node == Element( 

378 "ul", 

379 children=[ 

380 Element( 

381 "li", 

382 children=[ 

383 Text("fruit"), 

384 Element( 

385 "ul", 

386 children=[ 

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

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

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

390 ], 

391 ), 

392 ], 

393 ), 

394 Element( 

395 "li", 

396 children=[ 

397 Text("more fruit"), 

398 Element( 

399 "ul", 

400 children=[ 

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

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

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

404 ], 

405 ), 

406 ], 

407 ), 

408 ], 

409 ) 

410 assert ( 

411 str(node) 

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

413 ) 

414 

415 

416# -------------------------------------------------------------------------- 

417# Interpolated attribute content 

418# -------------------------------------------------------------------------- 

419 

420 

421def test_interpolated_attribute_value(): 

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

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

424 assert node == Element( 

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

426 ) 

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

428 

429 

430def test_escaping_of_interpolated_attribute_value(): 

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

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

433 assert node == Element( 

434 "a", 

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

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

437 ) 

438 assert ( 

439 str(node) 

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

441 ) 

442 

443 

444def test_interpolated_unquoted_attribute_value(): 

445 id = "roquefort" 

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

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

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

449 

450 

451def test_interpolated_attribute_value_true(): 

452 disabled = True 

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

454 assert node == Element( 

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

456 ) 

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

458 

459 

460def test_interpolated_attribute_value_falsy(): 

461 disabled = False 

462 crumpled = None 

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

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

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

466 

467 

468def test_interpolated_attribute_spread_dict(): 

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

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

471 assert node == Element( 

472 "a", 

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

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

475 ) 

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

477 

478 

479def test_interpolated_mixed_attribute_values_and_spread_dict(): 

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

481 target = "_blank" 

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

483 assert node == Element( 

484 "a", 

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

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

487 ) 

488 assert ( 

489 str(node) 

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

491 ) 

492 

493 

494def test_multiple_attribute_spread_dicts(): 

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

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

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

498 assert node == Element( 

499 "a", 

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

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

502 ) 

503 assert ( 

504 str(node) 

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

506 ) 

507 

508 

509def test_interpolated_class_attribute(): 

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

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

512 assert node == Element( 

513 "button", 

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

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

516 ) 

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

518 

519 

520def test_interpolated_class_attribute_with_multiple_placeholders(): 

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

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

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

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

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

526 assert node == Element( 

527 "button", 

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

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

530 ) 

531 

532 

533def test_interpolated_attribute_spread_with_class_attribute(): 

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

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

536 assert node == Element( 

537 "button", 

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

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

540 ) 

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

542 

543 

544def test_interpolated_attribute_value_embedded_placeholder(): 

545 slug = "item42" 

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

547 assert node == Element( 

548 "div", 

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

550 children=[], 

551 ) 

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

553 

554 

555def test_interpolated_attribute_value_with_static_prefix_and_suffix(): 

556 counter = 3 

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

558 assert node == Element( 

559 "div", 

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

561 children=[], 

562 ) 

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

564 

565 

566def test_attribute_value_empty_string(): 

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

568 assert node == Element( 

569 "div", 

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

571 children=[], 

572 ) 

573 

574 

575def test_interpolated_attribute_value_multiple_placeholders(): 

576 start = 1 

577 end = 5 

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

579 assert node == Element( 

580 "div", 

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

582 children=[], 

583 ) 

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

585 

586 

587def test_interpolated_attribute_value_tricky_multiple_placeholders(): 

588 start = "start" 

589 end = "end" 

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

591 assert node == Element( 

592 "div", 

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

594 children=[], 

595 ) 

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

597 

598 

599def test_placeholder_collision_avoidance(): 

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

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

602 tricky = "123" 

603 template = Template( 

604 '<div data-tricky="', 

605 _PLACEHOLDER_PREFIX, 

606 Interpolation(tricky, "tricky"), 

607 _PLACEHOLDER_SUFFIX, 

608 '"></div>', 

609 ) 

610 node = html(template) 

611 assert node == Element( 

612 "div", 

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

614 children=[], 

615 ) 

616 assert ( 

617 str(node) 

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

619 ) 

620 

621 

622def test_interpolated_attribute_value_multiple_placeholders_no_quotes(): 

623 start = 1 

624 end = 5 

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

626 assert node == Element( 

627 "div", 

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

629 children=[], 

630 ) 

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

632 

633 

634def test_interpolated_data_attributes(): 

635 data = {"user-id": 123, "role": "admin", "wild": True, "false": False, "none": None} 

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

637 assert node == Element( 

638 "div", 

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

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

641 ) 

642 assert ( 

643 str(node) 

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

645 ) 

646 

647 

648def test_data_attr_toggle_to_str(): 

649 for node in [ 

650 html(t"<div data-selected data={dict(selected='yes')}></div>"), 

651 html(t'<div data-selected="no" data={dict(selected="yes")}></div>'), 

652 ]: 

653 assert node == Element("div", {"data-selected": "yes"}) 

654 assert str(node) == '<div data-selected="yes"></div>' 

655 

656 

657def test_data_attr_toggle_to_true(): 

658 node = html(t'<div data-selected="yes" data={dict(selected=True)}></div>') 

659 assert node == Element("div", {"data-selected": None}) 

660 assert str(node) == "<div data-selected></div>" 

661 

662 

663def test_data_attr_unrelated_unaffected(): 

664 node = html(t"<div data-selected data={dict(active=True)}></div>") 

665 assert node == Element("div", {"data-selected": None, "data-active": None}) 

666 assert str(node) == "<div data-selected data-active></div>" 

667 

668 

669@pytest.mark.skip(reason="Waiting on attribute resolution ... resolution.") 

670def test_interpolated_data_attribute_multiple_placeholders(): 

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

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

673 with pytest.raises(TypeError): 

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

675 print(str(node)) 

676 

677 

678def test_interpolated_aria_attributes(): 

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

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

681 assert node == Element( 

682 "button", 

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

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

685 ) 

686 assert ( 

687 str(node) 

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

689 ) 

690 

691 

692def test_interpolated_style_attribute(): 

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

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

695 assert node == Element( 

696 "p", 

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

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

699 ) 

700 assert ( 

701 str(node) 

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

703 ) 

704 

705 

706def test_override_static_style_str_str(): 

707 node = html(t'<p style="font-color: red" {dict(style="font-size: 15px")}></p>') 

708 assert node == Element("p", {"style": "font-size: 15px"}) 

709 assert str(node) == '<p style="font-size: 15px"></p>' 

710 

711 

712def test_override_static_style_str_builder(): 

713 node = html(t'<p style="font-color: red" {dict(style={"font-size": "15px"})}></p>') 

714 assert node == Element("p", {"style": "font-size: 15px"}) 

715 assert str(node) == '<p style="font-size: 15px"></p>' 

716 

717 

718def test_interpolated_style_attribute_multiple_placeholders(): 

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

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

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

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

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

724 assert node == Element( 

725 "p", 

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

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

728 ) 

729 

730 

731def test_style_attribute_str(): 

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

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

734 assert node == Element( 

735 "p", 

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

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

738 ) 

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

740 

741 

742def test_style_attribute_non_str_non_dict(): 

743 with pytest.raises(TypeError): 

744 styles = [1, 2] 

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

746 

747 

748def test_special_attrs_as_static(): 

749 node = html(t'<p aria="aria?" data="data?" class="class?" style="style?"></p>') 

750 assert node == Element( 

751 "p", 

752 attrs={"aria": "aria?", "data": "data?", "class": "class?", "style": "style?"}, 

753 ) 

754 

755 

756# -------------------------------------------------------------------------- 

757# Function component interpolation tests 

758# -------------------------------------------------------------------------- 

759 

760 

761def FunctionComponent( 

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

763) -> Template: 

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

765 assert isinstance(first, str) 

766 assert isinstance(second, int) 

767 assert isinstance(third_arg, str) 

768 new_attrs = { 

769 "id": third_arg, 

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

771 **attrs, 

772 } 

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

774 

775 

776def test_interpolated_template_component(): 

777 node = html( 

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

779 ) 

780 assert node == Element( 

781 "div", 

782 attrs={ 

783 "id": "comp1", 

784 "data-first": "1", 

785 "data-second": "99", 

786 "class": "my-comp", 

787 }, 

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

789 ) 

790 assert ( 

791 str(node) 

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

793 ) 

794 

795 

796def test_interpolated_template_component_no_children_provided(): 

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

798 node = html( 

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

800 ) 

801 assert node == Element( 

802 "div", 

803 attrs={ 

804 "id": "comp1", 

805 "data-first": "1", 

806 "data-second": "99", 

807 "class": "my-comp", 

808 }, 

809 children=[ 

810 Text("Component: "), 

811 ], 

812 ) 

813 assert ( 

814 str(node) 

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

816 ) 

817 

818 

819def test_invalid_component_invocation(): 

820 with pytest.raises(TypeError): 

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

822 

823 

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

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

826 assert isinstance(first, str) 

827 assert isinstance(second, int) 

828 assert isinstance(third_arg, str) 

829 new_attrs = { 

830 "id": third_arg, 

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

832 } 

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

834 

835 

836def test_interpolated_template_component_ignore_children(): 

837 node = html( 

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

839 ) 

840 assert node == Element( 

841 "div", 

842 attrs={ 

843 "id": "comp1", 

844 "data-first": "1", 

845 "data-second": "99", 

846 }, 

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

848 ) 

849 assert ( 

850 str(node) 

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

852 ) 

853 

854 

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

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

857 assert isinstance(first, str) 

858 assert "children" in attrs 

859 _ = attrs.pop("children") 

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

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

862 

863 

864def test_children_always_passed_via_kwargs(): 

865 node = html( 

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

867 ) 

868 assert node == Element( 

869 "div", 

870 attrs={ 

871 "data-first": "value", 

872 "extra": "info", 

873 }, 

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

875 ) 

876 assert ( 

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

878 ) 

879 

880 

881def test_children_always_passed_via_kwargs_even_when_empty(): 

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

883 assert node == Element( 

884 "div", 

885 attrs={ 

886 "data-first": "value", 

887 "extra": "info", 

888 }, 

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

890 ) 

891 assert ( 

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

893 ) 

894 

895 

896def ColumnsComponent() -> Template: 

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

898 

899 

900def test_fragment_from_component(): 

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

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

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

904 assert node == Element( 

905 "table", 

906 children=[ 

907 Element( 

908 "tr", 

909 children=[ 

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

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

912 ], 

913 ), 

914 ], 

915 ) 

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

917 

918 

919def test_component_passed_as_attr_value(): 

920 def Wrapper( 

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

922 ) -> Template: 

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

924 

925 node = html( 

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

927 ) 

928 assert node == Element( 

929 "div", 

930 attrs={ 

931 "id": "comp1", 

932 "data-first": "1", 

933 "data-second": "99", 

934 "class": "wrapped", 

935 }, 

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

937 ) 

938 assert ( 

939 str(node) 

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

941 ) 

942 

943 

944def test_nested_component_gh23(): 

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

946 def Header(): 

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

948 

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

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

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

952 

953 

954def test_component_returning_iterable(): 

955 def Items() -> t.Iterable: 

956 for i in range(2): 

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

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

959 

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

961 assert node == Element( 

962 "ul", 

963 children=[ 

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

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

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

967 ], 

968 ) 

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

970 

971 

972def test_component_returning_fragment(): 

973 def Items() -> Node: 

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

975 

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

977 assert node == Element( 

978 "ul", 

979 children=[ 

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

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

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

983 ], 

984 ) 

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

986 

987 

988@dataclass 

989class ClassComponent: 

990 """Example class-based component.""" 

991 

992 user_name: str 

993 image_url: str 

994 homepage: str = "#" 

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

996 

997 def __call__(self) -> Node: 

998 return html( 

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

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

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

1002 t"</a>" 

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

1004 t"{self.children}" 

1005 t"</div>", 

1006 ) 

1007 

1008 

1009def test_class_component_implicit_invocation_with_children(): 

1010 node = html( 

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

1012 ) 

1013 assert node == Element( 

1014 "div", 

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

1016 children=[ 

1017 Element( 

1018 "a", 

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

1020 children=[ 

1021 Element( 

1022 "img", 

1023 attrs={ 

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

1025 "alt": "Avatar of Alice", 

1026 }, 

1027 ) 

1028 ], 

1029 ), 

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

1031 Text("Fun times!"), 

1032 ], 

1033 ) 

1034 assert ( 

1035 str(node) 

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

1037 ) 

1038 

1039 

1040def test_class_component_direct_invocation(): 

1041 avatar = ClassComponent( 

1042 user_name="Alice", 

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

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

1045 ) 

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

1047 assert node == Element( 

1048 "div", 

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

1050 children=[ 

1051 Element( 

1052 "a", 

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

1054 children=[ 

1055 Element( 

1056 "img", 

1057 attrs={ 

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

1059 "alt": "Avatar of Alice", 

1060 }, 

1061 ) 

1062 ], 

1063 ), 

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

1065 ], 

1066 ) 

1067 assert ( 

1068 str(node) 

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

1070 ) 

1071 

1072 

1073@dataclass 

1074class ClassComponentNoChildren: 

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

1076 

1077 user_name: str 

1078 image_url: str 

1079 homepage: str = "#" 

1080 

1081 def __call__(self) -> Node: 

1082 return html( 

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

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

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

1086 t"</a>" 

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

1088 t"ignore children" 

1089 t"</div>", 

1090 ) 

1091 

1092 

1093def test_class_component_implicit_invocation_ignore_children(): 

1094 node = html( 

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

1096 ) 

1097 assert node == Element( 

1098 "div", 

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

1100 children=[ 

1101 Element( 

1102 "a", 

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

1104 children=[ 

1105 Element( 

1106 "img", 

1107 attrs={ 

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

1109 "alt": "Avatar of Alice", 

1110 }, 

1111 ) 

1112 ], 

1113 ), 

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

1115 Text("ignore children"), 

1116 ], 

1117 ) 

1118 assert ( 

1119 str(node) 

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

1121 ) 

1122 

1123 

1124def AttributeTypeComponent( 

1125 data_int: int, 

1126 data_true: bool, 

1127 data_false: bool, 

1128 data_none: None, 

1129 data_float: float, 

1130 data_dt: datetime.datetime, 

1131 **kws: dict[str, object | None], 

1132) -> Template: 

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

1134 assert isinstance(data_int, int) 

1135 assert data_true is True 

1136 assert data_false is False 

1137 assert data_none is None 

1138 assert isinstance(data_float, float) 

1139 assert isinstance(data_dt, datetime.datetime) 

1140 for kw, v_type in [ 

1141 ("spread_true", True), 

1142 ("spread_false", False), 

1143 ("spread_int", int), 

1144 ("spread_none", None), 

1145 ("spread_float", float), 

1146 ("spread_dt", datetime.datetime), 

1147 ("spread_dict", dict), 

1148 ("spread_list", list), 

1149 ]: 

1150 if v_type in (True, False, None): 

1151 assert kw in kws and kws[kw] is v_type, ( 

1152 f"{kw} should be {v_type} but got {kws=}" 

1153 ) 

1154 else: 

1155 assert kw in kws and isinstance(kws[kw], v_type), ( 

1156 f"{kw} should instance of {v_type} but got {kws=}" 

1157 ) 

1158 return t"Looks good!" 

1159 

1160 

1161def test_attribute_type_component(): 

1162 an_int: int = 42 

1163 a_true: bool = True 

1164 a_false: bool = False 

1165 a_none: None = None 

1166 a_float: float = 3.14 

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

1168 spread_attrs: dict[str, object | None] = { 

1169 "spread_true": True, 

1170 "spread_false": False, 

1171 "spread_none": None, 

1172 "spread_int": 0, 

1173 "spread_float": 0.0, 

1174 "spread_dt": datetime.datetime(2024, 1, 1, 12, 0, 1), 

1175 "spread_dict": dict(), 

1176 "spread_list": ["eggs", "milk"], 

1177 } 

1178 node = html( 

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

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

1181 t"data-dt={a_dt} {spread_attrs}/>" 

1182 ) 

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

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

1185 

1186 

1187def test_component_non_callable_fails(): 

1188 with pytest.raises(TypeError): 

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

1190 

1191 

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

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

1194 

1195 

1196def test_component_requiring_positional_arg_fails(): 

1197 with pytest.raises(TypeError): 

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

1199 

1200 

1201def test_mismatched_component_closing_tag_fails(): 

1202 with pytest.raises(TypeError): 

1203 _ = html( 

1204 t"<{FunctionComponent} first=1 second={99} third-arg='comp1'>Hello</{ClassComponent}>" 

1205 ) 

1206 

1207 

1208def test_replace_static_attr_str_str(): 

1209 node = html(t'<div title="default" {dict(title="fresh")}></div>') 

1210 assert node == Element("div", {"title": "fresh"}) 

1211 assert str(node) == '<div title="fresh"></div>' 

1212 

1213 

1214def test_replace_static_attr_str_true(): 

1215 node = html(t'<div title="default" {dict(title=True)}></div>') 

1216 assert node == Element("div", {"title": None}) 

1217 assert str(node) == "<div title></div>" 

1218 

1219 

1220def test_replace_static_attr_true_str(): 

1221 node = html(t"<div title {dict(title='fresh')}></div>") 

1222 assert node == Element("div", {"title": "fresh"}) 

1223 assert str(node) == '<div title="fresh"></div>' 

1224 

1225 

1226def test_remove_static_attr_str_none(): 

1227 node = html(t'<div title="default" {dict(title=None)}></div>') 

1228 assert node == Element("div") 

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

1230 

1231 

1232def test_remove_static_attr_true_none(): 

1233 node = html(t"<div title {dict(title=None)}></div>") 

1234 assert node == Element("div") 

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

1236 

1237 

1238def test_other_static_attr_intact(): 

1239 node = html(t'<img title="default" {dict(alt="fresh")}>') 

1240 assert node == Element("img", {"title": "default", "alt": "fresh"}) 

1241 assert str(node) == '<img title="default" alt="fresh" />'