");
+ assert_eq!(nodes.len(), 1);
+ if let Some(Node::Element(div)) = nodes.first() {
+ assert_eq!(div.tag_name, "div");
+ assert_eq!(div.children.len(), 2);
+ if let Some(Node::Element(span)) = div.children.first() {
+ assert_eq!(span.tag_name, "span");
+ assert_eq!(span.text_content(), Some("Hello".into()));
+ }
+ }
+ }
+
+ #[test]
+ fn test_deeply_nested_fragment() {
+ let nodes = parse_fragment("
Deep
");
+ assert_eq!(nodes.len(), 1);
+ if let Some(Node::Element(div)) = nodes.first() {
+ let ul = div.find_element("ul").unwrap();
+ let li = ul.find_element("li").unwrap();
+ let span = li.find_element("span").unwrap();
+ assert_eq!(span.text_content(), Some("Deep".into()));
+ }
+ }
+
+ #[test]
+ fn test_fragment_void_elements() {
+ let nodes = parse_fragment("
After
");
+ assert_eq!(nodes.len(), 1);
+ if let Some(Node::Element(div)) = nodes.first() {
+ // br is void, should not nest span inside it
+ assert_eq!(div.children.len(), 2);
+ if let Some(Node::Element(br)) = div.children.first() {
+ assert_eq!(br.tag_name, "br");
+ assert!(br.children.is_empty());
+ }
+ }
+ }
+
+ #[test]
+ fn test_fragment_multiple_top_level() {
+ let nodes = parse_fragment("
One
Two
Three
");
+ assert_eq!(nodes.len(), 3);
+ }
+
+ #[test]
+ fn test_many_children_fragment() {
+ use core::fmt::Write;
+ let mut html = String::from("
");
+ for i in 0..1100 {
+ let _ = write!(html, "{i}");
+ }
+ html.push_str("
");
+ let nodes = parse_fragment(&html);
+ assert_eq!(nodes.len(), 1);
+ if let Some(Node::Element(div)) = nodes.first() {
+ assert_eq!(div.children.len(), 1100);
+ }
+ }
+
+ #[test]
+ fn test_unmatched_end_tag() {
+ // Unmatched should not crash or empty the stack
+ let nodes = parse_fragment("
Hello
");
+ assert_eq!(nodes.len(), 1);
+ if let Some(Node::Element(div)) = nodes.first() {
+ assert_eq!(div.tag_name, "div");
+ }
+ }
+
+ // ── pop_until tests ──────────────────────────────────────────────
+
+ #[test]
+ fn test_pop_until_skips_intermediate() {
+ // should pop both and , closing at
+ let nodes = parse_fragment("
Text
");
+ assert_eq!(nodes.len(), 1);
+ let div = nodes[0].as_element().unwrap();
+ assert_eq!(div.tag_name, "div");
+ // span was opened, em was opened inside span, then
+ // pops em, span, div
+ let span = div.find_element("span").unwrap();
+ let em = span.find_element("em").unwrap();
+ assert_eq!(em.text_content(), Some("Text".into()));
+ }
+
+ #[test]
+ fn test_pop_until_no_match_preserves_root() {
+ // pops elements but stops at root sentinel
+ let nodes = parse_fragment("
should only close the inner one
+ let nodes = parse_fragment("
Inner
Outer
");
+ assert_eq!(nodes.len(), 1);
+ let outer = nodes[0].as_element().unwrap();
+ assert_eq!(outer.tag_name, "div");
+ assert_eq!(outer.children.len(), 2);
+ // First child: inner div
+ let inner = outer.children[0].as_element().unwrap();
+ assert_eq!(inner.tag_name, "div");
+ assert_eq!(
+ inner.find_element("span").unwrap().text_content(),
+ Some("Inner".into())
+ );
+ // Second child: outer span (after inner div was closed)
+ let outer_span = outer.children[1].as_element().unwrap();
+ assert_eq!(outer_span.tag_name, "span");
+ assert_eq!(outer_span.text_content(), Some("Outer".into()));
+ }
+
+ #[test]
+ fn test_pop_until_multiple_same_tag() {
+ // Three nested s, one closes only the innermost
+ let nodes = parse_fragment("
DeepMidTop
");
+ assert_eq!(nodes.len(), 1);
+ let div = nodes[0].as_element().unwrap();
+ let s1 = div.find_element("span").unwrap();
+ let s2 = s1.find_element("span").unwrap();
+ let s3 = s2.find_element("span").unwrap();
+ assert_eq!(s3.text_content(), Some("Deep".into()));
+ // "Mid" is text after inner span closes, inside middle span
+ assert!(s2.children.len() >= 2);
+ // "Top" is text after middle span closes, inside outer span
+ assert!(s1.children.len() >= 2);
+ }
+
+ // ── fragment nesting tests ───────────────────────────────────────
+
+ #[test]
+ fn test_fragment_five_levels_deep() {
+ let nodes = parse_fragment(
+ "
Title
\
+
",
+ );
+ assert_eq!(nodes.len(), 1);
+ let div = nodes[0].as_element().unwrap();
+ let section = div.find_element("section").unwrap();
+ let article = section.find_element("article").unwrap();
+ let header = article.find_element("header").unwrap();
+ let h1 = header.find_element("h1").unwrap();
+ assert_eq!(h1.text_content(), Some("Title".into()));
+ }
+
+ #[test]
+ fn test_fragment_text_at_every_level() {
+ let nodes = parse_fragment("
ABCDE
");
+ assert_eq!(nodes.len(), 1);
+ let div = nodes[0].as_element().unwrap();
+ // div has: text("A"), span, text("E")
+ assert_eq!(div.children.len(), 3);
+ assert_eq!(div.children[0].as_text().unwrap().data, "A");
+ let span = div.children[1].as_element().unwrap();
+ // span has: text("B"), em, text("D")
+ assert_eq!(span.children.len(), 3);
+ assert_eq!(span.children[0].as_text().unwrap().data, "B");
+ let em = span.children[1].as_element().unwrap();
+ assert_eq!(em.text_content(), Some("C".into()));
+ assert_eq!(span.children[2].as_text().unwrap().data, "D");
+ assert_eq!(div.children[2].as_text().unwrap().data, "E");
+ }
+
+ #[test]
+ fn test_fragment_siblings_with_children() {
+ let nodes = parse_fragment("
One!
Two
Three
");
+ assert_eq!(nodes.len(), 1);
+ let ul = nodes[0].as_element().unwrap();
+ assert_eq!(ul.children.len(), 3);
+ // First li has text + em
+ let li1 = ul.children[0].as_element().unwrap();
+ assert_eq!(li1.children.len(), 2);
+ assert_eq!(li1.children[0].as_text().unwrap().data, "One");
+ assert_eq!(
+ li1.children[1].as_element().unwrap().text_content(),
+ Some("!".into())
+ );
+ // Second and third are simple
+ let li2 = ul.children[1].as_element().unwrap();
+ assert_eq!(li2.text_content(), Some("Two".into()));
+ let li3 = ul.children[2].as_element().unwrap();
+ assert_eq!(li3.text_content(), Some("Three".into()));
+ }
+
+ // ── fragment void element tests ──────────────────────────────────
+
+ #[test]
+ fn test_fragment_multiple_void_elements() {
+ let nodes = parse_fragment("
");
+ assert_eq!(nodes.len(), 1);
+ let div = nodes[0].as_element().unwrap();
+ assert_eq!(div.children.len(), 4);
+ assert_eq!(div.children[0].as_element().unwrap().tag_name, "br");
+ assert_eq!(div.children[1].as_element().unwrap().tag_name, "hr");
+ assert_eq!(div.children[2].as_element().unwrap().tag_name, "img");
+ assert_eq!(div.children[3].as_element().unwrap().tag_name, "input");
+ // None should have children
+ for child in &div.children {
+ assert!(child.as_element().unwrap().children.is_empty());
+ }
+ }
+
+ #[test]
+ fn test_fragment_void_between_text() {
+ let nodes = parse_fragment("
");
+ // No start tags to match, nothing produced
+ assert!(nodes.is_empty());
+ }
+
+ #[test]
+ fn test_malformed_extra_end_tags() {
+ let nodes = parse_fragment("
Hello
");
+ assert_eq!(nodes.len(), 1);
+ let div = nodes[0].as_element().unwrap();
+ assert_eq!(div.text_content(), Some("Hello".into()));
+ }
+
+ #[test]
+ fn test_malformed_unclosed_tags() {
+ // Tags that are never closed
+ let nodes = parse_fragment("
Text");
+ assert_eq!(nodes.len(), 1);
+ let div = nodes[0].as_element().unwrap();
+ let span = div.find_element("span").unwrap();
+ let em = span.find_element("em").unwrap();
+ assert_eq!(em.text_content(), Some("Text".into()));
+ }
+
+ #[test]
+ fn test_malformed_interleaved_tags() {
+ // - interleaved close order
+ let nodes = parse_fragment("TextAfter");
+ // After pops both i and b (pop_until finds b).
+ // "After" goes to root since both are closed.
+ // is unmatched, ignored.
+ assert!(!nodes.is_empty());
+ let b = nodes[0].as_element().unwrap();
+ assert_eq!(b.tag_name, "b");
+ }
+
+ #[test]
+ fn test_malformed_deeply_mismatched() {
+ let nodes = parse_fragment("Text");
+ // pops e, d, c, b, a
+ assert_eq!(nodes.len(), 1);
+ let a = nodes[0].as_element().unwrap();
+ assert_eq!(a.tag_name, "a");
+ assert!(a.find_element("e").is_some());
+ }
+
+ // ── full document tests ──────────────────────────────────────────
+
+ #[test]
+ fn test_document_head_elements() {
+ let doc = parse(
+ r#"
+ Test
+
+
+ "#,
+ );
+ let head = doc.head().unwrap();
+ assert!(head.find_element("title").is_some());
+ assert!(head.find_element("meta").is_some());
+ assert!(head.find_element("link").is_some());
+ }
+
+ #[test]
+ fn test_document_implicit_body() {
+ // No explicit tag, elements go into implicit body
+ let doc = parse("
Content
");
+ let body = doc.body().unwrap();
+ let div = body.find_element("div").unwrap();
+ assert_eq!(div.text_content(), Some("Content".into()));
+ }
+
+ #[test]
+ fn test_document_implicit_head_and_body() {
+ // No head or body, just content
+ let doc = parse("
Content
");
+ assert_eq!(doc.root.tag_name, "html");
+ assert!(doc.head().is_some());
+ assert!(doc.body().is_some());
+ let body = doc.body().unwrap();
+ let div = body.find_element("div").unwrap();
+ assert_eq!(div.text_content(), Some("Content".into()));
+ }
+
+ #[test]
+ fn test_document_title() {
+ let doc = parse(
+ "Hello World\
+ ",
+ );
+ assert_eq!(doc.title(), Some(String::from("Hello World")));
+ }
+
+ #[test]
+ fn test_document_round_trip() {
+ let html = "Test\
+
Hello
";
+ let doc = parse(html);
+ let output = doc.to_html();
+ // Re-parse the output and verify structure
+ let doc2 = parse(&output);
+ assert_eq!(doc2.title(), Some(String::from("Test")));
+ let body = doc2.body().unwrap();
+ let p = body.find_element("p").unwrap();
+ assert_eq!(p.text_content(), Some("Hello".into()));
+ }
}