quelch 0.5.0

Ingest data from Jira, Confluence, and more directly into Azure AI Search
Documentation
#[cfg(test)]
mod tests {
    use ratatui::{Terminal, backend::TestBackend};

    use crate::config::{
        AuthConfig, AzureConfig, Config, JiraSourceConfig, SourceConfig, SyncConfig,
    };
    use crate::tui::app::App;
    use crate::tui::prefs::Prefs;
    use crate::tui::widgets::source_table::SourceTable;

    fn cfg() -> Config {
        Config {
            azure: AzureConfig {
                endpoint: "x".into(),
                api_key: "k".into(),
            },
            sources: vec![SourceConfig::Jira(JiraSourceConfig {
                name: "my-jira".into(),
                url: "x".into(),
                auth: AuthConfig::DataCenter { pat: "p".into() },
                projects: vec!["DO".into(), "HR".into()],
                index: "i".into(),
            })],
            sync: SyncConfig::default(),
        }
    }

    fn rendered_text(app: &App, w: u16, h: u16) -> String {
        let backend = TestBackend::new(w, h);
        let mut term = Terminal::new(backend).unwrap();
        term.draw(|f| {
            f.render_widget(SourceTable { app }, f.area());
        })
        .unwrap();
        let buf = term.backend().buffer();
        (0..buf.area.height)
            .map(|y| {
                (0..buf.area.width)
                    .map(|x| buf[(x, y)].symbol())
                    .collect::<String>()
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    #[test]
    fn renders_column_headings() {
        let app = App::new(&cfg(), Prefs::default());
        let text = rendered_text(&app, 100, 10);
        assert!(text.contains("Source"), "missing Source heading:\n{text}");
        assert!(text.contains("Status"), "missing Status heading");
        assert!(text.contains("Items"), "missing Items heading");
        assert!(text.contains("Rate"), "missing Rate heading");
        assert!(text.contains("Last item"), "missing Last item heading");
        assert!(text.contains("Updated"), "missing Updated heading");
    }

    #[test]
    fn renders_source_row_and_expanded_subsources() {
        let app = App::new(&cfg(), Prefs::default());
        let text = rendered_text(&app, 100, 10);
        assert!(text.contains("my-jira"));
        assert!(text.contains("DO"));
        assert!(text.contains("HR"));
    }

    #[test]
    fn collapsed_source_hides_subsources() {
        let mut app = App::new(&cfg(), Prefs::default());
        app.prefs.toggle_source_collapsed("my-jira");
        let text = rendered_text(&app, 100, 10);
        assert!(text.contains("my-jira"));
        assert!(!text.contains("  DO"));
        assert!(!text.contains("  HR"));
    }

    use crate::tui::widgets::azure_panel::AzurePanelWidget;

    #[test]
    fn azure_panel_shows_plain_english_labels() {
        let app = App::new(&cfg(), Prefs::default());
        let mut term = Terminal::new(TestBackend::new(100, 12)).unwrap();
        term.draw(|f| {
            f.render_widget(
                AzurePanelWidget {
                    panel: &app.azure,
                    drops: 0,
                    focused: false,
                    backoff_reason: None,
                },
                f.area(),
            );
        })
        .unwrap();
        let buf = term.backend().buffer();
        let text: String = (0..buf.area.height)
            .flat_map(|y| (0..buf.area.width).map(move |x| buf[(x, y)].symbol().to_string()))
            .collect::<Vec<_>>()
            .join("");
        assert!(text.contains("Total requests"), "rendered: {text}");
        assert!(text.contains("median"), "rendered: {text}");
        assert!(text.contains("Failed (4xx)"));
        assert!(text.contains("Failed (5xx)"));
        assert!(text.contains("Throttled"));
    }

    use crate::tui::widgets::drilldown::Drilldown;

    #[test]
    fn drilldown_shows_subsource_details() {
        let mut app = App::new(&cfg(), Prefs::default());
        // Populate a subsource with some data
        app.apply(crate::tui::events::QuelchEvent::SubsourceBatch {
            source: "my-jira".into(),
            subsource: "DO".into(),
            fetched: 5,
            cursor: chrono::Utc::now(),
            sample_id: "DO-42".into(),
        });
        for i in 0..3 {
            app.apply(crate::tui::events::QuelchEvent::DocSynced {
                source: "my-jira".into(),
                subsource: "DO".into(),
                id: format!("DO-{i}"),
                updated: chrono::Utc::now(),
            });
        }
        app.move_selection_down(); // focus DO

        let mut term = Terminal::new(TestBackend::new(60, 20)).unwrap();
        term.draw(|f| {
            f.render_widget(Drilldown { app: &app }, f.area());
        })
        .unwrap();
        let buf = term.backend().buffer();
        let text: String = (0..buf.area.height)
            .flat_map(|y| (0..buf.area.width).map(move |x| buf[(x, y)].symbol().to_string()))
            .collect::<Vec<_>>()
            .join("");
        assert!(text.contains("Docs synced"));
        assert!(text.contains("Recent"));
        assert!(text.contains("DO-2"));
    }

    use crate::tui::widgets::help_overlay::HelpOverlay;

    #[test]
    fn help_overlay_lists_key_bindings() {
        let mut term = Terminal::new(TestBackend::new(70, 30)).unwrap();
        term.draw(|f| {
            f.render_widget(HelpOverlay {}, f.area());
        })
        .unwrap();
        let buf = term.backend().buffer();
        let text: String = (0..buf.area.height)
            .flat_map(|y| (0..buf.area.width).map(move |x| buf[(x, y)].symbol().to_string()))
            .collect::<Vec<_>>()
            .join("");
        assert!(text.contains("Keyboard shortcuts"));
        assert!(text.contains("sync now"));
        assert!(text.contains("pause"));
        assert!(text.contains("quit"));
    }

    use crate::tui::app::LogLine;
    use crate::tui::widgets::log_view::LogView;
    use std::collections::VecDeque;

    #[test]
    fn log_view_renders_column_headings() {
        let mut lines = VecDeque::new();
        lines.push_back(LogLine {
            ts: chrono::Utc::now(),
            level: tracing::Level::INFO,
            target: "quelch::sync".into(),
            message: "Cycle starting".into(),
        });
        let view = LogView {
            lines: &lines,
            focused: false,
        };
        let mut term = Terminal::new(TestBackend::new(100, 10)).unwrap();
        term.draw(|f| f.render_widget(view, f.area())).unwrap();
        let buf = term.backend().buffer();
        let text: String = (0..buf.area.height)
            .flat_map(|y| (0..buf.area.width).map(move |x| buf[(x, y)].symbol().to_string()))
            .collect::<Vec<_>>()
            .join("");
        assert!(text.contains("LEVEL"));
        assert!(text.contains("TIME"));
        assert!(text.contains("TARGET"));
        assert!(text.contains("MESSAGE"));
        assert!(text.contains("Cycle starting"));
    }
}