use std::fmt;
use std::path::Path;
use eframe::egui;
use crate::message::clean;
use super::app::central_panel;
use super::theme;
pub(crate) const ACCOUNTS_DOCS: &str = "https://postvak.pataar.nl/accounts.html";
#[derive(Debug)]
pub(crate) struct NoAccounts;
impl fmt::Display for NoAccounts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("no accounts configured; run `postvak account add`")
}
}
impl std::error::Error for NoAccounts {}
#[derive(Debug, PartialEq)]
pub(crate) struct Problem {
pub summary: String,
pub fix: String,
pub detail: Option<String>,
pub link: Option<(&'static str, &'static str)>,
}
impl Problem {
pub fn of(error: &anyhow::Error, log: Option<&Path>) -> Problem {
if error.is::<NoAccounts>() {
return Problem {
summary: "Postvak has no mail account set up yet.".into(),
fix: "Run `postvak account add` in a terminal, then open Postvak again.".into(),
detail: None,
link: Some(("Setting up an account", ACCOUNTS_DOCS)),
};
}
let fix = match log {
Some(log) => format!("The background sync logs to {}", log.display()),
None => "Run `postvak gui` in a terminal to see more.".into(),
};
Problem {
summary: "Postvak could not start.".into(),
fix,
detail: Some(clean(&format!("{error:#}"), false)),
link: None,
}
}
}
pub(crate) fn show(problem: &Problem, ui: &mut egui::Ui) -> bool {
let mut close = false;
central_panel(ui, false).show(ui, |ui| {
ui.heading(&problem.summary);
ui.add_space(8.0);
ui.label(&problem.fix);
if let Some((label, url)) = problem.link {
ui.hyperlink_to(label, url);
}
if let Some(detail) = &problem.detail {
ui.add_space(8.0);
egui::ScrollArea::vertical()
.max_height(ui.available_height() - 40.0)
.show(ui, |ui| {
ui.colored_label(theme::palette(ui).error, detail);
});
}
ui.add_space(12.0);
close = ui.button("Close").clicked();
});
close
}
#[cfg(test)]
mod tests {
use std::path::Path;
use eframe::egui;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use super::*;
use crate::gui::Explain;
fn harness(problem: Problem) -> Harness<'static, Explain> {
Harness::builder()
.with_size(egui::vec2(520.0, 260.0))
.build_ui_state(
|ui, explain: &mut Explain| explain.show(ui),
Explain(problem),
)
}
#[test]
fn no_accounts_points_to_account_add_and_the_docs() {
let problem = Problem::of(&anyhow::Error::new(NoAccounts), Some(Path::new("/l")));
assert_eq!(problem.summary, "Postvak has no mail account set up yet.");
assert!(problem.fix.contains("`postvak account add`"));
assert_eq!(problem.link, Some(("Setting up an account", ACCOUNTS_DOCS)));
assert_eq!(problem.detail, None);
}
#[test]
fn no_accounts_is_found_under_context() {
let error = anyhow::Error::new(NoAccounts).context("opening the window");
let problem = Problem::of(&error, None);
assert_eq!(problem.summary, "Postvak has no mail account set up yet.");
}
#[test]
fn other_errors_show_their_text_and_the_daemon_log() {
let error = anyhow::anyhow!("socket refused").context("could not start the daemon");
let problem = Problem::of(&error, Some(Path::new("/state/daemon.log")));
assert_eq!(problem.summary, "Postvak could not start.");
assert_eq!(problem.fix, "The background sync logs to /state/daemon.log");
assert_eq!(
problem.detail.as_deref(),
Some("could not start the daemon: socket refused")
);
}
#[test]
fn without_paths_the_fix_is_to_run_it_in_a_terminal() {
let problem = Problem::of(&anyhow::anyhow!("no home directory found"), None);
assert_eq!(problem.fix, "Run `postvak gui` in a terminal to see more.");
assert_eq!(problem.detail.as_deref(), Some("no home directory found"));
assert_eq!(problem.link, None);
}
#[test]
fn the_window_states_the_missing_account() {
let mut harness = harness(Problem::of(&anyhow::Error::new(NoAccounts), None));
harness.run();
harness.get_by_label("Postvak has no mail account set up yet.");
harness.get_by_label_contains("Run `postvak account add` in a terminal");
harness.get_by_label("Close");
}
#[test]
fn the_docs_link_opens_the_account_setup_page() {
let mut harness = harness(Problem::of(&anyhow::Error::new(NoAccounts), None));
harness.run();
harness.get_by_label("Setting up an account").click();
harness.step();
let opened: Vec<_> = harness
.output()
.platform_output
.commands
.iter()
.filter_map(|command| match command {
egui::OutputCommand::OpenUrl(open) => Some(open.url.clone()),
_ => None,
})
.collect();
assert_eq!(opened, [ACCOUNTS_DOCS]);
}
#[test]
fn the_window_shows_the_error_and_the_daemon_log() {
let error = anyhow::anyhow!("could not start the daemon");
let mut harness = harness(Problem::of(&error, Some(Path::new("/state/daemon.log"))));
harness.run();
harness.get_by_label("Postvak could not start.");
harness.get_by_label("could not start the daemon");
harness.get_by_label("The background sync logs to /state/daemon.log");
}
#[test]
fn close_asks_the_window_to_close() {
let mut harness = harness(Problem::of(&anyhow::Error::new(NoAccounts), None));
harness.run();
let closes = |harness: &Harness<'_, Explain>| {
harness.output().viewport_output[&egui::ViewportId::ROOT]
.commands
.contains(&egui::ViewportCommand::Close)
};
assert!(!closes(&harness));
harness.get_by_label("Close").click();
harness.step();
assert!(closes(&harness));
}
}