topcoat-view-macro 0.2.0

A modular, batteries-included Rust web framework for server-rendered apps.
Documentation
use topcoat::{context::Cx, view::view};

fn r(v: topcoat::Result) -> String {
    v.unwrap().render(&Cx::default())
}

#[tokio::test]
async fn if_true_branch_emits_its_body() {
    let signed_in = true;
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        if signed_in {
            <a href="/account">"Account"</a>
        } else {
            <a href="/login">"Sign in"</a>
        }
    });

    assert_eq!(html, r#"<a href="/account">Account</a>"#);
}

#[tokio::test]
async fn if_false_branch_emits_else_body() {
    let signed_in = false;
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        if signed_in {
            <a href="/account">"Account"</a>
        } else {
            <a href="/login">"Sign in"</a>
        }
    });

    assert_eq!(html, r#"<a href="/login">Sign in</a>"#);
}

#[tokio::test]
async fn if_without_else_emits_nothing_on_false() {
    let show = false;
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        <div>
            if show {
                <p>"shown"</p>
            }
        </div>
    });

    assert_eq!(html, "<div></div>");
}

#[tokio::test]
async fn if_else_if_else_chain_selects_first_match() {
    let n = 1;
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        if n == 0 {
            <p>"zero"</p>
        } else if n == 1 {
            <p>"one"</p>
        } else {
            <p>"many"</p>
        }
    });

    assert_eq!(html, "<p>one</p>");
}

#[tokio::test]
async fn if_in_attribute_list_adds_branch_attributes() {
    let current = true;
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        <a
            href="/posts"
            if current {
                aria-current="page"
                class="active"
            }
        >
            "Posts"
        </a>
    });

    assert!(html.contains(r#"href="/posts""#));
    assert!(html.contains(r#"aria-current="page""#));
    assert!(html.contains(r#"class="active""#));
}

#[tokio::test]
async fn for_loop_renders_body_per_item() {
    let posts = ["alpha", "beta", "gamma"];
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        <ul>
            for title in posts {
                <li>(title)</li>
            }
        </ul>
    });

    assert_eq!(html, "<ul><li>alpha</li><li>beta</li><li>gamma</li></ul>");
}

#[tokio::test]
async fn for_loop_in_attribute_list_emits_attributes_per_item() {
    let extras = [("data-a", "1"), ("data-b", "2")];
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        <div
            for (name, value) in extras {
                (name)=(value)
            }
        >

        </div>
    });

    assert!(html.contains(r#"data-a="1""#));
    assert!(html.contains(r#"data-b="2""#));
}

#[tokio::test]
async fn for_loop_filtering_with_if_emits_subset() {
    let items = ["keep", "drop", "keep"];
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        <ul>
            for item in items {
                if item == "keep" {
                    <li>(item)</li>
                }
            }
        </ul>
    });

    assert_eq!(html, "<ul><li>keep</li><li>keep</li></ul>");
}

#[derive(Clone, Copy)]
#[allow(dead_code)]
enum Status {
    Draft,
    Published,
    Archived,
}

#[tokio::test]
async fn match_chooses_arm_body() {
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        match Status::Published {
            Status::Draft => <span>"draft"</span>,
            Status::Published => <a href="/post">"open"</a>,
            Status::Archived => <span>"archived"</span>,
        }
    });

    assert_eq!(html, r#"<a href="/post">open</a>"#);
}

#[tokio::test]
async fn match_arm_with_block_emits_multiple_siblings() {
    let user = Some("ada");
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        match user {
            Some(name) => {
                <h1>(name)</h1>
                <p>"signed in"</p>
            }
            None => <a href="/login">"sign in"</a>,
        }
    });

    assert_eq!(html, "<h1>ada</h1><p>signed in</p>");
}

#[tokio::test]
async fn match_in_attribute_list_emits_attribute_per_arm() {
    let status = Status::Draft;
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        <article
            match status {
                Status::Draft => class="draft",
                Status::Published => class="published",
                Status::Archived => class="archived",
            }
        >

        </article>
    });

    assert_eq!(html, r#"<article class="draft"></article>"#);
}

#[tokio::test]
async fn let_binding_introduces_variable_for_following_nodes() {
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        <article>
            let title = "  Hello  ".trim();

            <h1>(title)</h1>
            <p>(title)</p>
        </article>
    });

    assert_eq!(html, "<article><h1>Hello</h1><p>Hello</p></article>");
}

#[tokio::test]
async fn let_binding_in_attribute_list_is_in_scope_for_later_attributes() {
    let cx = &Cx::default();
    let html = r(view! {
        cx =>
        <a let href = "/posts"; href=(href) data-href=(href)>"Posts"</a>
    });

    assert!(html.contains(r#"href="/posts""#));
    assert!(html.contains(r#"data-href="/posts""#));
}