use crate::component::{Component, ComponentContext};
use crate::error::Result;
use crate::html::Html;
pub struct ComponentTestHarness<C: Component> {
component: C,
context: ComponentContext,
}
impl<C: Component> ComponentTestHarness<C> {
pub fn new(component: C) -> Self {
Self {
component,
context: ComponentContext::new(),
}
}
pub fn with_context(mut self, context: ComponentContext) -> Self {
self.context = context;
self
}
pub fn render(&self, props: &C::Props) -> Result<Html> {
self.component.render(props, &self.context)
}
pub fn render_string(&self, props: &C::Props) -> Result<String> {
Ok(self.render(props)?.render())
}
}
pub struct HtmlAssertion {
rendered: String,
}
impl HtmlAssertion {
pub fn new(html: &Html) -> Self {
Self {
rendered: html.render(),
}
}
pub fn from_string<S: Into<String>>(rendered: S) -> Self {
Self {
rendered: rendered.into(),
}
}
pub fn contains(self, needle: &str) -> Self {
assert!(
self.rendered.contains(needle),
"expected rendered HTML to contain `{needle}`, got:\n{}",
self.rendered
);
self
}
pub fn not_contains(self, needle: &str) -> Self {
assert!(
!self.rendered.contains(needle),
"expected rendered HTML NOT to contain `{needle}`, got:\n{}",
self.rendered
);
self
}
pub fn element_count(self, tag: &str, n: usize) -> Self {
let needle = format!("<{tag}");
let count = self.rendered.matches(&needle).count();
assert_eq!(
count, n,
"expected {n} occurrence(s) of `<{tag}`, got {count} in:\n{}",
self.rendered
);
self
}
pub fn as_str(&self) -> &str {
&self.rendered
}
}
#[macro_export]
macro_rules! assert_html_contains {
($html:expr, $needle:expr $(,)?) => {{
let rendered: String = $crate::testing::__render_for_assert($html);
assert!(
rendered.contains($needle),
"expected rendered HTML to contain `{}`, got:\n{}",
$needle,
rendered
);
}};
}
#[macro_export]
macro_rules! assert_renders_to {
($html:expr, $expected:expr $(,)?) => {{
let rendered: String = $crate::testing::__render_for_assert($html);
assert_eq!(rendered, $expected);
}};
}
#[doc(hidden)]
pub fn __render_for_assert<T: Renderable>(value: T) -> String {
value.render_to_string()
}
pub trait Renderable {
fn render_to_string(self) -> String;
}
impl Renderable for &Html {
fn render_to_string(self) -> String {
self.render()
}
}
impl Renderable for &str {
fn render_to_string(self) -> String {
self.to_string()
}
}
impl Renderable for &String {
fn render_to_string(self) -> String {
self.clone()
}
}
impl Renderable for String {
fn render_to_string(self) -> String {
self
}
}
pub mod prelude {
pub use super::{ComponentTestHarness, HtmlAssertion};
pub use crate::component::{Component, ComponentContext, ComponentProps};
pub use crate::html::Html;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::component::ComponentProps;
use crate::html::{div, HtmlElement};
#[derive(Debug, Clone)]
struct EchoProps {
msg: String,
}
impl ComponentProps for EchoProps {}
#[derive(Debug)]
struct Echo;
impl Component for Echo {
type Props = EchoProps;
fn render(
&self,
props: &Self::Props,
_ctx: &ComponentContext,
) -> Result<Html> {
Ok(Html::Element(div().class("echo").text(&props.msg)))
}
}
#[test]
fn harness_renders_component() {
let harness = ComponentTestHarness::new(Echo);
let html = harness
.render(&EchoProps {
msg: "hello".into(),
})
.unwrap();
HtmlAssertion::new(&html)
.contains("hello")
.contains("class=\"echo\"");
}
#[test]
fn harness_render_string_matches_render() {
let harness = ComponentTestHarness::new(Echo);
let s = harness
.render_string(&EchoProps {
msg: "alpha".into(),
})
.unwrap();
assert!(s.contains("alpha"));
}
#[test]
fn assertion_element_count() {
let tree = Html::Element(
HtmlElement::new("ul")
.child(Html::Element(HtmlElement::new("li").text("a")))
.child(Html::Element(HtmlElement::new("li").text("b")))
.child(Html::Element(HtmlElement::new("li").text("c"))),
);
HtmlAssertion::new(&tree).element_count("li", 3);
}
#[test]
#[should_panic(expected = "expected rendered HTML to contain `nope`")]
fn assertion_contains_panics_on_miss() {
let html = Html::Element(div().text("yep"));
HtmlAssertion::new(&html).contains("nope");
}
}