Coverage for tdom / parser_test.py: 95%

195 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-01-12 16:43 +0000

1import pytest 

2 

3from .parser import TemplateParser 

4from .placeholders import TemplateRef 

5from .tnodes import ( 

6 TComment, 

7 TComponent, 

8 TDocumentType, 

9 TElement, 

10 TFragment, 

11 TInterpolatedAttribute, 

12 TLiteralAttribute, 

13 TSpreadAttribute, 

14 TTemplatedAttribute, 

15 TText, 

16) 

17 

18 

19def test_parse_mixed_literal_content(): 

20 node = TemplateParser.parse( 

21 t"<!DOCTYPE html>" 

22 t"<!-- Comment -->" 

23 t'<div class="container">' 

24 t"Hello, <br class='funky' />world <!-- neato -->!" 

25 t"</div>" 

26 ) 

27 assert node == TFragment( 

28 children=( 

29 TDocumentType("html"), 

30 TComment.literal(" Comment "), 

31 TElement( 

32 "div", 

33 attrs=(TLiteralAttribute("class", "container"),), 

34 children=( 

35 TText.literal("Hello, "), 

36 TElement("br", attrs=(TLiteralAttribute("class", "funky"),)), 

37 TText.literal("world "), 

38 TComment.literal(" neato "), 

39 TText.literal("!"), 

40 ), 

41 ), 

42 ) 

43 ) 

44 

45 

46# 

47# Text 

48# 

49def test_parse_empty(): 

50 node = TemplateParser.parse(t"") 

51 assert node == TFragment() 

52 

53 

54def test_parse_text(): 

55 node = TemplateParser.parse(t"Hello, world!") 

56 assert node == TText.literal("Hello, world!") 

57 

58 

59def test_parse_text_multiline(): 

60 node = TemplateParser.parse(t"""Hello, world! 

61 Hello, moon! 

62Hello, sun! 

63""") 

64 assert node == TText.literal("""Hello, world! 

65 Hello, moon! 

66Hello, sun! 

67""") 

68 

69 

70def test_parse_text_with_entities(): 

71 node = TemplateParser.parse(t"a &lt; b") 

72 assert node == TText.literal("a < b") 

73 

74 

75def test_parse_text_with_template_singleton(): 

76 greeting = "Hello, World!" 

77 node = TemplateParser.parse(t"{greeting}") 

78 assert node == TText(ref=TemplateRef(strings=("", ""), i_indexes=(0,))) 

79 

80 

81def test_parse_text_with_template(): 

82 who = "World" 

83 node = TemplateParser.parse(t"Hello, {who}!") 

84 assert node == TText(ref=TemplateRef(strings=("Hello, ", "!"), i_indexes=(0,))) 

85 

86 

87# 

88# Elements 

89# 

90def test_parse_void_element(): 

91 node = TemplateParser.parse(t"<br>") 

92 assert node == TElement("br") 

93 

94 

95def test_parse_void_element_self_closed(): 

96 node = TemplateParser.parse(t"<br />") 

97 assert node == TElement("br") 

98 

99 

100def test_parse_uppercase_void_element(): 

101 node = TemplateParser.parse(t"<BR>") 

102 assert node == TElement("br") 

103 

104 

105def test_parse_standard_element_with_text(): 

106 node = TemplateParser.parse(t"<div>Hello, world!</div>") 

107 assert node == TElement("div", children=(TText.literal("Hello, world!"),)) 

108 

109 

110def test_parse_nested_elements(): 

111 node = TemplateParser.parse(t"<div><span>Nested</span> content</div>") 

112 assert node == TElement( 

113 "div", 

114 children=( 

115 TElement("span", children=(TText.literal("Nested"),)), 

116 TText.literal(" content"), 

117 ), 

118 ) 

119 

120 

121def test_parse_element_with_template(): 

122 who = "World" 

123 node = TemplateParser.parse(t"<div>Hello, {who}!</div>") 

