1use crate::core::Element;
8
9macro_rules! define_elements {
11 ($($(#[$meta:meta])* $name:ident => $tag:literal),* $(,)?) => {
12 $(
13 $(#[$meta])*
14 #[doc = concat!("let el = ", stringify!($name), "();")]
19 #[doc = concat!("assert_eq!(el.tag(), ", stringify!($tag), ");")]
20 #[must_use]
22 pub fn $name() -> Element {
23 Element::from_static($tag)
24 }
25 )*
26
27 pub const ALL_TAGS: &[(&str, &str)] = &[$((stringify!($name), $tag)),*];
31 };
32}
33
34define_elements! {
35 abbr => "abbr",
38 cite => "cite",
40 del => "del",
42 em => "em",
44 i => "i",
46 ins => "ins",
48 kbd => "kbd",
50 mark => "mark",
52 q => "q",
54 samp => "samp",
56 small => "small",
58 span => "span",
60 strong => "strong",
62 sub => "sub",
64 sup => "sup",
66 time => "time",
68 var => "var",
70 wbr => "wbr",
72
73 br => "br",
76 hr => "hr",
78 img => "img",
80 link => "link",
82 base => "base",
84 meta => "meta",
86 title => "title",
88 script => "script",
91 style => "style",
93 embed => "embed",
95
96 h1 => "h1",
99 h2 => "h2",
101 h3 => "h3",
103 h4 => "h4",
105 h5 => "h5",
107 h6 => "h6",
109
110 address => "address",
113 article => "article",
115 aside => "aside",
117 blockquote => "blockquote",
119 body => "body",
121 canvas => "canvas",
123 dialog => "dialog",
125 div => "div",
127 figcaption => "figcaption",
129 figure => "figure",
131 footer => "footer",
133 head => "head",
135 header => "header",
137 main_tag => "main",
143 nav => "nav",
145 noscript => "noscript",
147 p => "p",
149 picture => "picture",
151 section => "section",
153 html_tag => "html",
155
156 ul => "ul",
159 ol => "ol",
161 li => "li",
163 dl => "dl",
165 dt => "dt",
167 dd => "dd",
169
170 a => "a",
173 button => "button",
175 details => "details",
177 summary => "summary",
179
180 table => "table",
183 tr => "tr",
185 td => "td",
187 th => "th",
189 caption => "caption",
191 colgroup => "colgroup",
193 col => "col",
195 tbody => "tbody",
197 tfoot => "tfoot",
199 thead => "thead",
201
202 form => "form",
205 input => "input",
207 textarea => "textarea",
209 label => "label",
211 fieldset => "fieldset",
213 legend => "legend",
215 select => "select",
217 option => "option",
219 optgroup => "optgroup",
221 datalist => "datalist",
223 output => "output",
225 meter => "meter",
227 progress => "progress",
229
230 audio => "audio",
233 video => "video",
235 source => "source",
237 track => "track",
239 iframe => "iframe",
242
243 code => "code",
246 pre => "pre",
248}
249
250#[must_use]
263pub fn link_to(href: impl AsRef<str>) -> Element {
264 a().attr("href", href)
265}
266
267#[must_use]
273pub fn image(src: impl AsRef<str>, alt: impl AsRef<str>) -> Element {
274 img().attr("src", src).attr("alt", alt)
275}
276
277#[must_use]
279pub fn script_src(src: impl AsRef<str>) -> Element {
280 script().attr("src", src)
281}
282
283#[must_use]
285pub fn stylesheet(href: impl AsRef<str>) -> Element {
286 link().attr("href", href).attr("rel", "stylesheet")
287}
288
289#[must_use]
291pub fn button_typed(button_type: impl AsRef<str>) -> Element {
292 button().attr("type", button_type)
293}
294
295#[must_use]
297pub fn input_named(input_type: impl AsRef<str>, name: impl AsRef<str>) -> Element {
298 input().attr("type", input_type).attr("name", name)
299}
300
301#[must_use]
305pub fn label_for(for_id: impl AsRef<str>) -> Element {
306 label().attr("for", for_id)
307}
308
309#[must_use]
314pub fn iframe_titled(src: impl AsRef<str>, title_text: impl AsRef<str>) -> Element {
315 iframe()
316 .attr("src", src)
317 .attr("title", title_text)
318 .attr("loading", "lazy")
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::core::{Render, RenderOptions};
325
326 #[test]
327 fn the_catalog_covers_every_generated_tag() {
328 assert!(
329 ALL_TAGS.len() >= 90,
330 "expected ~93 tags, found {}",
331 ALL_TAGS.len()
332 );
333 }
334
335 #[test]
338 fn the_catalog_includes_the_headings() {
339 for heading in ["h1", "h2", "h3", "h4", "h5", "h6"] {
340 assert!(
341 ALL_TAGS.iter().any(|(_, tag)| *tag == heading),
342 "{heading} missing from ALL_TAGS"
343 );
344 }
345 }
346
347 #[test]
348 fn no_tag_is_listed_twice() {
349 let mut tags: Vec<&str> = ALL_TAGS.iter().map(|(_, tag)| *tag).collect();
350 tags.sort_unstable();
351 let before = tags.len();
352 tags.dedup();
353 assert_eq!(before, tags.len(), "duplicate tag in ALL_TAGS");
354 }
355
356 #[test]
358 fn the_renamed_tags_still_emit_their_html_names() {
359 assert_eq!(main_tag().render(), "<main></main>");
360 assert_eq!(var().render(), "<var></var>");
361 }
362
363 #[test]
364 fn typed_constructors_set_their_attributes() {
365 assert_eq!(link_to("/x").render(), r#"<a href="/x"></a>"#);
366 assert_eq!(
367 image("/a.png", "A cat").render(),
368 r#"<img src="/a.png" alt="A cat">"#
369 );
370 assert_eq!(
371 label_for("email").text("E-mail").render(),
372 r#"<label for="email">E-mail</label>"#
373 );
374 assert_eq!(
375 stylesheet("/s.css").render(),
376 r#"<link href="/s.css" rel="stylesheet">"#
377 );
378 }
379
380 #[test]
381 fn an_iframe_built_through_the_typed_constructor_always_has_a_title() {
382 let rendered = iframe_titled("/embed", "A map").render();
383 assert!(rendered.contains(r#"title="A map""#));
384 assert!(rendered.contains(r#"loading="lazy""#));
385 }
386
387 #[test]
391 fn a_table_renders_every_section_in_order() {
392 let table = table()
393 .child(caption().text("Sales"))
394 .child(colgroup().child(col().attr("span", "2")))
395 .child(thead().child(tr().child(th().text("Month")).child(th().text("Total"))))
396 .child(tbody().child(tr().child(td().text("Jan")).child(td().text("10"))))
397 .child(tfoot().child(tr().child(td().text("Sum")).child(td().text("10"))));
398
399 assert_eq!(
400 table.render(),
401 concat!(
402 "<table><caption>Sales</caption>",
403 r#"<colgroup><col span="2"></colgroup>"#,
404 "<thead><tr><th>Month</th><th>Total</th></tr></thead>",
405 "<tbody><tr><td>Jan</td><td>10</td></tr></tbody>",
406 "<tfoot><tr><td>Sum</td><td>10</td></tr></tfoot></table>",
407 )
408 );
409 }
410
411 #[test]
412 fn void_elements_render_without_a_closing_tag() {
413 assert_eq!(col().render(), "<col>");
414 assert_eq!(wbr().render(), "<wbr>");
415 assert_eq!(
416 base().attr("href", "https://example.com/").render(),
417 r#"<base href="https://example.com/">"#
418 );
419 assert_eq!(
420 source()
421 .attr("srcset", "a.webp")
422 .attr("type", "image/webp")
423 .render(),
424 r#"<source srcset="a.webp" type="image/webp">"#
425 );
426 assert_eq!(
427 track()
428 .attr("src", "cc.vtt")
429 .attr("kind", "subtitles")
430 .attr("srclang", "en")
431 .bool_attr("default")
432 .render(),
433 r#"<track src="cc.vtt" kind="subtitles" srclang="en" default>"#
434 );
435 }
436
437 #[test]
438 fn void_elements_honour_xhtml_mode() {
439 let options = RenderOptions::compact().with_xhtml_self_closing(true);
440
441 assert_eq!(col().render_with(&options), "<col />");
442 assert_eq!(wbr().render_with(&options), "<wbr />");
443 }
444
445 #[test]
446 fn a_picture_wraps_its_sources_and_fallback_image() {
447 let picture = picture()
448 .child(
449 source()
450 .attr("srcset", "/img/hero.avif")
451 .attr("type", "image/avif"),
452 )
453 .child(image("/img/hero.jpg", "Hero"));
454
455 assert_eq!(
456 picture.render(),
457 concat!(
458 r#"<picture><source srcset="/img/hero.avif" type="image/avif">"#,
459 r#"<img src="/img/hero.jpg" alt="Hero"></picture>"#,
460 )
461 );
462 }
463
464 #[test]
465 fn video_boolean_attributes_render_bare() {
466 let rendered = video()
467 .attr("src", "/demo.mp4")
468 .attr("poster", "/poster.jpg")
469 .bool_attr("controls")
470 .bool_attr("muted")
471 .render();
472
473 assert_eq!(
474 rendered,
475 r#"<video src="/demo.mp4" poster="/poster.jpg" controls muted></video>"#
476 );
477 assert!(!rendered.contains("autoplay"));
478 }
479
480 #[test]
481 fn audio_without_controls_has_no_boolean_attribute() {
482 assert_eq!(
483 audio().attr("src", "/song.mp3").render(),
484 r#"<audio src="/song.mp3"></audio>"#
485 );
486 }
487
488 #[test]
489 fn an_iframe_carries_its_title_and_lazy_loading() {
490 assert_eq!(
491 iframe_titled("https://example.com", "Example").render(),
492 r#"<iframe src="https://example.com" title="Example" loading="lazy"></iframe>"#
493 );
494 }
495
496 #[test]
497 fn details_renders_open_with_a_summary() {
498 let details = details()
499 .bool_attr("open")
500 .child(summary().text("More"))
501 .child(p().text("Hidden text"));
502
503 assert_eq!(
504 details.render(),
505 "<details open><summary>More</summary><p>Hidden text</p></details>"
506 );
507 }
508
509 #[test]
510 fn details_is_closed_unless_open_is_set() {
511 assert_eq!(
512 details().child(summary().text("More")).render(),
513 "<details><summary>More</summary></details>"
514 );
515 }
516
517 #[test]
518 fn a_definition_list_pairs_terms_with_descriptions() {
519 let list = dl()
520 .child(dt().text("Molt"))
521 .child(dd().text("An HTML library for Rust"));
522
523 assert_eq!(
524 list.render(),
525 "<dl><dt>Molt</dt><dd>An HTML library for Rust</dd></dl>"
526 );
527 }
528
529 #[test]
530 fn the_text_semantic_tags_render_their_own_names() {
531 assert_eq!(
532 blockquote().text("Quoted").render(),
533 "<blockquote>Quoted</blockquote>"
534 );
535 assert_eq!(
536 q().attr("cite", "https://example.com")
537 .text("Short")
538 .render(),
539 r#"<q cite="https://example.com">Short</q>"#
540 );
541 assert_eq!(cite().text("Moby Dick").render(), "<cite>Moby Dick</cite>");
542 assert_eq!(
543 abbr()
544 .attr("title", "HyperText Markup Language")
545 .text("HTML")
546 .render(),
547 r#"<abbr title="HyperText Markup Language">HTML</abbr>"#
548 );
549 assert_eq!(address().text("Rua 1").render(), "<address>Rua 1</address>");
550 assert_eq!(sub().text("2").render(), "<sub>2</sub>");
551 assert_eq!(sup().text("2").render(), "<sup>2</sup>");
552 assert_eq!(del().text("old").render(), "<del>old</del>");
553 assert_eq!(ins().text("new").render(), "<ins>new</ins>");
554 assert_eq!(kbd().text("\u{2318}S").render(), "<kbd>\u{2318}S</kbd>");
555 assert_eq!(samp().text("ok").render(), "<samp>ok</samp>");
556 assert_eq!(var().text("x").render(), "<var>x</var>");
557 assert_eq!(dialog().text("Hi").render(), "<dialog>Hi</dialog>");
558 assert_eq!(
559 noscript().text("Enable JS").render(),
560 "<noscript>Enable JS</noscript>"
561 );
562 assert_eq!(
563 canvas().attr("width", "300").render(),
564 r#"<canvas width="300"></canvas>"#
565 );
566 }
567
568 #[test]
572 fn a_stylesheet_is_not_escaped() {
573 assert_eq!(
574 style()
575 .attr("media", "screen")
576 .raw_text("a > b { color: red; }")
577 .render(),
578 r#"<style media="screen">a > b { color: red; }</style>"#
579 );
580 }
581
582 #[test]
584 fn media_elements_render_only_the_attributes_they_are_given() {
585 assert_eq!(
586 source()
587 .attr("src", "a.mp4")
588 .attr("media", "(min-width: 40em)")
589 .render(),
590 r#"<source src="a.mp4" media="(min-width: 40em)">"#
591 );
592 assert_eq!(
593 track()
594 .attr("src", "t.vtt")
595 .attr("kind", "captions")
596 .attr("label", "PT")
597 .render(),
598 r#"<track src="t.vtt" kind="captions" label="PT">"#
599 );
600 assert_eq!(
601 video().bool_attr("controls").render(),
602 "<video controls></video>"
603 );
604 assert_eq!(
605 audio().bool_attr("autoplay").bool_attr("loop").render(),
606 "<audio autoplay loop></audio>"
607 );
608 assert_eq!(
609 iframe()
610 .attr("src", "/e")
611 .attr("title", "E")
612 .bool_attr("allowfullscreen")
613 .render(),
614 r#"<iframe src="/e" title="E" allowfullscreen></iframe>"#
615 );
616 assert_eq!(
617 base().attr("href", "/").attr("target", "_blank").render(),
618 r#"<base href="/" target="_blank">"#
619 );
620 assert_eq!(col().render(), "<col>");
621 assert_eq!(q().text("quoted").render(), "<q>quoted</q>");
622 }
623
624 #[test]
625 fn meta_renders_whichever_attributes_it_is_given() {
626 assert_eq!(
627 meta()
628 .attr("http-equiv", "refresh")
629 .attr("content", "5")
630 .render(),
631 r#"<meta http-equiv="refresh" content="5">"#
632 );
633 assert_eq!(
634 meta().attr("itemprop", "name").render(),
635 r#"<meta itemprop="name">"#
636 );
637 }
638
639 #[test]
640 fn flow_containers_accept_text_as_well_as_children() {
641 assert_eq!(aside().text("Note").render(), "<aside>Note</aside>");
642 assert_eq!(nav().text("Menu").render(), "<nav>Menu</nav>");
643 assert_eq!(header().text("Top").render(), "<header>Top</header>");
644 assert_eq!(footer().text("Bottom").render(), "<footer>Bottom</footer>");
645 assert_eq!(main_tag().text("Body").render(), "<main>Body</main>");
646 assert_eq!(article().text("Post").render(), "<article>Post</article>");
647 assert_eq!(figure().text("Fig").render(), "<figure>Fig</figure>");
648 assert_eq!(form().text("F").render(), "<form>F</form>");
649 assert_eq!(fieldset().text("Set").render(), "<fieldset>Set</fieldset>");
650 }
651
652 #[test]
653 fn flow_container_content_is_escaped() {
654 assert_eq!(
655 aside().text("<b>x</b>").render(),
656 "<aside><b>x</b></aside>"
657 );
658 }
659
660 #[test]
661 fn a_fieldset_carries_a_legend_and_its_fields() {
662 let fieldset = fieldset()
663 .child(legend().text("Account"))
664 .child(input_named("text", "email"));
665
666 assert_eq!(
667 fieldset.render(),
668 r#"<fieldset><legend>Account</legend><input type="text" name="email"></fieldset>"#
669 );
670 }
671
672 #[test]
673 fn a_select_groups_its_options() {
674 let select = select().attr("name", "city").child(
675 optgroup()
676 .attr("label", "Brazil")
677 .child(option().attr("value", "sp").text("S\u{e3}o Paulo")),
678 );
679
680 assert_eq!(
681 select.render(),
682 concat!(
683 r#"<select name="city"><optgroup label="Brazil">"#,
684 "<option value=\"sp\">S\u{e3}o Paulo</option></optgroup></select>",
685 )
686 );
687 }
688
689 #[test]
690 fn the_form_display_elements_render() {
691 assert_eq!(
692 datalist().child(option().attr("value", "rust")).render(),
693 r#"<datalist><option value="rust"></option></datalist>"#
694 );
695 assert_eq!(
696 progress().attr("value", "0.7").attr("max", "1.0").render(),
697 r#"<progress value="0.7" max="1.0"></progress>"#
698 );
699 assert_eq!(
700 progress().attr("max", "1.0").render(),
701 r#"<progress max="1.0"></progress>"#
702 );
703 assert_eq!(
704 meter()
705 .attr("value", "6.0")
706 .attr("min", "0.0")
707 .attr("max", "10.0")
708 .render(),
709 r#"<meter value="6.0" min="0.0" max="10.0"></meter>"#
710 );
711 assert_eq!(output().text("42").render(), "<output>42</output>");
712 }
713
714 #[test]
715 fn boolean_attributes_carry_no_value() {
716 let rendered = input_named("checkbox", "agree")
717 .bool_attr("checked")
718 .bool_attr("required")
719 .render();
720
721 assert_eq!(
722 rendered,
723 r#"<input type="checkbox" name="agree" checked required>"#
724 );
725 assert!(!rendered.contains("checked="));
726 assert!(!rendered.contains("required="));
727 }
728
729 #[test]
730 fn void_elements_do_not_self_close_by_default() {
731 let rendered = image("a.png", "A").render();
732
733 assert_eq!(rendered, r#"<img src="a.png" alt="A">"#);
734 assert!(!rendered.contains("/>"));
735 }
736
737 #[test]
738 fn xhtml_mode_self_closes_void_elements() {
739 let options = RenderOptions::compact().with_xhtml_self_closing(true);
740
741 assert_eq!(
742 image("a.png", "A").render_with(&options),
743 r#"<img src="a.png" alt="A" />"#
744 );
745 }
746
747 #[test]
748 fn anchors_and_headings_take_element_children() {
749 assert_eq!(
750 link_to("/news").child(image("thumb.jpg", "Thumb")).render(),
751 r#"<a href="/news"><img src="thumb.jpg" alt="Thumb"></a>"#
752 );
753 assert_eq!(
754 h3().child(link_to("/news").text("Headline")).render(),
755 r#"<h3><a href="/news">Headline</a></h3>"#
756 );
757 assert_eq!(
758 i().add_class("fas fa-search").render(),
759 r#"<i class="fas fa-search"></i>"#
760 );
761 }
762
763 #[test]
764 fn a_submit_button_carries_its_type() {
765 assert_eq!(
766 button_typed("submit").text("Send").render(),
767 r#"<button type="submit">Send</button>"#
768 );
769 }
770
771 #[test]
772 fn a_label_without_a_target_has_no_for_attribute() {
773 let rendered = label().text("Accept cookies").render();
774
775 assert_eq!(rendered, "<label>Accept cookies</label>");
776 assert!(!rendered.contains("for="));
777 }
778
779 #[test]
780 fn an_input_without_a_name_has_no_name_attribute() {
781 let rendered = input()
782 .attr("type", "search")
783 .attr("placeholder", "Search")
784 .render();
785
786 assert_eq!(rendered, r#"<input type="search" placeholder="Search">"#);
787 assert!(!rendered.contains("name="));
788 }
789
790 #[test]
791 fn a_section_renders_its_class_and_text() {
792 assert_eq!(
793 section().add_class("hero").text("Hello").render(),
794 r#"<section class="hero">Hello</section>"#
795 );
796 }
797
798 #[test]
799 fn the_inline_semantic_tags_render() {
800 assert_eq!(strong().text("bold").render(), "<strong>bold</strong>");
801 assert_eq!(em().text("emph").render(), "<em>emph</em>");
802 assert_eq!(small().text("fine").render(), "<small>fine</small>");
803 assert_eq!(br().render(), "<br>");
804 assert_eq!(hr().render(), "<hr>");
805 }
806
807 #[test]
808 fn a_pre_block_keeps_its_line_breaks() {
809 let block =
810 pre().text("This is preformatted text.\nIt preserves whitespace and line breaks.");
811
812 assert_eq!(
813 block.render(),
814 "<pre>This is preformatted text.\nIt preserves whitespace and line breaks.</pre>"
815 );
816 }
817
818 #[test]
819 fn a_code_block_keeps_its_line_breaks() {
820 let block = code().text("let x = 10\nprint(x)");
821
822 assert_eq!(block.render(), "<code>let x = 10\nprint(x)</code>");
823 }
824
825 #[test]
826 fn an_embed_is_a_void_element() {
827 assert_eq!(
828 embed()
829 .attr("src", "video.mp4")
830 .attr("type", "video/mp4")
831 .render(),
832 r#"<embed src="video.mp4" type="video/mp4">"#
833 );
834 }
835
836 #[test]
837 fn an_article_wraps_its_children() {
838 assert_eq!(
839 article().child(h1().text("Title")).render(),
840 "<article><h1>Title</h1></article>"
841 );
842 }
843
844 #[test]
845 fn an_aside_wraps_its_children() {
846 assert_eq!(
847 aside().child(p().text("Sidebar")).render(),
848 "<aside><p>Sidebar</p></aside>"
849 );
850 }
851
852 #[test]
853 fn a_figure_pairs_an_image_with_its_caption() {
854 let block = figure()
855 .child(image("image.jpg", "Test"))
856 .child(figcaption().text("Image caption"));
857
858 assert_eq!(
859 block.render(),
860 concat!(
861 r#"<figure><img src="image.jpg" alt="Test">"#,
862 "<figcaption>Image caption</figcaption></figure>",
863 )
864 );
865 }
866
867 #[test]
868 fn a_time_element_carries_its_datetime() {
869 assert_eq!(
870 time()
871 .attr("datetime", "2024-01-15")
872 .text("January 15, 2024")
873 .render(),
874 r#"<time datetime="2024-01-15">January 15, 2024</time>"#
875 );
876 }
877
878 #[test]
879 fn a_mark_element_renders_its_text() {
880 assert_eq!(
881 mark().text("highlighted").render(),
882 "<mark>highlighted</mark>"
883 );
884 }
885
886 #[test]
887 fn every_heading_level_renders_its_own_tag() {
888 let headings = [
889 h1().text("H1"),
890 h2().text("H2"),
891 h3().text("H3"),
892 h4().text("H4"),
893 h5().text("H5"),
894 h6().text("H6"),
895 ];
896
897 for (index, heading) in headings.into_iter().enumerate() {
898 let level = index + 1;
899 assert_eq!(heading.render(), format!("<h{level}>H{level}</h{level}>"));
900 }
901 }
902
903 #[test]
904 fn a_form_renders_its_fieldsets_and_submit() {
905 let form = form()
906 .attr("action", "/submit")
907 .child(
908 fieldset()
909 .child(label_for("name").text("Name"))
910 .child(input_named("text", "name")),
911 )
912 .child(
913 fieldset()
914 .child(label_for("message").text("Message"))
915 .child(textarea().attr("name", "message")),
916 )
917 .child(input_named("submit", "submit").attr("value", "Send"));
918
919 assert_eq!(
920 form.render(),
921 concat!(
922 r#"<form action="/submit"><fieldset><label for="name">Name</label>"#,
923 r#"<input type="text" name="name"></fieldset><fieldset>"#,
924 r#"<label for="message">Message</label>"#,
925 r#"<textarea name="message"></textarea></fieldset>"#,
926 r#"<input type="submit" name="submit" value="Send"></form>"#,
927 )
928 );
929 }
930
931 #[test]
932 fn a_select_renders_each_option() {
933 let select = select()
934 .attr("name", "options")
935 .children_from((1..=3).map(|i| {
936 option()
937 .attr("value", i.to_string())
938 .text(format!("Option {i}"))
939 }));
940
941 assert_eq!(
942 select.render(),
943 concat!(
944 r#"<select name="options"><option value="1">Option 1</option>"#,
945 r#"<option value="2">Option 2</option>"#,
946 r#"<option value="3">Option 3</option></select>"#,
947 )
948 );
949 }
950
951 #[test]
952 fn a_label_points_at_its_field() {
953 assert_eq!(
954 label_for("username").text("Username").render(),
955 r#"<label for="username">Username</label>"#
956 );
957 }
958
959 #[test]
960 fn an_input_renders_its_type_name_and_value() {
961 assert_eq!(
962 input_named("text", "username")
963 .attr("value", "JohnDoe")
964 .render(),
965 r#"<input type="text" name="username" value="JohnDoe">"#
966 );
967 }
968
969 #[test]
970 fn a_textarea_renders_its_content_between_the_tags() {
971 assert_eq!(
972 textarea()
973 .attr("name", "message")
974 .text("Hello, World!")
975 .render(),
976 r#"<textarea name="message">Hello, World!</textarea>"#
977 );
978 }
979
980 #[test]
981 fn a_fieldset_groups_a_label_and_its_field() {
982 let block = fieldset()
983 .child(label_for("name").text("Name"))
984 .child(input_named("text", "name"));
985
986 assert_eq!(
987 block.render(),
988 concat!(
989 r#"<fieldset><label for="name">Name</label>"#,
990 r#"<input type="text" name="name"></fieldset>"#,
991 )
992 );
993 }
994
995 #[test]
996 fn a_section_wraps_its_children() {
997 assert_eq!(
998 section().child(p().text("This is a section.")).render(),
999 "<section><p>This is a section.</p></section>"
1000 );
1001 }
1002}