kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use super::{HelpDialog, HelpNode};
use crate::i18n::Lang;
use iced::Point;
use iced::widget::text_editor;
use std::time::{Duration, Instant};

#[test]
fn search_input_updates_text_before_results() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.update_search_input("эмулятор".to_owned(), Lang::Ru);

    assert_eq!(dialog.search, "эмулятор");
    assert_eq!(dialog.results_query(), "");
    assert!(dialog.search_results().is_empty());
    assert!(dialog.node_matches_search(HelpNode::TopicAbout, Lang::Ru));
}

#[test]
fn clearing_search_clears_cached_results() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.update_search_input("эмулятор".to_owned(), Lang::Ru);
    apply_due_search(&mut dialog, Lang::Ru);
    dialog.update_search_input(String::new(), Lang::Ru);

    assert_eq!(dialog.results_query(), "");
    assert!(dialog.node_matches_search(HelpNode::TopicAbout, Lang::Ru));
}

#[test]
fn search_results_use_bounded_previews() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.update_search_input("команда".to_owned(), Lang::Ru);
    apply_due_search(&mut dialog, Lang::Ru);

    assert!(!dialog.search_results().is_empty());
    assert!(
        dialog
            .search_results()
            .iter()
            .all(|result| result.preview_lines().len() <= 4)
    );
}

#[test]
fn search_result_preview_keeps_matching_line_without_full_article() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.update_search_input("NOP".to_owned(), Lang::Ru);
    apply_due_search(&mut dialog, Lang::Ru);
    let result = dialog
        .search_results()
        .iter()
        .find(|result| result.node() == HelpNode::TopicProcessorControlCommands)
        .expect("processor control commands should match NOP");

    let preview = result.preview_lines().join("\n");
    assert!(preview.contains("NOP"));
    assert!(result.preview_lines().len() < 6);
}

#[test]
fn article_click_keeps_empty_selection_anchor() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.perform_text_action(text_editor::Action::Click(Point::new(32.0, 20.0)));
    let cursor = dialog.article_content.cursor();

    assert_eq!(cursor.selection, Some(cursor.position));
    assert_eq!(dialog.article_content.selection(), Some(String::new()));
}

#[test]
fn article_noop_drag_after_click_keeps_empty_selection_anchor() {
    let mut dialog = HelpDialog::new(Lang::Ru);
    let point = Point::new(32.0, 20.0);

    dialog.perform_text_action(text_editor::Action::Click(point));
    dialog.perform_text_action(text_editor::Action::Drag(point));
    let cursor = dialog.article_content.cursor();

    assert_eq!(cursor.selection, Some(cursor.position));
    assert_eq!(dialog.article_content.selection(), Some(String::new()));
}

#[test]
fn article_text_actions_select_without_editing() {
    let mut dialog = HelpDialog::new(Lang::Ru);
    let before = dialog.article_content.text();

    dialog.perform_text_action(text_editor::Action::SelectAll);
    dialog.perform_text_action(text_editor::Action::Edit(text_editor::Edit::Delete));

    assert_eq!(dialog.article_content.text(), before);
    assert_eq!(dialog.article_content.selection(), Some(before));
}

#[test]
fn search_request_waits_for_debounce_deadline() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.update_search_input("команда".to_owned(), Lang::Ru);

    assert!(
        dialog
            .take_due_search_request(Lang::Ru, Instant::now())
            .is_none()
    );
}

#[test]
fn stale_search_response_does_not_replace_newer_input() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.update_search_input("эмулятор".to_owned(), Lang::Ru);
    let stale = due_search_response(&mut dialog, Lang::Ru);
    dialog.update_search_input("NOP".to_owned(), Lang::Ru);
    dialog.apply_search_response(stale, Lang::Ru);

    assert_eq!(dialog.search, "NOP");
    assert_eq!(dialog.results_query(), "");

    apply_due_search(&mut dialog, Lang::Ru);

    assert_eq!(dialog.results_query(), "NOP");
    assert!(
        dialog
            .search_results()
            .iter()
            .any(|result| result.node() == HelpNode::TopicProcessorControlCommands)
    );
}

#[test]
fn selecting_subtopic_reveals_parent_category() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.select_node(HelpNode::TopicMenuHelp, Lang::Ru);

    assert_eq!(dialog.selected, HelpNode::TopicMenuHelp);
    assert!(dialog.expanded.contains(&HelpNode::CatMainMenu));
}

#[test]
fn selecting_subtopic_keeps_existing_expansion() {
    let mut dialog = HelpDialog::new(Lang::Ru);
    dialog.expanded.insert(HelpNode::CatSettings);

    dialog.select_node(HelpNode::TopicExport, Lang::Ru);

    assert!(dialog.expanded.contains(&HelpNode::CatSettings));
    assert!(dialog.expanded.contains(&HelpNode::CatFilesExport));
}

#[test]
fn article_content_renders_inline_bold_without_markers() {
    let mut dialog = HelpDialog::new(Lang::Ru);

    dialog.select_node(HelpNode::TopicProcessorControlCommands, Lang::Ru);

    assert!(!dialog.article_content.text().contains("**"));
    assert!(dialog.article_content.text().contains("NOP (00h)"));
}

fn apply_due_search(dialog: &mut HelpDialog, lang: Lang) {
    let response = due_search_response(dialog, lang);
    dialog.apply_search_response(response, lang);
}

fn due_search_response(dialog: &mut HelpDialog, lang: Lang) -> super::search::HelpSearchResponse {
    dialog
        .take_due_search_request(lang, Instant::now() + Duration::from_secs(1))
        .expect("search request should be due")
        .resolve()
}