124 assert node == TElement( 

125 "div", 

126 children=(TText(ref=TemplateRef(strings=("Hello, ", "!"), i_indexes=(0,))),), 

127 ) 

128 

129 

130def test_parse_element_with_template_singleton(): 

131 greeting = "Hello, World!" 

132 node = TemplateParser.parse(t"<div>{greeting}</div>") 

133 assert node == TElement( 

134 "div", children=(TText(ref=TemplateRef(strings=("", ""), i_indexes=(0,))),) 

135 ) 

136 

137 

138def test_parse_multiple_voids(): 

139 node = TemplateParser.parse(t"<br><hr><hr /><hr /><br /><br><br>") 

140 assert node == TFragment( 

141 children=( 

142 TElement("br"), 

143 TElement("hr"), 

144 TElement("hr"), 

145 TElement("hr"), 

146 TElement("br"), 

147 TElement("br"), 

148 TElement("br"), 

149 ) 

150 ) 

151 

152 

153def test_parse_text_entities(): 

154 node = TemplateParser.parse(t"<p>&lt;/p&gt;</p>") 

155 assert node == TElement( 

156 "p", 

157 children=(TText.literal("</p>"),), 

158 ) 

159 

160 

161def test_parse_script_tag_content(): 

162 node = TemplateParser.parse( 

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

164 ) 

165 assert node == TElement( 

166 "script", 

167 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),), 

168 ) 

169 

170 

171def test_parse_script_with_entities(): 

172 # The <script> tag (and <style>) tag uses the CDATA content model. 

173 node = TemplateParser.parse(t"<script>var x = 'a &amp; b';</script>") 

174 assert node == TElement( 

175 "script", 

176 children=(TText.literal("var x = 'a &amp; b';"),), 

177 ), "Entities SHOULD NOT be evaluated in scripts." 

178 

179 

180def test_parse_textarea_tag_content(): 

181 node = TemplateParser.parse( 

182 t"<textarea>if (a < b && c > d) { alert('wow'); } </textarea>" 

183 ) 

184 assert node == TElement( 

185 "textarea", 

186 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),), 

187 ) 

188 

189 

190def test_parse_textarea_with_entities(): 

191 # The <textarea> (and <title>) tag uses the RCDATA content model. 

192 node = TemplateParser.parse(t"<textarea>var x = 'a &amp; b';</textarea>") 

193 assert node == TElement( 

194 "textarea", 

195 children=(TText.literal("var x = 'a & b';"),), 

196 ), "Entities SHOULD be evaluated in textarea/title." 

197 

198 

199def test_parse_title_unusual(): 

200 node = TemplateParser.parse(t"<title>My & Awesome <Site></title>") 

201 assert node == TElement( 

202 "title", 

203 children=(TText.literal("My & Awesome <Site>"),), 

204 ) 

205 

206 

207def test_parse_mismatched_tags(): 

208 with pytest.raises(ValueError): 

209 _ = TemplateParser.parse(t"<div><span>Mismatched</div></span>") 

210 

211 

212def test_parse_unclosed_tag(): 

213 with pytest.raises(ValueError): 

214 _ = TemplateParser.parse(t"<div>Unclosed") 

215 

216 

217def test_parse_unexpected_closing_tag(): 

218 with pytest.raises(ValueError): 

219 _ = TemplateParser.parse(t"Unopened</div>") 

220 

221 

222def test_self_closing_tags(): 

223 node = TemplateParser.parse(t"<div/><p></p>") 

224 assert node == TFragment( 

225 children=( 

226 TElement("div"), 

227 TElement("p"), 

228 ) 

229 ) 

230 

231 

232def test_nested_self_closing_tags(): 

233 node = TemplateParser.parse(t"<div><br><div /><br></div>") 

234 assert node == TElement( 

235 "div", children=(TElement("br"), TElement("div"), TElement("br")) 

236 ) 

