1use crate::core::Element;
13
14macro_rules! define_elements {
16 ($($(#[$meta:meta])* $name:ident => $tag:literal),* $(,)?) => {
17 $(
18 $(#[$meta])*
19 #[doc = concat!("let el = ", stringify!($name), "();")]
24 #[doc = concat!("assert_eq!(el.tag(), ", stringify!($tag), ");")]
25 #[must_use]
27 pub fn $name() -> Element {
28 Element::new($tag)
29 }
30 )*
31
32 pub const ALL_TAGS: &[(&str, &str)] = &[$((stringify!($name), $tag)),*];
36 };
37}
38
39define_elements! {
40 abbr => "abbr",
43 cite => "cite",
45 del => "del",
47 em => "em",
49 i => "i",
51 ins => "ins",
53 kbd => "kbd",
55 mark => "mark",
57 q => "q",
59 samp => "samp",
61 small => "small",
63 span => "span",
65 strong => "strong",
67 sub => "sub",
69 sup => "sup",
71 time => "time",
73 var => "var",
75 wbr => "wbr",
77
78 br => "br",
81 hr => "hr",
83 img => "img",
85 link => "link",
87 base => "base",
89 meta => "meta",
91 title => "title",
93 script => "script",
96 style => "style",
98 embed => "embed",
100
101 h1 => "h1",
104 h2 => "h2",
106 h3 => "h3",
108 h4 => "h4",
110 h5 => "h5",
112 h6 => "h6",
114
115 address => "address",
118 article => "article",
120 aside => "aside",
122 blockquote => "blockquote",
124 body => "body",
126 canvas => "canvas",
128 dialog => "dialog",
130 div => "div",
132 figcaption => "figcaption",
134 figure => "figure",
136 footer => "footer",
138 head => "head",
140 header => "header",
142 main_tag => "main",
149 nav => "nav",
151 noscript => "noscript",
153 p => "p",
155 picture => "picture",
157 section => "section",
159 html_tag => "html",
161
162 ul => "ul",
165 ol => "ol",
167 li => "li",
169 dl => "dl",
171 dt => "dt",
173 dd => "dd",
175
176 a => "a",
179 button => "button",
181 details => "details",
183 summary => "summary",
185
186 table => "table",
189 tr => "tr",
191 td => "td",
193 th => "th",
195 caption => "caption",
197 colgroup => "colgroup",
199 col => "col",
201 tbody => "tbody",
203 tfoot => "tfoot",
205 thead => "thead",
207
208 form => "form",
211 input => "input",
213 textarea => "textarea",
215 label => "label",
217 fieldset => "fieldset",
219 legend => "legend",
221 select => "select",
223 option => "option",
225 optgroup => "optgroup",
227 datalist => "datalist",
229 output => "output",
231 meter => "meter",
233 progress => "progress",
235
236 audio => "audio",
239 video => "video",
241 source => "source",
243 track => "track",
245 iframe => "iframe",
248
249 code => "code",
252 pre => "pre",
254}
255
256#[must_use]
270pub fn link_to(href: impl AsRef<str>) -> Element {
271 a().attr("href", href)
272}
273
274#[must_use]
281pub fn image(src: impl AsRef<str>, alt: impl AsRef<str>) -> Element {
282 img().attr("src", src).attr("alt", alt)
283}
284
285#[must_use]
287pub fn script_src(src: impl AsRef<str>) -> Element {
288 script().attr("src", src)
289}
290
291#[must_use]
293pub fn stylesheet(href: impl AsRef<str>) -> Element {
294 link().attr("href", href).attr("rel", "stylesheet")
295}
296
297#[must_use]
299pub fn button_typed(button_type: impl AsRef<str>) -> Element {
300 button().attr("type", button_type)
301}
302
303#[must_use]
305pub fn input_named(input_type: impl AsRef<str>, name: impl AsRef<str>) -> Element {
306 input().attr("type", input_type).attr("name", name)
307}
308
309#[must_use]
313pub fn label_for(for_id: impl AsRef<str>) -> Element {
314 label().attr("for", for_id)
315}
316
317#[must_use]
323pub fn iframe_titled(src: impl AsRef<str>, title_text: impl AsRef<str>) -> Element {
324 iframe()
325 .attr("src", src)
326 .attr("title", title_text)
327 .attr("loading", "lazy")
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::core::{Render, RenderOptions};
334
335 #[test]
336 fn the_catalog_covers_every_generated_tag() {
337 assert!(
338 ALL_TAGS.len() >= 90,
339 "expected ~93 tags, found {}",
340 ALL_TAGS.len()
341 );
342 }
343
344 #[test]
348 fn the_catalog_includes_the_headings() {
349 for heading in ["h1", "h2", "h3", "h4", "h5", "h6"] {
350 assert!(
351 ALL_TAGS.iter().any(|(_, tag)| *tag == heading),
352 "{heading} missing from ALL_TAGS"
353 );
354 }
355 }
356
357 #[test]
358 fn no_tag_is_listed_twice() {
359 let mut tags: Vec<&str> = ALL_TAGS.iter().map(|(_, tag)| *tag).collect();
360 tags.sort_unstable();
361 let before = tags.len();
362 tags.dedup();
363 assert_eq!(before, tags.len(), "duplicate tag in ALL_TAGS");
364 }
365
366 #[test]
368 fn the_renamed_tags_still_emit_their_html_names() {
369 assert_eq!(main_tag().render(), "<main></main>");
370 assert_eq!(var().render(), "<var></var>");
371 }
372
373 #[test]
374 fn typed_constructors_set_their_attributes() {
375 assert_eq!(link_to("/x").render(), r#"<a href="/x"></a>"#);
376 assert_eq!(
377 image("/a.png", "A cat").render(),
378 r#"<img src="/a.png" alt="A cat">"#
379 );
380 assert_eq!(
381 label_for("email").text("E-mail").render(),
382 r#"<label for="email">E-mail</label>"#
383 );
384 assert_eq!(
385 stylesheet("/s.css").render(),
386 r#"<link href="/s.css" rel="stylesheet">"#
387 );
388 }
389
390 #[test]
391 fn an_iframe_built_through_the_typed_constructor_always_has_a_title() {
392 let rendered = iframe_titled("/embed", "A map").render();
393 assert!(rendered.contains(r#"title="A map""#));
394 assert!(rendered.contains(r#"loading="lazy""#));
395 }
396
397 #[test]
404 fn a_table_renders_every_section_in_order() {
405 let table = table()
406 .child(caption().text("Sales"))
407 .child(colgroup().child(col().attr("span", "2")))
408 .child(thead().child(tr().child(th().text("Month")).child(th().text("Total"))))
409 .child(tbody().child(tr().child(td().text("Jan")).child(td().text("10"))))
410 .child(tfoot().child(tr().child(td().text("Sum")).child(td().text("10"))));
411
412 assert_eq!(
413 table.render(),
414 concat!(
415 "<table><caption>Sales</caption>",
416 r#"<colgroup><col span="2"></colgroup>"#,
417 "<thead><tr><th>Month</th><th>Total</th></tr></thead>",
418 "<tbody><tr><td>Jan</td><td>10</td></tr></tbody>",
419 "<tfoot><tr><td>Sum</td><td>10</td></tr></tfoot></table>",
420 )
421 );
422 }
423
424 #[test]
426 fn void_elements_render_without_a_closing_tag() {
427 assert_eq!(col().render(), "<col>");
428 assert_eq!(wbr().render(), "<wbr>");
429 assert_eq!(
430 base().attr("href", "https://example.com/").render(),
431 r#"<base href="https://example.com/">"#
432 );
433 assert_eq!(
434 source()
435 .attr("srcset", "a.webp")
436 .attr("type", "image/webp")
437 .render(),
438 r#"<source srcset="a.webp" type="image/webp">"#
439 );
440 assert_eq!(
441 track()
442 .attr("src", "cc.vtt")
443 .attr("kind", "subtitles")
444 .attr("srclang", "en")
445 .bool_attr("default")
446 .render(),
447 r#"<track src="cc.vtt" kind="subtitles" srclang="en" default>"#
448 );
449 }
450
451 #[test]
453 fn void_elements_honour_xhtml_mode() {
454 let options = RenderOptions::compact().with_xhtml_self_closing(true);
455
456 assert_eq!(col().render_with(&options), "<col />");
457 assert_eq!(wbr().render_with(&options), "<wbr />");
458 }
459
460 #[test]
462 fn a_picture_wraps_its_sources_and_fallback_image() {
463 let picture = picture()
464 .child(
465 source()
466 .attr("srcset", "/img/hero.avif")
467 .attr("type", "image/avif"),
468 )
469 .child(image("/img/hero.jpg", "Hero"));
470
471 assert_eq!(
472 picture.render(),
473 concat!(
474 r#"<picture><source srcset="/img/hero.avif" type="image/avif">"#,
475 r#"<img src="/img/hero.jpg" alt="Hero"></picture>"#,
476 )
477 );
478 }
479
480 #[test]
482 fn video_boolean_attributes_render_bare() {
483 let rendered = video()
484 .attr("src", "/demo.mp4")
485 .attr("poster", "/poster.jpg")
486 .bool_attr("controls")
487 .bool_attr("muted")
488 .render();
489
490 assert_eq!(
491 rendered,
492 r#"<video src="/demo.mp4" poster="/poster.jpg" controls muted></video>"#
493 );
494 assert!(!rendered.contains("autoplay"));
495 }
496
497 #[test]
499 fn audio_without_controls_has_no_boolean_attribute() {
500 assert_eq!(
501 audio().attr("src", "/song.mp3").render(),
502 r#"<audio src="/song.mp3"></audio>"#
503 );
504 }
505
506 #[test]
508 fn an_iframe_carries_its_title_and_lazy_loading() {
509 assert_eq!(
510 iframe_titled("https://example.com", "Example").render(),
511 r#"<iframe src="https://example.com" title="Example" loading="lazy"></iframe>"#
512 );
513 }
514
515 #[test]
517 fn details_renders_open_with_a_summary() {
518 let details = details()
519 .bool_attr("open")
520 .child(summary().text("More"))
521 .child(p().text("Hidden text"));
522
523 assert_eq!(
524 details.render(),
525 "<details open><summary>More</summary><p>Hidden text</p></details>"
526 );
527 }
528
529 #[test]
531 fn details_is_closed_unless_open_is_set() {
532 assert_eq!(
533 details().child(summary().text("More")).render(),
534 "<details><summary>More</summary></details>"
535 );
536 }
537
538 #[test]
540 fn a_definition_list_pairs_terms_with_descriptions() {
541 let list = dl()
542 .child(dt().text("WingedSwift"))
543 .child(dd().text("An HTML DSL for Swift"));
544
545 assert_eq!(
546 list.render(),
547 "<dl><dt>WingedSwift</dt><dd>An HTML DSL for Swift</dd></dl>"
548 );
549 }
550
551 #[test]
553 fn the_text_semantic_tags_render_their_own_names() {
554 assert_eq!(
555 blockquote().text("Quoted").render(),
556 "<blockquote>Quoted</blockquote>"
557 );
558 assert_eq!(
559 q().attr("cite", "https://example.com")
560 .text("Short")
561 .render(),
562 r#"<q cite="https://example.com">Short</q>"#
563 );
564 assert_eq!(cite().text("Moby Dick").render(), "<cite>Moby Dick</cite>");
565 assert_eq!(
566 abbr()
567 .attr("title", "HyperText Markup Language")
568 .text("HTML")
569 .render(),
570 r#"<abbr title="HyperText Markup Language">HTML</abbr>"#
571 );
572 assert_eq!(address().text("Rua 1").render(), "<address>Rua 1</address>");
573 assert_eq!(sub().text("2").render(), "<sub>2</sub>");
574 assert_eq!(sup().text("2").render(), "<sup>2</sup>");
575 assert_eq!(del().text("old").render(), "<del>old</del>");
576 assert_eq!(ins().text("new").render(), "<ins>new</ins>");
577 assert_eq!(kbd().text("\u{2318}S").render(), "<kbd>\u{2318}S</kbd>");
578 assert_eq!(samp().text("ok").render(), "<samp>ok</samp>");
579 assert_eq!(var().text("x").render(), "<var>x</var>");
580 assert_eq!(dialog().text("Hi").render(), "<dialog>Hi</dialog>");
581 assert_eq!(
582 noscript().text("Enable JS").render(),
583 "<noscript>Enable JS</noscript>"
584 );
585 assert_eq!(
586 canvas().attr("width", "300").render(),
587 r#"<canvas width="300"></canvas>"#
588 );
589 }
590
591 #[test]
598 fn a_stylesheet_is_not_escaped() {
599 assert_eq!(
600 style()
601 .attr("media", "screen")
602 .raw_text("a > b { color: red; }")
603 .render(),
604 r#"<style media="screen">a > b { color: red; }</style>"#
605 );
606 }
607
608 #[test]
613 fn media_elements_render_only_the_attributes_they_are_given() {
614 assert_eq!(
615 source()
616 .attr("src", "a.mp4")
617 .attr("media", "(min-width: 40em)")
618 .render(),
619 r#"<source src="a.mp4" media="(min-width: 40em)">"#
620 );
621 assert_eq!(
622 track()
623 .attr("src", "t.vtt")
624 .attr("kind", "captions")
625 .attr("label", "PT")
626 .render(),
627 r#"<track src="t.vtt" kind="captions" label="PT">"#
628 );
629 assert_eq!(
630 video().bool_attr("controls").render(),
631 "<video controls></video>"
632 );
633 assert_eq!(
634 audio().bool_attr("autoplay").bool_attr("loop").render(),
635 "<audio autoplay loop></audio>"
636 );
637 assert_eq!(
638 iframe()
639 .attr("src", "/e")
640 .attr("title", "E")
641 .bool_attr("allowfullscreen")
642 .render(),
643 r#"<iframe src="/e" title="E" allowfullscreen></iframe>"#
644 );
645 assert_eq!(
646 base().attr("href", "/").attr("target", "_blank").render(),
647 r#"<base href="/" target="_blank">"#
648 );
649 assert_eq!(col().render(), "<col>");
650 assert_eq!(q().text("quoted").render(), "<q>quoted</q>");
651 }
652
653 #[test]
655 fn meta_renders_whichever_attributes_it_is_given() {
656 assert_eq!(
657 meta()
658 .attr("http-equiv", "refresh")
659 .attr("content", "5")
660 .render(),
661 r#"<meta http-equiv="refresh" content="5">"#
662 );
663 assert_eq!(
664 meta().attr("itemprop", "name").render(),
665 r#"<meta itemprop="name">"#
666 );
667 }
668
669 #[test]
671 fn flow_containers_accept_text_as_well_as_children() {
672 assert_eq!(aside().text("Note").render(), "<aside>Note</aside>");
673 assert_eq!(nav().text("Menu").render(), "<nav>Menu</nav>");
674 assert_eq!(header().text("Top").render(), "<header>Top</header>");
675 assert_eq!(footer().text("Bottom").render(), "<footer>Bottom</footer>");
676 assert_eq!(main_tag().text("Body").render(), "<main>Body</main>");
677 assert_eq!(article().text("Post").render(), "<article>Post</article>");
678 assert_eq!(figure().text("Fig").render(), "<figure>Fig</figure>");
679 assert_eq!(form().text("F").render(), "<form>F</form>");
680 assert_eq!(fieldset().text("Set").render(), "<fieldset>Set</fieldset>");
681 }
682
683 #[test]
685 fn flow_container_content_is_escaped() {
686 assert_eq!(
687 aside().text("<b>x</b>").render(),
688 "<aside><b>x</b></aside>"
689 );
690 }
691
692 #[test]
694 fn a_fieldset_carries_a_legend_and_its_fields() {
695 let fieldset = fieldset()
696 .child(legend().text("Account"))
697 .child(input_named("text", "email"));
698
699 assert_eq!(
700 fieldset.render(),
701 r#"<fieldset><legend>Account</legend><input type="text" name="email"></fieldset>"#
702 );
703 }
704
705 #[test]
707 fn a_select_groups_its_options() {
708 let select = select().attr("name", "city").child(
709 optgroup()
710 .attr("label", "Brazil")
711 .child(option().attr("value", "sp").text("S\u{e3}o Paulo")),
712 );
713
714 assert_eq!(
715 select.render(),
716 concat!(
717 r#"<select name="city"><optgroup label="Brazil">"#,
718 "<option value=\"sp\">S\u{e3}o Paulo</option></optgroup></select>",
719 )
720 );
721 }
722
723 #[test]
725 fn the_form_display_elements_render() {
726 assert_eq!(
727 datalist().child(option().attr("value", "swift")).render(),
728 r#"<datalist><option value="swift"></option></datalist>"#
729 );
730 assert_eq!(
731 progress().attr("value", "0.7").attr("max", "1.0").render(),
732 r#"<progress value="0.7" max="1.0"></progress>"#
733 );
734 assert_eq!(
735 progress().attr("max", "1.0").render(),
736 r#"<progress max="1.0"></progress>"#
737 );
738 assert_eq!(
739 meter()
740 .attr("value", "6.0")
741 .attr("min", "0.0")
742 .attr("max", "10.0")
743 .render(),
744 r#"<meter value="6.0" min="0.0" max="10.0"></meter>"#
745 );
746 assert_eq!(output().text("42").render(), "<output>42</output>");
747 }
748
749 #[test]
754 fn boolean_attributes_carry_no_value() {
755 let rendered = input_named("checkbox", "agree")
756 .bool_attr("checked")
757 .bool_attr("required")
758 .render();
759
760 assert_eq!(
761 rendered,
762 r#"<input type="checkbox" name="agree" checked required>"#
763 );
764 assert!(!rendered.contains("checked="));
765 assert!(!rendered.contains("required="));
766 }
767
768 #[test]
770 fn void_elements_do_not_self_close_by_default() {
771 let rendered = image("a.png", "A").render();
772
773 assert_eq!(rendered, r#"<img src="a.png" alt="A">"#);
774 assert!(!rendered.contains("/>"));
775 }
776
777 #[test]
779 fn xhtml_mode_self_closes_void_elements() {
780 let options = RenderOptions::compact().with_xhtml_self_closing(true);
781
782 assert_eq!(
783 image("a.png", "A").render_with(&options),
784 r#"<img src="a.png" alt="A" />"#
785 );
786 }
787
788 #[test]
790 fn anchors_and_headings_take_element_children() {
791 assert_eq!(
792 link_to("/news").child(image("thumb.jpg", "Thumb")).render(),
793 r#"<a href="/news"><img src="thumb.jpg" alt="Thumb"></a>"#
794 );
795 assert_eq!(
796 h3().child(link_to("/news").text("Headline")).render(),
797 r#"<h3><a href="/news">Headline</a></h3>"#
798 );
799 assert_eq!(
800 i().add_class("fas fa-search").render(),
801 r#"<i class="fas fa-search"></i>"#
802 );
803 }
804
805 #[test]
807 fn a_submit_button_carries_its_type() {
808 assert_eq!(
809 button_typed("submit").text("Send").render(),
810 r#"<button type="submit">Send</button>"#
811 );
812 }
813
814 #[test]
816 fn a_label_without_a_target_has_no_for_attribute() {
817 let rendered = label().text("Accept cookies").render();
818
819 assert_eq!(rendered, "<label>Accept cookies</label>");
820 assert!(!rendered.contains("for="));
821 }
822
823 #[test]
825 fn an_input_without_a_name_has_no_name_attribute() {
826 let rendered = input()
827 .attr("type", "search")
828 .attr("placeholder", "Search")
829 .render();
830
831 assert_eq!(rendered, r#"<input type="search" placeholder="Search">"#);
832 assert!(!rendered.contains("name="));
833 }
834
835 #[test]
837 fn a_section_renders_its_class_and_text() {
838 assert_eq!(
839 section().add_class("hero").text("Hello").render(),
840 r#"<section class="hero">Hello</section>"#
841 );
842 }
843
844 #[test]
846 fn the_inline_semantic_tags_render() {
847 assert_eq!(strong().text("bold").render(), "<strong>bold</strong>");
848 assert_eq!(em().text("emph").render(), "<em>emph</em>");
849 assert_eq!(small().text("fine").render(), "<small>fine</small>");
850 assert_eq!(br().render(), "<br>");
851 assert_eq!(hr().render(), "<hr>");
852 }
853
854 #[test]
858 fn a_pre_block_keeps_its_line_breaks() {
859 let block =
860 pre().text("This is preformatted text.\nIt preserves whitespace and line breaks.");
861
862 assert_eq!(
863 block.render(),
864 "<pre>This is preformatted text.\nIt preserves whitespace and line breaks.</pre>"
865 );
866 }
867
868 #[test]
870 fn a_code_block_keeps_its_line_breaks() {
871 let block = code().text("let x = 10\nprint(x)");
872
873 assert_eq!(block.render(), "<code>let x = 10\nprint(x)</code>");
874 }
875
876 #[test]
878 fn an_embed_is_a_void_element() {
879 assert_eq!(
880 embed()
881 .attr("src", "video.mp4")
882 .attr("type", "video/mp4")
883 .render(),
884 r#"<embed src="video.mp4" type="video/mp4">"#
885 );
886 }
887
888 #[test]
890 fn an_article_wraps_its_children() {
891 assert_eq!(
892 article().child(h1().text("Title")).render(),
893 "<article><h1>Title</h1></article>"
894 );
895 }
896
897 #[test]
899 fn an_aside_wraps_its_children() {
900 assert_eq!(
901 aside().child(p().text("Sidebar")).render(),
902 "<aside><p>Sidebar</p></aside>"
903 );
904 }
905
906 #[test]
908 fn a_figure_pairs_an_image_with_its_caption() {
909 let block = figure()
910 .child(image("image.jpg", "Test"))
911 .child(figcaption().text("Image caption"));
912
913 assert_eq!(
914 block.render(),
915 concat!(
916 r#"<figure><img src="image.jpg" alt="Test">"#,
917 "<figcaption>Image caption</figcaption></figure>",
918 )
919 );
920 }
921
922 #[test]
924 fn a_time_element_carries_its_datetime() {
925 assert_eq!(
926 time()
927 .attr("datetime", "2024-01-15")
928 .text("January 15, 2024")
929 .render(),
930 r#"<time datetime="2024-01-15">January 15, 2024</time>"#
931 );
932 }
933
934 #[test]
936 fn a_mark_element_renders_its_text() {
937 assert_eq!(
938 mark().text("highlighted").render(),
939 "<mark>highlighted</mark>"
940 );
941 }
942
943 #[test]
945 fn every_heading_level_renders_its_own_tag() {
946 let headings = [
947 h1().text("H1"),
948 h2().text("H2"),
949 h3().text("H3"),
950 h4().text("H4"),
951 h5().text("H5"),
952 h6().text("H6"),
953 ];
954
955 for (index, heading) in headings.into_iter().enumerate() {
956 let level = index + 1;
957 assert_eq!(heading.render(), format!("<h{level}>H{level}</h{level}>"));
958 }
959 }
960
961 #[test]
966 fn a_form_renders_its_fieldsets_and_submit() {
967 let form = form()
968 .attr("action", "/submit")
969 .child(
970 fieldset()
971 .child(label_for("name").text("Name"))
972 .child(input_named("text", "name")),
973 )
974 .child(
975 fieldset()
976 .child(label_for("message").text("Message"))
977 .child(textarea().attr("name", "message")),
978 )
979 .child(input_named("submit", "submit").attr("value", "Send"));
980
981 assert_eq!(
982 form.render(),
983 concat!(
984 r#"<form action="/submit"><fieldset><label for="name">Name</label>"#,
985 r#"<input type="text" name="name"></fieldset><fieldset>"#,
986 r#"<label for="message">Message</label>"#,
987 r#"<textarea name="message"></textarea></fieldset>"#,
988 r#"<input type="submit" name="submit" value="Send"></form>"#,
989 )
990 );
991 }
992
993 #[test]
995 fn a_select_renders_each_option() {
996 let select = select()
997 .attr("name", "options")
998 .children_from((1..=3).map(|i| {
999 option()
1000 .attr("value", i.to_string())
1001 .text(format!("Option {i}"))
1002 }));
1003
1004 assert_eq!(
1005 select.render(),
1006 concat!(
1007 r#"<select name="options"><option value="1">Option 1</option>"#,
1008 r#"<option value="2">Option 2</option>"#,
1009 r#"<option value="3">Option 3</option></select>"#,
1010 )
1011 );
1012 }
1013
1014 #[test]
1016 fn a_label_points_at_its_field() {
1017 assert_eq!(
1018 label_for("username").text("Username").render(),
1019 r#"<label for="username">Username</label>"#
1020 );
1021 }
1022
1023 #[test]
1025 fn an_input_renders_its_type_name_and_value() {
1026 assert_eq!(
1027 input_named("text", "username")
1028 .attr("value", "JohnDoe")
1029 .render(),
1030 r#"<input type="text" name="username" value="JohnDoe">"#
1031 );
1032 }
1033
1034 #[test]
1036 fn a_textarea_renders_its_content_between_the_tags() {
1037 assert_eq!(
1038 textarea()
1039 .attr("name", "message")
1040 .text("Hello, World!")
1041 .render(),
1042 r#"<textarea name="message">Hello, World!</textarea>"#
1043 );
1044 }
1045
1046 #[test]
1048 fn a_fieldset_groups_a_label_and_its_field() {
1049 let block = fieldset()
1050 .child(label_for("name").text("Name"))
1051 .child(input_named("text", "name"));
1052
1053 assert_eq!(
1054 block.render(),
1055 concat!(
1056 r#"<fieldset><label for="name">Name</label>"#,
1057 r#"<input type="text" name="name"></fieldset>"#,
1058 )
1059 );
1060 }
1061
1062 #[test]
1064 fn a_section_wraps_its_children() {
1065 assert_eq!(
1066 section().child(p().text("This is a section.")).render(),
1067 "<section><p>This is a section.</p></section>"
1068 );
1069 }
1070}