leaf-markdown-viewer 1.26.2

Terminal Markdown previewer with a GUI-like experience
use super::{lock_theme_test_state, test_assets, test_md_theme};
use crate::app::{App, AppConfig};
use crate::markdown::parse_markdown;
use crate::theme::{
    current_syntect_theme, current_theme_preset, current_theme_selection, resolve_theme_selection,
    set_theme_preset, set_theme_selection, theme_preset_index, validate_theme_syntax,
};
use crate::*;
use ratatui::style::Color;
use std::collections::BTreeMap;
use syntect::highlighting::ThemeSet;

#[test]
fn parse_theme_preset_supports_ocean_and_forest() {
    assert_eq!(parse_theme_preset("arctic"), Some(ThemePreset::Arctic));
    assert_eq!(parse_theme_preset("ocean"), Some(ThemePreset::OceanDark));
    assert_eq!(parse_theme_preset("forest"), Some(ThemePreset::Forest));
    assert_eq!(
        parse_theme_preset("solarized-dark"),
        Some(ThemePreset::SolarizedDark)
    );
    assert_eq!(parse_theme_preset("nope"), None);
}

#[test]
fn parse_theme_color_supports_hex_rgb_arrays_and_names() {
    assert_eq!(parse_theme_color("#112233"), Some(Color::Rgb(17, 34, 51)));
    assert_eq!(parse_theme_color("abc"), Some(Color::Rgb(170, 187, 204)));
    assert_eq!(parse_theme_color("rgb(1, 2, 3)"), Some(Color::Rgb(1, 2, 3)));
    assert_eq!(parse_theme_color("light-blue"), Some(Color::LightBlue));
    assert_eq!(parse_theme_color("not-a-color"), None);
}

#[test]
fn resolve_theme_selection_supports_custom_theme_overrides() {
    let custom: CustomThemeConfig = toml::from_str(
        r##"
base = "arctic"
syntax = "InspiredGitHub"

[ui]
content_bg = "#112233"

[markdown]
heading_1 = [1, 2, 3]
"##,
    )
    .unwrap();
    let mut themes = BTreeMap::new();
    themes.insert("midnight".to_string(), custom);

    let selection = resolve_theme_selection("midnight", &themes, None).unwrap();
    let ThemeSelection::Custom(custom) = selection else {
        panic!("expected custom theme selection");
    };

    assert_eq!(custom.name, "midnight");
    assert_eq!(custom.base_preset, ThemePreset::Arctic);
    assert_eq!(custom.theme.syntax_theme_name.as_ref(), "InspiredGitHub");
    assert_eq!(custom.theme.ui.content_bg, Color::Rgb(17, 34, 51));
    assert_eq!(custom.theme.markdown.heading_1, Color::Rgb(1, 2, 3));
    assert_eq!(
        custom.theme.markdown.heading_2,
        crate::theme::theme_by_preset(ThemePreset::Arctic)
            .markdown
            .heading_2
    );
}