237 node = TemplateParser.parse(t"<div><div /></div>") 

238 assert node == TElement("div", children=(TElement("div"),)) 

239 

240 

241def test_self_closing_tags_unexpected_closing_tag(): 

242 with pytest.raises(ValueError): 

243 _ = TemplateParser.parse(t"<div /></div>") 

244 

245 

246def test_self_closing_void_tags_unexpected_closing_tag(): 

247 with pytest.raises(ValueError): 

248 _ = TemplateParser.parse(t"<input /></input>") 

249 

250 

251# 

252# Attributes 

253# 

254def test_literal_attrs(): 

255 node = TemplateParser.parse( 

256 ( 

257 t"<a" 

258 t" id=example_link" # no quotes allowed without spaces 

259 t" autofocus" # bare / boolean 

260 t' title=""' # empty attribute 

261 t' href="https://example.com" target="_blank"' 

262 t">Link</a>" 

263 ) 

264 ) 

265 assert node == TElement( 

266 "a", 

267 attrs=( 

268 TLiteralAttribute("id", "example_link"), 

269 TLiteralAttribute("autofocus", None), 

270 TLiteralAttribute("title", ""), 

271 TLiteralAttribute("href", "https://example.com"), 

272 TLiteralAttribute("target", "_blank"), 

273 ), 

274 children=(TText.literal("Link"),), 

275 ) 

276 

277 

278def test_literal_attr_entities(): 

279 node = TemplateParser.parse(t'<a title="&lt;">Link</a>') 

280 assert node == TElement( 

281 "a", 

282 attrs=(TLiteralAttribute("title", "<"),), 

283 children=(TText.literal("Link"),), 

284 ) 

285 

286 

287def test_literal_attr_order(): 

288 node = TemplateParser.parse(t'<a title="a" href="b" title="c"></a>') 

289 assert isinstance(node, TElement) 

290 assert node.attrs == ( 

291 TLiteralAttribute("title", "a"), 

292 TLiteralAttribute("href", "b"), 

293 TLiteralAttribute("title", "c"), # dupe IS allowed 

294 ) 

295 

296 

297def test_interpolated_attr(): 

298 value1 = 42 

299 value2 = 99 

300 node = TemplateParser.parse(t'<div value1="{value1}" value2={value2} />') 

301 assert node == TElement( 

302 "div", 

303 attrs=( 

304 TInterpolatedAttribute("value1", 0), 

305 TInterpolatedAttribute("value2", 1), 

306 ), 

307 children=(), 

308 ) 

309 

310 

311def test_templated_attr(): 

312 value1 = 42 

313 value2 = 99 

314 node = TemplateParser.parse( 

315 t'<div value1="{value1}-burrito" value2="neato-{value2}-wow" />' 

316 ) 

317 value1_ref = TemplateRef(strings=("", "-burrito"), i_indexes=(0,)) 

318 value2_ref = TemplateRef(strings=("neato-", "-wow"), i_indexes=(1,)) 

319 assert node == TElement( 

320 "div", 

321 attrs=( 

322 TTemplatedAttribute("value1", value1_ref), 

323 TTemplatedAttribute("value2", value2_ref), 

324 ), 

325 children=(), 

326 ) 

327 

328 

329def test_spread_attr(): 

330 spread_attrs = {} 

331 node = TemplateParser.parse(t"<div {spread_attrs} />") 

332 assert node == TElement( 

333 "div", 

334 attrs=(TSpreadAttribute(i_index=0),), 

335 children=(), 

336 ) 

337 

338 

339def test_templated_attribute_name_error(): 

340 with pytest.raises(ValueError): 

341 attr_name = "some-attr" 

342 _ = TemplateParser.parse(t'<div {attr_name}="value" />') 

343 

344 

345def test_templated_attribute_name_and_value_error(): 

346 with pytest.raises(ValueError): 

347 attr_name = "some-attr" 

