annatomic 0.4.0

The Annatomic annotation editor is intended to be used for the [RIDGES corpus](https://www.linguistik.hu-berlin.de/en/institut-en/professuren-en/korpuslinguistik/research/ridges-projekt). It is based on [graphANNIS](https://github.com/korpling/graphANNIS) and thus is internal data model is in principle suitable for a wide range of annotation concepts. "
Documentation
use std::{io::Read, path::PathBuf};

use anyhow::Context;
use egui::{Id, accesskit::Role, mutex::RwLock};
use egui_kittest::{
    Harness,
    kittest::{NodeT, Queryable},
};
use graphannis::model::AnnotationComponentType;
use tempfile::{TempDir, env};

use super::*;

/// 30 Seconds, since th tests are run with 4fps
pub const MAX_WAIT_STEPS: usize = 120;

pub(crate) fn create_app_with_corpus<R: Read, S: Into<String>>(
    corpus_name: S,
    graphml: R,
) -> crate::AnnatomicApp {
    // Load the graphml into a temporary folder
    let (mut graph, _config) = graphannis_core::graph::serialization::graphml::import::<
        AnnotationComponentType,
        _,
        _,
    >(graphml, false, |_| {})
    .unwrap();
    let graph_dir = TempDir::new().unwrap();
    graph.persist_to(graph_dir.path()).unwrap();

    let mut app_state = crate::AnnatomicApp::default();
    app_state
        .project
        .corpus_locations
        .insert(corpus_name.into(), graph_dir.keep());
    app_state
}

/// Delete the temporary project files from this app
pub(crate) fn cleanup_test_project(app: Arc<RwLock<crate::AnnatomicApp>>) {
    let mut app = app.write();
    let default_temp_dir = env::temp_dir();
    for (_name, location) in app.project.corpus_locations.iter() {
        if location.ancestors().any(|a| a == default_temp_dir) {
            std::fs::remove_dir_all(location).unwrap();
        }
    }
    app.project.corpus_locations.clear();
}

pub(crate) fn create_test_harness(
    app_state: crate::AnnatomicApp,
) -> (Harness<'static>, Arc<RwLock<crate::AnnatomicApp>>) {
    let app_state = Arc::new(RwLock::new(app_state));
    let result_app_state = app_state.clone();
    let app = move |ctx: &egui::Context| {
        let frame_info = IntegrationInfo {
            cpu_usage: Some(3.14),
        };
        let mut app_state = app_state.write();
        set_fonts(ctx);
        app_state.show(ctx, &frame_info);
    };

    let harness = Harness::builder()
        .with_size(egui::Vec2::new(800.0, 600.0))
        .with_max_steps(24)
        .build(app);

    (harness, result_app_state.clone())
}

pub(crate) fn create_test_harness_with_document_editor(
    app: crate::AnnatomicApp,
    corpus: &str,
    document_name: &str,
) -> (Harness<'static>, Arc<RwLock<crate::AnnatomicApp>>) {
    let (mut harness, app) = create_test_harness(app);
    open_corpus_structure(corpus, &mut harness, app.clone());
    harness.get_by_label(document_name).click();
    harness.run();
    harness.get_by_label(OPEN_LABEL.as_str()).click();
    wait_for_editor(&mut harness, app.clone());
    (harness, app)
}

/// Execute the actions to open the corpus structure editor for a corpus.
pub(crate) fn open_corpus_structure(
    corpus: &str,
    harness: &mut Harness<'static>,
    app_state: Arc<RwLock<crate::AnnatomicApp>>,
) {
    harness.get_by_label(corpus).click();
    wait_until_jobs_finished(harness, app_state.clone());
    harness.get_by_label(OPEN_LABEL.as_str()).click();
    wait_for_editor(harness, app_state);
}

/// Wait until editor has been created
pub(crate) fn wait_for_editor(
    harness: &mut Harness<'static>,
    app_state: Arc<RwLock<crate::AnnatomicApp>>,
) {
    for i in 0..MAX_WAIT_STEPS {
        harness.step();
        let app_state = app_state.read();
        if i > 3 && app_state.current_editor.get().is_some() {
            break;
        }
    }

    wait_until_jobs_finished(harness, app_state);
}

pub(crate) fn focus_and_wait(harness: &mut Harness<'static>, id: Id) {
    harness.get_by(|n| n.id().0 == id.value()).focus();
    for i in 0..MAX_WAIT_STEPS {
        harness.step();
        if i > 3 && harness.get_by(|n| n.id().0 == id.value()).is_focused() {
            break;
        }
    }
    harness.step();
}

pub(crate) fn focus_wait_and_type(harness: &mut Harness<'static>, id: Id, text: &str) {
    focus_and_wait(harness, id);
    let text_value = harness
        .get_all_by_role(Role::TextInput)
        .filter(|t| t.accesskit_node().id().0 == id.value())
        .next()
        .unwrap();
    text_value.type_text(text);
    harness.step();
}

pub(crate) fn wait_for_editor_vanished(
    harness: &mut Harness<'static>,
    app_state: Arc<RwLock<crate::AnnatomicApp>>,
) {
    for i in 0..MAX_WAIT_STEPS {
        harness.step();
        let app_state = app_state.read();
        if i > 3 && app_state.current_editor.get().is_none() {
            break;
        }
    }

    wait_until_jobs_finished(harness, app_state);
}

pub(crate) fn wait_until_jobs_finished(
    harness: &mut Harness<'static>,
    app_state: Arc<RwLock<crate::AnnatomicApp>>,
) {
    harness.run_steps(3);
    let mut steps_without_jobs = 0;
    for _ in 0..MAX_WAIT_STEPS {
        harness.step();
        let app_state = app_state.read();
        if !app_state.jobs.has_active_jobs() {
            steps_without_jobs += 1;
        } else {
            steps_without_jobs = 0;
        }

        if steps_without_jobs > 10 {
            break;
        }
    }
    // Jobs can create notifications wait until these are finished, too
    wait_until_notifications_finished(harness, app_state);
}

pub(crate) fn wait_until_notifications_finished(
    harness: &mut Harness<'static>,
    app_state: Arc<RwLock<crate::AnnatomicApp>>,
) {
    let mut steps_without_notification = 0;
    for _ in 0..MAX_WAIT_STEPS {
        harness.step();
        let app_state = app_state.read();
        if !app_state.notifier.is_empty() {
            steps_without_notification += 1;
        } else {
            steps_without_notification = 0;
        }

        if steps_without_notification > 10 {
            break;
        }
    }
    harness.step();
}

pub(crate) fn get_text_input<'a>(
    harness: &'a Harness<'_>,
    value: &'a str,
) -> egui_kittest::Node<'a> {
    harness
        .get_all_by_value(value)
        .filter(|n| n.accesskit_node().role() == Role::TextInput)
        .next()
        .context(format!("Missing text input with value \"{value}\""))
        .unwrap()
}

#[macro_export]
macro_rules! assert_screenshots {
    ($($x:expr),* ) => {
        $(
            match $x {
                Ok(_) => {}
                Err(err) => {
                    panic!("{}", err);
                }
            }
        )*
    };
}

#[test]
fn show_main_page() {
    let mut app_state = crate::AnnatomicApp::default();

    app_state
        .project
        .corpus_locations
        .insert("single_sentence".to_string(), PathBuf::default());
    app_state
        .project
        .corpus_locations
        .insert("test".to_string(), PathBuf::default());

    let (mut harness, _) = create_test_harness(app_state);
    harness.run();

    harness.snapshot("show_main_page");
}