ferrovue 0.8.0

Vue templates compiled to Rust render functions: server-side rendering with no JavaScript at run time
Documentation
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};

struct Fixtures(PathBuf);

impl Fixtures {
    fn new(files: &[(&str, &str)]) -> Self {
        static NEXT: AtomicUsize = AtomicUsize::new(0);
        let dir = std::env::temp_dir().join(format!(
            "ferrovue-fixtures-{}-{}",
            std::process::id(),
            NEXT.fetch_add(1, Ordering::Relaxed)
        ));
        for (path, text) in files {
            let path = dir.join(path);
            fs::create_dir_all(path.parent().unwrap()).unwrap();
            fs::write(path, text).unwrap();
        }
        fs::create_dir_all(&dir).unwrap();
        Self(dir)
    }
}

impl Drop for Fixtures {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

fn render_json(component: &str, json: &str) -> Result<String, String> {
    let props: serde_json::Value = serde_json::from_str(json).map_err(|e| e.to_string())?;
    match component {
        "Hello" => Ok(format!("<p>Hello, {}</p>", props["name"].as_str().unwrap())),
        other => Err(format!("no component called {other}")),
    }
}

const ADA: [(&str, &str); 2] = [
    ("Hello/ada.json", r#"{"name":"Ada"}"#),
    ("Hello/ada.html", "<p>Hello, Ada</p>"),
];

#[test]
fn counts_the_fixtures_that_render_as_recorded() {
    let fixtures = Fixtures::new(&[
        ADA[0],
        ADA[1],
        ("Hello/grace.json", r#"{"name":"Grace"}"#),
        ("Hello/grace.html", "<p>Hello, Grace</p>"),
        ("Hello/notes.txt", "not a fixture"),
    ]);
    assert_eq!(check_fixtures(&fixtures.0, 2, render_json), 2);
}

#[test]
fn reports_where_a_render_differs_from_the_recorded_html() {
    let fixtures = Fixtures::new(&[
        ADA[0],
        ("Hello/ada.html", "<p>Hello, Ada</p>\n"),
        ("Hello/zed.json", r#"{"name":"Zé"}"#),
        ("Hello/zed.html", "<p>\nHello, Zè</p>"),
    ]);
    let report = compare(&fixtures.0, 1, &render_json).unwrap_err();
    assert_eq!(
        report,
        format!(
            "the fixtures under {} do not all render as Vue rendered them:\n\n\
             Hello/ada.json: the Rust render differs from the recorded HTML, first difference at byte 17 (line 1, column 18):\n  \
             recorded: \"<p>Hello, Ada</p>\\n\"\n  \
             rendered: \"<p>Hello, Ada</p>\"\n\n\
             Hello/zed.json: the Rust render differs from the recorded HTML, first difference at byte 3 (line 1, column 4):\n  \
             recorded: \"<p>\\nHello, Zè</p>\"\n  \
             rendered: \"<p>Hello, Zé</p>\"",
            fixtures.0.display()
        )
    );
}

#[test]
fn reports_a_difference_inside_a_character_from_where_it_starts() {
    assert_eq!(
        first_difference("<p>é</p>", "<p>è</p>"),
        "first difference at byte 3 (line 1, column 4):\n  recorded: \"<p>é</p>\"\n  rendered: \"<p>è</p>\""
    );
    let long = "x".repeat(100);
    assert_eq!(
        first_difference(&format!("{long}\n{long}a"), &format!("{long}\n{long}b")),
        format!(
            "first difference at byte 201 (line 2, column 101):\n  recorded: {:?}\n  rendered: {:?}",
            format!("{}a", "x".repeat(40)),
            format!("{}b", "x".repeat(40)),
        )
    );
}

#[test]
fn reports_a_fixture_without_recorded_html_or_without_a_component() {
    let fixtures = Fixtures::new(&[
        ("Hello/ada.json", r#"{"name":"Ada"}"#),
        ("Ghost/one.json", "{}"),
        ("Ghost/one.html", ""),
    ]);
    let report = compare(&fixtures.0, 1, &render_json).unwrap_err();
    assert!(
        report.ends_with(
            "\n\nGhost/one.json: no component called Ghost\n\n\
             Hello/ada.json: no recorded HTML: record it from Vue with FERROVUE_FIXTURES_WRITE=1"
        ),
        "{report}"
    );
}

#[test]
fn fails_when_fewer_fixtures_are_found_than_expected() {
    let fixtures = Fixtures::new(&ADA);
    let report = compare(&fixtures.0, 2, &render_json).unwrap_err();
    assert!(
        report.ends_with(&format!(
            "found 1 fixtures under {}, and expected at least 2",
            fixtures.0.display()
        )),
        "{report}"
    );
    let empty = Fixtures::new(&[]);
    assert!(compare(&empty.0, 1, &render_json).is_err());
    assert_eq!(compare(&empty.0, 0, &render_json), Ok(0));
}

#[test]
fn fails_when_the_fixtures_cannot_be_read() {
    let missing = std::env::temp_dir().join("ferrovue-no-such-fixtures");
    let report = compare(&missing, 1, &render_json).unwrap_err();
    assert!(
        report.starts_with(&format!("cannot read {}: ", missing.display())),
        "{report}"
    );
}

#[test]
#[should_panic(expected = "Hello/ada.json: the Rust render differs from the recorded HTML")]
fn check_fixtures_panics_with_the_report() {
    let fixtures = Fixtures::new(&[ADA[0], ("Hello/ada.html", "<p>Hi, Ada</p>")]);
    check_fixtures(&fixtures.0, 1, render_json);
}