layout-cat 0.1.0

Box-model layout: cascade + block layout over a dom-cat tree using css-cat stylesheets. Produces a LayoutBox tree with positions and dimensions. No mut, no Rc/Arc, no interior mutability, no panics, exhaustive matches. Fourth sub-crate of a Servo-replacement webview runtime targeting Tauri.
//! Integration tests covering the cascade + block layout path.

#![allow(clippy::float_cmp)]

use layout_cat::{Error, LayoutBox, Viewport, layout};

fn run(html: &str, css: &str) -> Result<layout_cat::LayoutTree, Error> {
    let html_doc = html_cat::parse(html).map_err(|_| fail("html parse"))?;
    let dom = dom_cat::Document::from_html_doc(&html_doc);
    let sheet = css_cat::parse(css)?;
    Ok(layout(&dom, &sheet, Viewport::new(800, 600)))
}

fn fail(msg: &'static str) -> Error {
    Error::Css(css_cat::Error::EmptySelectorList {
        at: css_cat::span::Span::new(
            css_cat::span::Position::new(1, 1, 0),
            css_cat::span::Position::new(1, 1, 0),
        ),
    })
    .ignore(msg)
}

trait ErrorTagged {
    fn ignore(self, _msg: &'static str) -> Self;
}

impl ErrorTagged for Error {
    fn ignore(self, _msg: &'static str) -> Self {
        self
    }
}

fn first_descendant(node: &LayoutBox, idx: usize) -> Option<&LayoutBox> {
    node.children().get(idx)
}

#[test]
fn root_box_present() -> Result<(), Error> {
    let tree = run("<html><body><p>hi</p></body></html>", "")?;
    tree.root_box()
        .map(|_| ())
        .ok_or_else(|| fail("expected root box"))
}

#[test]
fn body_fills_viewport_width_by_default() -> Result<(), Error> {
    let tree = run("<html><body><p>hi</p></body></html>", "")?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    (body.rect().width() == 800.0)
        .then_some(())
        .ok_or_else(|| fail("body width != viewport"))
}

#[test]
fn explicit_width_applied() -> Result<(), Error> {
    let tree = run("<html><body><p>hi</p></body></html>", "p { width: 200px; }")?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    let p = first_descendant(body, 0).ok_or_else(|| fail("no p"))?;
    (p.rect().width() == 200.0)
        .then_some(())
        .ok_or_else(|| fail("p width != 200"))
}

#[test]
fn padding_consumed() -> Result<(), Error> {
    let tree = run(
        "<html><body><p>hi</p></body></html>",
        "p { width: 100px; padding: 8px; }",
    )?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    let p = first_descendant(body, 0).ok_or_else(|| fail("no p"))?;
    (p.padding().left() == 8.0 && p.padding().right() == 8.0)
        .then_some(())
        .ok_or_else(|| fail("padding wrong"))
}

#[test]
fn vertical_stacking() -> Result<(), Error> {
    let tree = run(
        "<html><body><p>a</p><p>b</p></body></html>",
        "p { height: 50px; }",
    )?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    let a = first_descendant(body, 0).ok_or_else(|| fail("no a"))?;
    let b = first_descendant(body, 1).ok_or_else(|| fail("no b"))?;
    (a.rect().origin().y() == 0.0 && b.rect().origin().y() == 50.0)
        .then_some(())
        .ok_or_else(|| fail("vertical stacking wrong"))
}

#[test]
fn margin_shorthand_four_sides() -> Result<(), Error> {
    let tree = run(
        "<html><body><p>x</p></body></html>",
        "p { margin: 10px 20px 30px 40px; }",
    )?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    let p = first_descendant(body, 0).ok_or_else(|| fail("no p"))?;
    let m = p.margin();
    (m.top() == 10.0 && m.right() == 20.0 && m.bottom() == 30.0 && m.left() == 40.0)
        .then_some(())
        .ok_or_else(|| fail("margin shorthand wrong"))
}

#[test]
fn padding_shorthand_two_values() -> Result<(), Error> {
    let tree = run(
        "<html><body><p>x</p></body></html>",
        "p { padding: 5px 10px; }",
    )?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    let p = first_descendant(body, 0).ok_or_else(|| fail("no p"))?;
    let pad = p.padding();
    (pad.top() == 5.0 && pad.right() == 10.0 && pad.bottom() == 5.0 && pad.left() == 10.0)
        .then_some(())
        .ok_or_else(|| fail("padding two-value wrong"))
}

#[test]
fn display_none_skipped() -> Result<(), Error> {
    let tree = run(
        "<html><body><p>shown</p><p class=\"hidden\">gone</p></body></html>",
        ".hidden { display: none; }",
    )?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    (body.children().len() == 1)
        .then_some(())
        .ok_or_else(|| fail("display:none should drop child"))
}

#[test]
fn color_resolved_from_hex() -> Result<(), Error> {
    let tree = run(
        "<html><body><p>x</p></body></html>",
        "p { color: #ff0000; }",
    )?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    let p = first_descendant(body, 0).ok_or_else(|| fail("no p"))?;
    let c = p.style().color();
    (c.red() > 0.99 && c.green() < 0.01 && c.blue() < 0.01)
        .then_some(())
        .ok_or_else(|| fail("color wrong"))
}

#[test]
fn percentage_width_resolved() -> Result<(), Error> {
    let tree = run("<html><body><p>x</p></body></html>", "p { width: 50%; }")?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    let p = first_descendant(body, 0).ok_or_else(|| fail("no p"))?;
    (p.rect().width() == 400.0)
        .then_some(())
        .ok_or_else(|| fail("percentage width wrong"))
}

#[test]
fn inline_style_attribute() -> Result<(), Error> {
    let tree = run(
        "<html><body><p style=\"width: 123px;\">x</p></body></html>",
        "",
    )?;
    let body = tree.root_box().ok_or_else(|| fail("no root"))?;
    let p = first_descendant(body, 0).ok_or_else(|| fail("no p"))?;
    (p.rect().width() == 123.0)
        .then_some(())
        .ok_or_else(|| fail("inline style ignored"))
}