348 value = "value" 

349 _ = TemplateParser.parse(t'<div {attr_name}="{value}" />') 

350 

351 

352def test_adjacent_spread_attrs_error(): 

353 with pytest.raises(ValueError): 

354 attrs1 = {} 

355 attrs2 = {} 

356 _ = TemplateParser.parse(t"<div {attrs1}{attrs2} />") 

357 

358 

359# 

360# Comments 

361# 

362def test_parse_comment(): 

363 node = TemplateParser.parse(t"<!-- This is a comment -->") 

364 assert node == TComment.literal(" This is a comment ") 

365 

366 

367def test_parse_comment_interpolation(): 

368 text = "comment" 

369 node = TemplateParser.parse(t"<!-- This is a {text} -->") 

370 assert node == TComment( 

371 ref=TemplateRef(strings=(" This is a ", " "), i_indexes=(0,)) 

372 ) 

373 

374 

375# 

376# Doctypes 

377# 

378def test_parse_doctype(): 

379 node = TemplateParser.parse(t"<!DOCTYPE html>") 

380 assert node == TDocumentType("html") 

381 

382 

383def test_parse_doctype_interpolation_error(): 

384 extra = "SYSTEM" 

385 with pytest.raises(ValueError): 

386 _ = TemplateParser.parse(t"<!DOCTYPE html {extra}>") 

387 

388 

389def test_unsupported_decl_error(): 

390 with pytest.raises(NotImplementedError): 

391 _ = TemplateParser.parse(t"<!doctype-alt html500>") # Unknown declaration 

392 with pytest.raises(NotImplementedError): 

393 _ = TemplateParser.parse(t"<!doctype>") # missing DTD 

394 

395 

396# 

397# Components. 

398# 

399def test_component_element_with_children(): 

400 def Component(children): 

401 return t"{children}" 

402 

403 node = TemplateParser.parse(t"<{Component}><div>Hello, World!</div></{Component}>") 

404 assert node == TComponent( 

405 start_i_index=0, 

406 end_i_index=1, 

407 children=(TElement("div", children=(TText.literal("Hello, World!"),)),), 

408 ) 

409 

410 

411def test_component_element_self_closing(): 

412 def Component(): 

413 pass 

414 

415 node = TemplateParser.parse(t"<{Component} />") 

416 assert node == TComponent(start_i_index=0) 

417 

418 

419def test_component_element_with_closing_tag(): 

420 def Component(): 

421 pass 

422 

423 node = TemplateParser.parse(t"<{Component}></{Component}>") 

424 assert node == TComponent(start_i_index=0, end_i_index=1) 

425 

426 

427def test_component_element_special_case_mismatched_closing_tag_still_parses(): 

428 def Component1(): 

429 pass 

430 

431 def Component2(): 

432 pass 

433 

434 node = TemplateParser.parse(t"<{Component1}></{Component2}>") 

435 assert node == TComponent(start_i_index=0, end_i_index=1) 

436 

437 

438def test_component_element_invalid_closing_tag(): 

439 def Component(): 

440 pass 

441 

442 with pytest.raises(ValueError): 

443 _ = TemplateParser.parse(t"<{Component}></div>") 

444 

445 

446def test_component_element_invalid_opening_tag(): 

447 def Component(): 

448 pass 

449 

450 with pytest.raises(ValueError): 

451 _ = TemplateParser.parse(t"<div></{Component}>") 

452 

453 

454def test_adjacent_start_component_tag_error(): 

455 def Component(): 

456 pass 

457 

458 with pytest.raises(ValueError): 

459 _ = TemplateParser.parse(t"<{Component}{Component}></{Component}>") 

460 

461 

462def test_adjacent_end_component_tag_error(): 

463 def Component(): 

464 pass 

465 

466 with pytest.raises(ValueError): 

467 _ = TemplateParser.parse(t"<{Component}></{Component}{Component}>")