use std::io::Write as _;
use crossterm::event::{KeyCode, KeyModifiers};
use super::{FixtureBackend, TerminalTheme, TestApp, account};
use crate::app::LoadPhase;
use crate::model::MailboxKind;
use crate::ui;
const WIDTH: u16 = 100;
const HEIGHT: u16 = 32;
fn assert_svg(name: &str, app: &mut TestApp) {
for theme in TerminalTheme::ALL {
let svg = app.svg(theme);
write_svg(name, theme, svg);
}
}
fn assert_mono_svg(name: &str, app: &mut TestApp) {
for theme in TerminalTheme::ALL {
let svg = app.svg_in_palette(ui::Theme::Mono, theme);
write_svg(name, theme, svg);
}
}
fn write_svg(name: &str, theme: TerminalTheme, svg: String) {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_omit_expression(true);
settings.bind(|| {
insta::assert_binary_snapshot!(
format!("{name}-{}.svg", theme.name()).as_str(),
svg.into_bytes()
);
});
}
fn inbox() -> TestApp {
TestApp::inbox(WIDTH, HEIGHT)
}
fn message_view() -> TestApp {
let mut app = inbox();
app.key(KeyCode::Enter);
app
}
fn assert_on_screen(app: &TestApp, needle: &str) {
assert!(
app.buffer_lines().iter().any(|line| line.contains(needle)),
"{needle:?} is not on screen:\n{}",
app.buffer_lines().join("\n")
);
}
#[test]
fn message_list() {
let mut app = inbox();
assert_on_screen(&app, "Invoice 2026-0342 for February");
assert_svg("message_list", &mut app);
}
#[test]
fn message_list_scrolls_with_the_selection() {
let mut app = TestApp::inbox(WIDTH, 12);
assert_svg("message_list_scrolled_to_the_newest", &mut app);
app.key(KeyCode::Home);
assert_on_screen(&app, "2025]");
assert_svg("message_list_scrolled_to_the_oldest", &mut app);
}
#[test]
fn message_list_with_scheduled_actions() {
let mut app = inbox();
app.key(KeyCode::Home);
app.char('d'); app.char('y'); app.char('!'); assert_on_screen(&app, "3 scheduled actions");
assert_svg("message_list_with_scheduled_actions", &mut app);
}
#[test]
fn message_list_while_backfilling() {
let mut app = TestApp::new(
WIDTH,
HEIGHT,
vec![account(
"Personal",
FixtureBackend::backfilling(MailboxKind::Inbox, 18),
)],
);
assert_on_screen(&app, "Loading message...");
assert_svg("message_list_while_backfilling", &mut app);
}
#[test]
fn message_list_empty_mailbox() {
let mut app = TestApp::new(
WIDTH,
HEIGHT,
vec![account("Personal", FixtureBackend::empty())],
);
assert_svg("message_list_empty_mailbox", &mut app);
}
#[test]
fn message_list_sent_shows_recipients() {
let mut app = inbox();
app.char('g');
app.char('t');
assert_on_screen(&app, "Sent");
assert_svg("message_list_sent_shows_recipients", &mut app);
}
#[test]
fn message_view_formatted() {
let mut app = message_view();
assert_on_screen(&app, "Invoice 2026-0342");
assert_svg("message_view_formatted", &mut app);
}
#[test]
fn message_view_raw_html() {
let mut app = message_view();
app.char('.');
assert_on_screen(&app, "Showing raw HTML");
assert_svg("message_view_raw_html", &mut app);
}
#[test]
fn message_view_scrolled() {
let mut app = message_view();
app.key(KeyCode::PageDown);
app.key(KeyCode::PageDown);
assert_svg("message_view_scrolled", &mut app);
}
#[test]
fn message_view_body_loading() {
let mut app = inbox();
app.key_without_settling(KeyCode::Enter);
assert_on_screen(&app, "Loading 'Invoice 2026-0342");
assert_svg("message_view_body_loading", &mut app);
app.settle();
}
#[test]
fn message_view_without_attachments() {
let mut app = inbox();
app.key(KeyCode::Up);
app.key(KeyCode::Enter);
assert_on_screen(&app, "Release notes for 0.3");
assert_svg("message_view_without_attachments", &mut app);
}
#[test]
fn message_view_body_load_failed() {
let mut app = message_view();
app.char('k');
app.char('k');
assert_on_screen(&app, "Failed to load message");
assert_svg("message_view_body_load_failed", &mut app);
}
#[test]
fn save_attachment_dialog() {
let mut app = message_view();
app.key_with(KeyCode::Char('S'), KeyModifiers::SHIFT);
assert_on_screen(&app, "Save attachment");
app.key(KeyCode::Tab); app.clear_field();
app.type_text("/home/rob/Downloads");
assert_svg("save_attachment_dialog_folder_focused", &mut app);
app.key(KeyCode::Tab); assert_svg("save_attachment_dialog", &mut app);
}
#[test]
fn compose_blank() {
let mut app = inbox();
app.char('c');
assert_svg("compose_blank", &mut app);
}
#[test]
fn compose_reply() {
let mut app = inbox();
app.char('r');
assert_on_screen(&app, "billing@vendor.example");
assert_svg("compose_reply", &mut app);
}
#[test]
fn compose_reply_all() {
let mut app = inbox();
app.char('a');
assert_on_screen(&app, "Cc: rob@example.com, accounts@example.com");
assert_svg("compose_reply_all", &mut app);
}
#[test]
fn compose_forward() {
let mut app = inbox();
app.char('f');
assert_on_screen(&app, "invoice-2026-0342.pdf");
assert_svg("compose_forward", &mut app);
}
#[test]
fn compose_editing_a_draft() {
let mut app = inbox();
app.char('g');
app.char('d');
app.key(KeyCode::Enter);
assert_on_screen(&app, "Editing draft.");
assert_svg("compose_editing_a_draft", &mut app);
}
#[test]
fn compose_filled_in() {
let mut app = inbox();
app.char('c');
app.type_text("anna@example.com");
app.key(KeyCode::Tab); app.key(KeyCode::Tab); app.key(KeyCode::Tab); app.type_text("Thursday works");
assert_svg("compose_filled_in", &mut app);
}
fn focus_attach_button(app: &mut TestApp) {
for _ in 0..5 {
app.key(KeyCode::Tab); }
}
#[test]
fn compose_attachment_prompt() {
let mut app = inbox();
app.char('c');
focus_attach_button(&mut app);
app.key(KeyCode::Enter);
assert_on_screen(&app, "Path:");
assert_svg("compose_attachment_prompt", &mut app);
}
#[test]
fn compose_sending() {
let mut app = inbox();
app.char('c');
app.type_text("anna@example.com");
for _ in 0..9 {
app.key(KeyCode::Tab); }
app.key_without_settling(KeyCode::Enter);
assert_on_screen(&app, "please wait");
assert_svg("compose_sending", &mut app);
app.settle();
}
#[test]
fn compose_with_attachment() {
let dir = tempfile::tempdir().expect("temporary directory");
let path = dir.path().join("meeting-notes.txt");
let mut file = std::fs::File::create(&path).expect("create the attachment");
file.write_all(b"Agenda:\n- release 0.3\n- snapshot tests\n")
.expect("write the attachment");
let mut app = inbox();
app.char('c');
focus_attach_button(&mut app);
app.key(KeyCode::Enter);
app.type_text(path.to_str().expect("utf-8 path"));
app.key(KeyCode::Enter);
assert_on_screen(&app, "meeting-notes.txt");
assert_svg("compose_with_attachment", &mut app);
}
#[test]
fn search_input_opened() {
let mut app = inbox();
app.char('/');
assert_on_screen(&app, "Find:");
assert_svg("search_input_opened", &mut app);
}
#[test]
fn search_filters_while_typing() {
let mut app = inbox();
app.char('/');
app.type_text("example.org");
assert_svg("search_filters_while_typing", &mut app);
}
#[test]
fn search_results_confirmed() {
let mut app = inbox();
app.char('/');
app.type_text("invoice");
app.key(KeyCode::Enter);
assert_on_screen(&app, "Showing results for:");
assert_svg("search_results_confirmed", &mut app);
}
#[test]
fn search_without_matches() {
let mut app = inbox();
app.char('/');
app.type_text("nothing matches this");
app.key(KeyCode::Enter);
assert_svg("search_without_matches", &mut app);
}
#[test]
fn loading_overlay_connecting() {
let (backend, gate) = FixtureBackend::blocking();
let mut app = TestApp::starting(WIDTH, HEIGHT, vec![account("Personal", backend)]);
assert_on_screen(&app, "Connecting to the mail server");
assert_svg("loading_overlay_connecting", &mut app);
gate.open();
app.settle();
}
#[test]
fn loading_overlay_receiving() {
let (backend, gate) = FixtureBackend::blocking();
let mut app = TestApp::starting(WIDTH, HEIGHT, vec![account("Personal", backend)]);
app.app_mut().set_load_phase(LoadPhase::Receiving {
loaded: 40,
total: 250,
});
app.draw();
assert_on_screen(&app, "40 of 250 headers");
assert_svg("loading_overlay_receiving", &mut app);
gate.open();
app.settle();
}
#[test]
fn loading_overlay_failed() {
let mut app = TestApp::starting(
WIDTH,
HEIGHT,
vec![account(
"Personal",
FixtureBackend::failing("login failed: the server rejected the app-specific password"),
)],
);
app.settle();
app.draw();
assert_on_screen(&app, "Could not open this mailbox");
assert_svg("loading_overlay_failed", &mut app);
}
#[test]
fn mailbox_chooser() {
let mut app = inbox();
app.char('g');
assert_on_screen(&app, "Go to");
assert_svg("mailbox_chooser", &mut app);
}
#[test]
fn account_chooser() {
let mut app = TestApp::new(
WIDTH,
HEIGHT,
vec![
account("Personal", FixtureBackend::new()),
account("Work", FixtureBackend::new()),
account("rob@fastmail.example", FixtureBackend::empty()),
],
);
app.char('G');
assert_on_screen(&app, "(current)");
assert_svg("account_chooser", &mut app);
}
#[test]
fn account_switched() {
let mut app = TestApp::new(
WIDTH,
HEIGHT,
vec![
account("Personal", FixtureBackend::new()),
account("Work", FixtureBackend::empty()),
],
);
app.char('G');
app.char('2');
assert_on_screen(&app, "Work");
assert_svg("account_switched", &mut app);
}
#[test]
fn mono_message_list() {
let mut app = inbox();
assert_mono_svg("mono_message_list", &mut app);
}
#[test]
fn mono_message_list_while_backfilling() {
let mut app = TestApp::new(
WIDTH,
HEIGHT,
vec![account(
"Personal",
FixtureBackend::backfilling(MailboxKind::Inbox, 18),
)],
);
assert_on_screen(&app, "Loading message...");
assert_mono_svg("mono_message_list_while_backfilling", &mut app);
}
#[test]
fn mono_compose_under_a_prompt() {
let mut app = inbox();
app.char('c');
focus_attach_button(&mut app);
app.key(KeyCode::Enter);
assert_on_screen(&app, "Path:");
assert_mono_svg("mono_compose_under_a_prompt", &mut app);
}
#[test]
fn mono_search_results() {
let mut app = inbox();
app.char('/');
app.type_text("invoice");
app.key(KeyCode::Enter);
assert_on_screen(&app, "Showing results for:");
assert_mono_svg("mono_search_results", &mut app);
}
#[test]
fn mono_save_attachment_dialog() {
let mut app = message_view();
app.key_with(KeyCode::Char('S'), KeyModifiers::SHIFT);
app.key(KeyCode::Tab); app.clear_field();
app.type_text("/home/rob/Downloads");
assert_mono_svg("mono_save_attachment_dialog_folder_focused", &mut app);
app.key(KeyCode::Tab); assert_mono_svg("mono_save_attachment_dialog", &mut app);
}
#[test]
fn nothing_in_the_monochrome_theme_names_a_colour() {
let mut app = inbox();
assert_colourless(&mut app, "the message list");
app.char('g');
assert_colourless(&mut app, "the mailbox chooser");
app.key(KeyCode::Esc);
app.char('/');
app.type_text("invoice");
assert_colourless(&mut app, "the search prompt");
app.key(KeyCode::Enter);
assert_colourless(&mut app, "a filtered list");
app.key(KeyCode::Esc);
app.char('c');
focus_attach_button(&mut app);
app.key(KeyCode::Enter);
assert_colourless(&mut app, "the composer under a prompt");
app.key(KeyCode::Esc);
app.key(KeyCode::Esc);
let mut app = message_view();
assert_colourless(&mut app, "the message viewer");
app.key_with(KeyCode::Char('S'), KeyModifiers::SHIFT);
assert_colourless(&mut app, "the save dialog");
let mut app = TestApp::new(
WIDTH,
HEIGHT,
vec![account(
"Personal",
FixtureBackend::failing("connection refused"),
)],
);
assert_colourless(&mut app, "a failed load");
let mut app = TestApp::new(
WIDTH,
HEIGHT,
vec![account(
"Personal",
FixtureBackend::backfilling(MailboxKind::Inbox, 18),
)],
);
assert_colourless(&mut app, "a mailbox still arriving");
}
fn assert_colourless(app: &mut TestApp, what: &str) {
use ratatui::style::Color;
app.draw_in(ui::Theme::Mono);
for cell in app.buffer().content() {
for (role, colour) in [("foreground", cell.fg), ("background", cell.bg)] {
assert_eq!(
colour,
Color::Reset,
"{what}: {:?} asks for a {role} of {colour:?}",
cell.symbol()
);
}
}
}