dom-cat 0.1.0

Persistent DOM model: arena-backed Node tree with mutation API and CSS-selector matching. Consumes html-cat trees; selectors via css-cat. No mut, no Rc/Arc, no interior mutability, no panics, exhaustive matches. Third sub-crate of a Servo-replacement webview runtime targeting Tauri.
//! Integration tests covering build + query + mutation flows.

use dom_cat::{Document, Error, Node, query_selector, query_selector_all};

fn build(source: &str) -> Result<Document, Error> {
    let html_doc = html_cat::parse(source)?;
    Ok(Document::from_html_doc(&html_doc))
}

fn fail(msg: &'static str) -> Error {
    Error::InvalidSelector {
        selector: msg.to_owned(),
    }
}

#[test]
fn query_type_selector() -> Result<(), Error> {
    let doc = build("<html><body><p>hi</p></body></html>")?;
    query_selector(&doc, "p")?
        .map(|_| ())
        .ok_or_else(|| fail("expected p"))
}

#[test]
fn query_class_selector() -> Result<(), Error> {
    let doc = build("<html><body><p class=\"hi\">x</p><p>y</p></body></html>")?;
    let id = query_selector(&doc, ".hi")?.ok_or_else(|| fail("expected .hi"))?;
    let class = doc
        .get(id)
        .and_then(Node::as_element)
        .and_then(|e| e.attribute("class"))
        .ok_or_else(|| fail("expected class attr"))?;
    (class == "hi")
        .then_some(())
        .ok_or_else(|| fail("class mismatch"))
}

#[test]
fn query_id_selector() -> Result<(), Error> {
    let doc = build("<html><body><p id=\"main\">x</p></body></html>")?;
    query_selector(&doc, "#main")?
        .map(|_| ())
        .ok_or_else(|| fail("expected #main"))
}

#[test]
fn query_attribute_selector() -> Result<(), Error> {
    let doc = build("<html><body><input type=\"text\"><input type=\"radio\"></body></html>")?;
    let matches = query_selector_all(&doc, "input[type=\"text\"]")?;
    (matches.len() == 1)
        .then_some(())
        .ok_or_else(|| fail("expected one text input"))
}

#[test]
fn query_descendant_combinator() -> Result<(), Error> {
    let doc = build("<html><body><div><p>x</p></div><p>y</p></body></html>")?;
    let matches = query_selector_all(&doc, "div p")?;
    (matches.len() == 1)
        .then_some(())
        .ok_or_else(|| fail("expected one p under div"))
}

#[test]
fn query_child_combinator() -> Result<(), Error> {
    let doc =
        build("<html><body><div><p>x</p></div><div><span><p>y</p></span></div></body></html>")?;
    let matches = query_selector_all(&doc, "div > p")?;
    (matches.len() == 1)
        .then_some(())
        .ok_or_else(|| fail("expected one direct child p"))
}

#[test]
fn query_selector_list() -> Result<(), Error> {
    let doc = build("<html><body><h1>a</h1><h2>b</h2><p>c</p></body></html>")?;
    let matches = query_selector_all(&doc, "h1, h2")?;
    (matches.len() == 2)
        .then_some(())
        .ok_or_else(|| fail("expected h1 and h2"))
}

#[test]
fn query_first_child() -> Result<(), Error> {
    let doc = build("<html><body><p>a</p><p>b</p><p>c</p></body></html>")?;
    let matches = query_selector_all(&doc, "p:first-child")?;
    (matches.len() == 1)
        .then_some(())
        .ok_or_else(|| fail("expected one first-child p"))
}

#[test]
fn set_attribute_round_trip() -> Result<(), Error> {
    let doc = build("<html><body><p>x</p></body></html>")?;
    let p_id = query_selector(&doc, "p")?.ok_or_else(|| fail("expected p"))?;
    let updated = dom_cat::set_attribute(doc, p_id, "data-x", "42")?;
    let value = updated
        .get(p_id)
        .and_then(Node::as_element)
        .and_then(|e| e.attribute("data-x"))
        .ok_or_else(|| fail("attribute missing after set"))?;
    (value == "42")
        .then_some(())
        .ok_or_else(|| fail("attribute value mismatch"))
}

#[test]
fn remove_attribute_round_trip() -> Result<(), Error> {
    let doc = build("<html><body><p class=\"hi\">x</p></body></html>")?;
    let p_id = query_selector(&doc, "p")?.ok_or_else(|| fail("expected p"))?;
    let updated = dom_cat::remove_attribute(doc, p_id, "class")?;
    let missing = updated
        .get(p_id)
        .and_then(Node::as_element)
        .map(|e| e.attribute("class").is_none())
        .ok_or_else(|| fail("element missing"))?;
    missing
        .then_some(())
        .ok_or_else(|| fail("attribute still present after remove"))
}

#[test]
fn remove_child_drops_subtree() -> Result<(), Error> {
    let doc = build("<html><body><p>x</p><span>y</span></body></html>")?;
    let body_id = query_selector(&doc, "body")?.ok_or_else(|| fail("expected body"))?;
    let p_id = query_selector(&doc, "p")?.ok_or_else(|| fail("expected p"))?;
    let updated = dom_cat::remove_child(doc, body_id, p_id)?;
    let still_there = query_selector(&updated, "p")?;
    still_there
        .is_none()
        .then_some(())
        .ok_or_else(|| fail("p still present after remove"))
}