clankerdiff-ratatui 0.1.7

Embeddable Ratatui diff review widget
Documentation
use clankerdiff_core::testing::DocumentBuilder;
use clankerdiff_markdown::MarkdownDocument;
use clankerdiff_ratatui::{
    DiffReviewState, DiffReviewWidget, InputOutcome, InteractionPhase, KeyCode, KeyEvent,
    KeyModifiers, MarkdownReviewState, ReviewCommand, ReviewInput, ThemeChoice,
};
use clankerdiff_theme::{ReviewTheme, ThemeId};
use ratatui::{
    buffer::{Buffer, Cell},
    layout::Rect,
    widgets::StatefulWidget,
};
use std::{error::Error, sync::Arc};

#[test]
fn custom_theme_choices_preserve_order_labels_and_commit_explicitly() {
    let original = ReviewTheme::builder("original").build();
    let choices = choices();
    let mut state = DiffReviewState::with_theme(
        DocumentBuilder::new().changed("a", "old", "new").build(),
        original.clone(),
    );
    assert!(state.theme_choices().is_empty());
    assert!(matches!(
        state.handle_command(ReviewCommand::OpenThemePicker),
        InputOutcome::Ignored
    ));
    state.set_theme_choices(choices.as_slice());
    assert_eq!(state.theme_choices()[0].name, "Zebra custom");
    state.handle_command(ReviewCommand::OpenThemePicker);
    let area = Rect::new(0, 0, 80, 20);
    let mut buffer = Buffer::empty(area);
    DiffReviewWidget::new().render(area, &mut buffer, &mut state);
    let text: String = buffer.content.iter().map(Cell::symbol).collect();
    assert!(text.find("Zebra custom") < text.find("Alpha custom"));
    assert!(text.contains("Zebra custom") && text.contains("Alpha custom"));
    assert!(!text.contains("Ayu"));
    assert!(matches!(
        state.handle_command(ReviewCommand::SelectTheme(99)),
        InputOutcome::Ignored
    ));
    assert_eq!(state.theme().id(), original.id());
    state.handle_command(ReviewCommand::SelectTheme(1));
    assert_eq!(state.theme().id(), choices[1].theme.id());
    assert_eq!(state.interaction_phase(), InteractionPhase::ThemePicker);
    state.handle_command(ReviewCommand::Cancel);
    assert_eq!(state.theme().id(), original.id());
    state.handle_command(ReviewCommand::OpenThemePicker);
    assert!(
        matches!(state.handle_command(ReviewCommand::CommitTheme), InputOutcome::ThemeSelected(ThemeId::Custom(id)) if id == "zebra")
    );
    assert_eq!(state.theme().id(), choices[0].theme.id());
    assert_eq!(state.interaction_phase(), InteractionPhase::Browse);
    state.handle_command(ReviewCommand::OpenThemePicker);
    let _ = state.handle_input(ReviewInput::Key(KeyEvent::new(
        KeyCode::Down,
        KeyModifiers::CONTROL,
    )));
    assert!(
        matches!(state.handle_command(ReviewCommand::CommitTheme), InputOutcome::ThemeSelected(ThemeId::Custom(id)) if id == "zebra")
    );
}

#[test]
fn replacing_choices_or_theme_cancels_an_active_preview() {
    let mut state = DiffReviewState::new(DocumentBuilder::new().build());
    let original = state.theme().id().clone();
    state.set_theme_choices(choices());
    state.handle_command(ReviewCommand::OpenThemePicker);
    state.handle_command(ReviewCommand::SelectTheme(1));
    state.set_theme_choices(Vec::new());
    assert_eq!(state.theme().id(), &original);
    assert_eq!(state.interaction_phase(), InteractionPhase::Browse);
    state.set_theme_choices(choices());
    state.handle_command(ReviewCommand::OpenThemePicker);
    let replacement = ReviewTheme::builder("host override").build();
    state.set_theme(replacement.clone());
    assert_eq!(state.interaction_phase(), InteractionPhase::Browse);
    assert_eq!(state.theme().id(), replacement.id());
}

#[test]
fn markdown_custom_themes_preview_cancel_and_emit_commit() -> Result<(), Box<dyn Error>> {
    let choices = choices();
    let mut state = MarkdownReviewState::with_theme(
        Arc::new(MarkdownDocument::parse("# Review")),
        choices[1].theme.clone(),
    );
    assert!(matches!(
        state.handle_command(ReviewCommand::OpenThemePicker)?,
        InputOutcome::Ignored
    ));
    state.set_theme_choices(choices.as_slice());
    state.handle_command(ReviewCommand::OpenThemePicker)?;
    assert!(matches!(
        state.handle_command(ReviewCommand::SelectTheme(100))?,
        InputOutcome::Ignored
    ));
    assert!(
        matches!(state.handle_command(ReviewCommand::CommitTheme)?, InputOutcome::ThemeSelected(ThemeId::Custom(id)) if id == "alpha")
    );
    state.handle_command(ReviewCommand::OpenThemePicker)?;
    state.handle_command(ReviewCommand::MoveTheme(isize::MIN))?;
    assert_eq!(state.theme().id(), choices[0].theme.id());
    assert_eq!(state.interaction_phase(), InteractionPhase::ThemePicker);
    state.handle_command(ReviewCommand::Cancel)?;
    assert_eq!(state.theme().id(), choices[1].theme.id());
    state.handle_command(ReviewCommand::OpenThemePicker)?;
    state.handle_command(ReviewCommand::SelectTheme(0))?;
    state.set_theme_choices(Vec::new());
    assert_eq!(state.theme().id(), choices[1].theme.id());
    assert_eq!(state.interaction_phase(), InteractionPhase::Browse);
    Ok(())
}

fn choices() -> Vec<ThemeChoice> {
    vec![
        ThemeChoice::new("Zebra custom", ReviewTheme::builder("zebra").build()),
        ThemeChoice::new("Alpha custom", ReviewTheme::builder("alpha").build()),
    ]
}