#[test]
fn resolve_theme_selection_rejects_unknown_custom_base() {
    let custom: CustomThemeConfig = toml::from_str(r#"base = "missing""#).unwrap();
    let mut themes = BTreeMap::new();
    themes.insert("broken".to_string(), custom);

    let err = resolve_theme_selection("broken", &themes, None).unwrap_err();
    assert!(err.contains("Unknown base theme \"missing\""));
}

#[test]
fn validate_theme_syntax_warns_for_unknown_syntect_theme() {
    let custom: CustomThemeConfig = toml::from_str(r#"syntax = "missing-syntax-theme""#).unwrap();
    let mut themes = BTreeMap::new();
    themes.insert("custom".to_string(), custom);
    let selection = resolve_theme_selection("custom", &themes, None).unwrap();
    let theme_set = ThemeSet::load_defaults();

    let warning = validate_theme_syntax(&selection, &theme_set).unwrap();
    assert!(warning.contains("missing-syntax-theme"));
}

#[test]
fn set_theme_selection_applies_custom_theme() {
    let _guard = lock_theme_test_state();
    let original = current_theme_selection();
    let custom: CustomThemeConfig = toml::from_str(
        r##"[markdown]
text = "#010203"
"##,
    )
    .unwrap();
    let mut themes = BTreeMap::new();
    themes.insert("custom".to_string(), custom);
    let selection = resolve_theme_selection("custom", &themes, None).unwrap();

    set_theme_selection(selection);

    assert_eq!(app_theme().markdown.text, Color::Rgb(1, 2, 3));
    set_theme_selection(original);
}

#[test]
fn theme_presets_are_in_alphabetical_order() {
    let labels: Vec<_> = THEME_PRESETS
        .iter()
        .map(|preset| theme_preset_label(*preset))
        .collect();
    let mut sorted = labels.clone();
    sorted.sort();
    assert_eq!(labels, sorted);
}

#[test]
fn theme_picker_restores_original_preset_on_escape() {
    let _guard = lock_theme_test_state();
    let (ss, theme) = test_assets();
    let ts = ThemeSet::load_defaults();
    let (lines, toc, _, _) =
        parse_markdown("# Demo\n", &ss, &theme, &test_md_theme(), false, true).into();
    let mut app = App::new_with_source(
        lines,
        toc,
        AppConfig {
            filename: "stdin".to_string(),
            source: "# Demo\n".to_string(),
            debug_input: false,
            watch: false,
            filepath: None,
            last_file_state: None,
        },
    );

    let original = current_theme_selection();
    set_theme_preset(ThemePreset::OceanDark);
    app.open_theme_picker();
    assert!(app.set_theme_picker_index(theme_preset_index(ThemePreset::Forest)));
    app.preview_theme_preset(ThemePreset::Forest, &ss, &ts);

    assert_eq!(current_theme_preset(), ThemePreset::Forest);

    app.restore_theme_picker_preview(&ss, &ts);

    assert_eq!(current_theme_preset(), ThemePreset::OceanDark);
    assert!(!app.is_theme_picker_open());
    assert_eq!(app.theme_picker_original(), None);
    set_theme_selection(original);
}

#[test]
fn theme_picker_caches_previewed_themes_for_reuse() {
    let _guard = lock_theme_test_state();
    let (ss, theme) = test_assets();
    let ts = ThemeSet::load_defaults();
    let (lines, toc, _, _) = parse_markdown(
        "# Demo\n\n```rs\nfn main() {}\n```\n",
        &ss,
        &theme,
        &test_md_theme(),
        false,
        true,
    )
    .into();
    let mut app = App::new_with_source(
        lines,
        toc,
        AppConfig {
            filename: "stdin".to_string(),
            source: "# Demo\n\n```rs\nfn main() {}\n```\n".to_string(),
            debug_input: false,
            watch: false,
            filepath: None,
            last_file_state: None,
        },
    );

    let original = current_theme_selection();
    set_theme_preset(ThemePreset::OceanDark);
    app.open_theme_picker();
    app.preview_theme_preset(ThemePreset::Forest, &ss, &ts);

    assert!(app.has_cached_theme_preview(ThemePreset::Forest));
    assert_eq!(current_theme_preset(), ThemePreset::Forest);

    app.preview_theme_preset(ThemePreset::OceanDark, &ss, &ts);
    assert_eq!(current_theme_preset(), ThemePreset::OceanDark);
    assert!(app.has_cached_theme_preview(ThemePreset::OceanDark));
    set_theme_selection(original);
}

#[test]
fn theme_picker_restores_custom_theme_on_escape() {
    let _guard = lock_theme_test_state();
    let original = current_theme_selection();
    let custom: CustomThemeConfig = toml::from_str(
        r##"
base = "ocean"

[markdown]
heading_1 = "#010203"
"##,
    )
    .unwrap();
    let mut themes = BTreeMap::new();
    themes.insert("custom".to_string(), custom);
    let custom_selection = resolve_theme_selection("custom", &themes, None).unwrap();
    set_theme_selection(custom_selection.clone());

    let ss = syntect::parsing::SyntaxSet::load_defaults_newlines();
    let ts = ThemeSet::load_defaults();
    let theme = current_syntect_theme(&ts).clone();
    let (lines, toc, _, _) =
        parse_markdown("# Demo\n", &ss, &theme, &test_md_theme(), false, true).into();
    let mut app = App::new_with_source(
        lines,
        toc,
        AppConfig {
            filename: "stdin".to_string(),
            source: "# Demo\n".to_string(),
            debug_input: false,
            watch: false,
            filepath: None,
            last_file_state: None,
        },
    );

    app.open_theme_picker();
    app.preview_theme_preset(ThemePreset::Forest, &ss, &ts);
    assert_eq!(
        current_theme_selection().as_preset(),
        Some(ThemePreset::Forest)
    );

    app.restore_theme_picker_preview(&ss, &ts);

    assert_eq!(current_theme_selection(), custom_selection);
    assert_eq!(app_theme().markdown.heading_1, Color::Rgb(1, 2, 3));
    set_theme_selection(original);
}