katana-canvas-forge 0.1.7

Versioned diagram rendering and document export runtime for KatanA (Mermaid, Draw.io, HTML/PDF/PNG/JPEG).
Documentation
use super::{
    MermaidJsRuntimeOps, MermaidRenderRequest, RuntimeBundleCache, ensure_svg, lock_cache,
    read_mermaid_bundle, rendered_svg,
};
use crate::markdown::color_preset::DiagramColorPreset;
use crate::markdown::runtime_assets::RuntimeAsset;
use std::collections::HashMap;
use std::sync::Mutex;

#[test]
fn fake_bundle_renders_svg_and_rejects_non_svg_output() {
    let path = std::env::temp_dir().join(format!(
        "kcf-mermaid-runtime-unit-{}.js",
        std::process::id()
    ));
    assert!(std::fs::write(&path, fake_bundle()).is_ok());
    assert!(read_mermaid_bundle(&path).is_ok());
    assert!(read_mermaid_bundle(&path).is_ok());

    let rendered =
        MermaidJsRuntimeOps::render("graph TD; A-->B", &path, DiagramColorPreset::dark());
    assert!(
        rendered.as_ref().is_ok_and(|svg| svg.contains("<svg")),
        "{rendered:?}"
    );
    assert!(ensure_svg("plain text").is_err());
    assert!(read_mermaid_bundle(std::path::Path::new("target/kcf-tests/missing.js")).is_err());
}

#[test]
fn zenuml_source_uses_v8_renderer_from_mermaid_surface() {
    let mermaid = RuntimeAsset::mermaid();
    let rendered = mermaid
        .materialize_at(mermaid.materialized_path())
        .and_then(|mermaid_js| {
            MermaidJsRuntimeOps::render(
                "zenuml\ntitle Surface Route\nA.method()",
                &mermaid_js,
                DiagramColorPreset::dark(),
            )
        });

    assert_zenuml_v8_svg(&rendered);
}

fn assert_zenuml_v8_svg(rendered: &Result<String, String>) {
    assert!(
        rendered
            .as_ref()
            .is_ok_and(|svg| svg.contains("<svg") && svg.contains("viewBox=")),
        "{rendered:?}"
    );
    assert!(
        rendered
            .as_ref()
            .is_ok_and(|svg| !svg.contains("<foreignObject")),
        "{rendered:?}"
    );
    assert!(
        rendered
            .as_ref()
            .is_ok_and(|svg| !svg.contains("data:image/png;base64,")),
        "{rendered:?}"
    );
}

#[test]
fn render_reports_missing_bundle_through_surface_path() {
    let result = MermaidJsRuntimeOps::render(
        "graph TD; A-->B",
        std::path::Path::new("target/kcf-tests/missing-mermaid-render.js"),
        DiagramColorPreset::dark(),
    );

    assert!(result.is_err());
}

#[test]
fn rendered_svg_rejects_plain_text_from_runtime() {
    assert!(rendered_svg("plain text".to_string()).is_err());
}

#[test]
fn poisoned_cache_reports_lock_error() {
    let cache: RuntimeBundleCache = Mutex::new(HashMap::new());
    let poison = std::panic::catch_unwind(|| poison_cache(&cache));

    assert!(poison.is_err());
    assert!(lock_cache(&cache).is_err());
    poison_cache(&cache);
}

fn poison_cache(cache: &RuntimeBundleCache) {
    let _guard = match cache.lock() {
        Ok(guard) => guard,
        Err(_) => return,
    };
    std::panic::resume_unwind(Box::new("poison mermaid cache"));
}

#[test]
fn request_id_changes_with_theme_and_source() {
    let dark = MermaidRenderRequest::new("graph TD; A-->B", DiagramColorPreset::dark());
    let light = MermaidRenderRequest::new("graph TD; A-->B", DiagramColorPreset::light());
    let other = MermaidRenderRequest::new("graph TD; B-->C", DiagramColorPreset::dark());

    assert!(dark.svg_id.starts_with("katana-mermaid-svg-"));
    assert_ne!(dark.svg_id, light.svg_id);
    assert_ne!(dark.svg_id, other.svg_id);
}

#[test]
fn request_fields_come_from_preset_not_global_state() {
    DiagramColorPreset::set_dark_mode(true);
    let request = MermaidRenderRequest::new("graph TD; A-->B", DiagramColorPreset::light());

    assert_eq!(request.theme, "default");
    assert_eq!(request.background, "transparent");
    assert_eq!(request.fill, "#fff2cc");
    assert_eq!(request.text, "#333333");
    assert_eq!(request.stroke, "#d6b656");
    assert_eq!(request.arrow, "#555555");
}

fn fake_bundle() -> &'static str {
    r#"
globalThis.mermaid = {
  initialize() {},
  render: async (id, source) => ({
    svg: `<svg xmlns="http://www.w3.org/2000/svg" id="${id}" width="20" height="10" viewBox="0 0 20 10"><text>${source}</text></svg>`
  })
};
"#
}