use eframe::egui;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use super::App;
use super::app::View;
use super::test_support::{Fixture, Wires, message};
use crate::store::{Folder, LogEntry, Message};
use crate::sync::{Activity, Event};
use crate::trash::Trash;
const AT: i64 = 1_790_000_000;
const RULES: &str = "[[rules]]\nname = \"newsletters\"\nmatch.from = { contains = \"news@\" }\nactions = [\"archive\"]\n\n[[rules]]\nname = \"receipts\"\naccount = \"work\"\nmatch.subject = { contains = \"receipt\" }\nactions = [{ move = \"Receipts\" }]\n\n[[rules]]\nname = \"codes\"\nenabled = false\nproposed_by = \"agent\"\nmatch.subject = { contains = \"verification code\" }\nactions = [\"delete\"]\n";
fn enabled() -> bool {
std::env::var_os("UPDATE_SNAPSHOTS").is_some()
|| std::env::var("POSTVAK_SNAPSHOTS").is_ok_and(|v| v == "1")
}
fn snapshot(harness: &mut Harness<'static, App>, name: &str) {
if !enabled() {
return;
}
let fixture = chrono::DateTime::from_timestamp(AT, 0).unwrap();
for (when, offset) in [
("now", chrono::Local::now().offset().local_minus_utc()),
(
"at the fixture time",
fixture
.with_timezone(&chrono::Local)
.offset()
.local_minus_utc(),
),
] {
assert_eq!(
offset, 0,
"snapshots show local times, and the local offset {when} is not zero; run them with TZ=UTC"
);
}
harness.snapshot(name);
}
fn mail(uid: u32, from: &str, subject: &str, flags: &str, body: &str) -> Message {
let mut m = message("INBOX", uid, subject);
let at = AT - i64::from(100 - uid) * 3_600;
m.from_addr = Some(from.into());
m.to_addr = Some("Robin Example <robin@example.com>".into());
m.flags = flags.into();
m.body_text = Some(body.into());
m.date = Some(at);
m.internaldate = at;
m
}
fn mailbox(theme: &str) -> Fixture {
let fx = Fixture::new(&["home", "work"]);
fx.append_config(&format!("[ui]\ntheme = \"{theme}\"\n"));
for (name, special) in [
("Archive", Some("Archive")),
("Sent", Some("Sent")),
("Trash", Some("Trash")),
("Receipts", None),
] {
fx.folder("work", name, special);
}
fx.folder("home", "Archive", Some("Archive"));
for name in ["Projects/Postvak", "Projects/Website"] {
let nested = Folder {
name: name.into(),
uidvalidity: 1,
last_uid: 0,
special_use: None,
delimiter: Some("/".into()),
};
fx.store("work").upsert_folder(&nested).unwrap();
}
let lunch = "Hi Robin,\n\nShall we try the new place on the corner this Friday at noon?\nThe menu is at https://example.com/menu and they take bookings.\n\nCheers,\nLinus";
let inbox = [
(
90,
"Ada Lovelace <ada@example.com>",
"Notes on the analytical engine",
"\\Seen",
"",
),
(
91,
"Example Shop <shop@example.com>",
"Your receipt for order 1042",
"\\Seen",
"",
),
(
92,
"Weekly News <news@example.org>",
"This week: ten things about tea",
"",
"1. Green tea\n2. Black tea\n3. Mint tea",
),
(
93,
"Grace Hopper <grace@example.com>",
"Planning the offsite",
"\\Seen",
"Shall we meet in Utrecht on the 14th?",
),
(
94,
"Alan Turing <alan@example.com>",
"Re: Planning the offsite",
"\\Seen \\Flagged",
"The 14th works for me.",
),
(
95,
"Linus Example <linus@example.com>",
"Lunch on Friday?",
"",
lunch,
),
];
for (uid, from, subject, flags, body) in inbox {
let mut m = mail(uid, from, subject, flags, body);
if uid == 93 || uid == 94 {
m.thread_id = "<offsite@example.com>".into();
}
fx.add("work", m);
}
let photos = mail(
96,
"Mum <mum@example.net>",
"Photos from the weekend",
"",
"",
);
fx.add("home", photos);
fx
}
fn open(fx: &Fixture) -> (Harness<'static, App>, Wires) {
let (mut harness, wires) = fx.harness();
let events = [
Event::Activity {
account: "home".into(),
activity: Activity::Offline {
reason: "no network".into(),
retry_at: AT + 300,
},
},
Event::Activity {
account: "work".into(),
activity: Activity::Idle { since: AT },
},
];
for event in events {
wires.events.send(event).unwrap();
}
harness.run();
(harness, wires)
}
fn inbox(theme: &str) -> (Fixture, Harness<'static, App>, Wires) {
let fx = mailbox(theme);
let (mut harness, wires) = open(&fx);
harness.state_mut().select_view(View::Folder {
account: 1,
folder: "INBOX".into(),
});
harness.run();
assert!(harness.query_by_label_contains("Shall we try").is_some());
(fx, harness, wires)
}
fn press(harness: &mut Harness<'static, App>, text: &str) {
harness.event(egui::Event::Text(text.into()));
harness.run();
}
#[test]
fn inbox_light() {
let (_fx, mut harness, _wires) = inbox("light");
snapshot(&mut harness, "gui_inbox_light");
}
#[test]
fn inbox_dark() {
let (_fx, mut harness, _wires) = inbox("dark");
snapshot(&mut harness, "gui_inbox_dark");
}
#[test]
fn inbox_with_marks_and_an_expanded_thread() {
let (_fx, mut harness, _wires) = inbox("light");
for key in ["x", "j", "x"] {
press(&mut harness, key);
}
harness.key_press(egui::Key::ArrowRight);
harness.run();
let list = &harness.state().list;
for uid in [95, 94] {
assert!(
list.marked.contains(&("INBOX".to_string(), uid)),
"{uid} not marked"
);
}
for uid in [94, 93] {
assert!(
list.rows.iter().any(|row| row.member && row.uid == uid),
"thread member {uid} not shown"
);
}
snapshot(&mut harness, "gui_inbox_thread");
}
#[test]
fn keys_help() {
let (_fx, mut harness, _wires) = inbox("light");
press(&mut harness, "?");
assert!(harness.state().help_open);
snapshot(&mut harness, "gui_keys_help");
}
#[test]
fn rules() {
let fx = mailbox("light");
std::fs::write(fx.paths.rules_file(), RULES).unwrap();
let (mut harness, _wires) = open(&fx);
harness.state_mut().select_view(View::Rules);
harness.run();
assert!(harness.query_by_label("Approve").is_some());
snapshot(&mut harness, "gui_rules");
}
#[test]
fn activity() {
let fx = mailbox("light");
for (minutes, rule, action, subject) in [
(
0,
"newsletters",
"archive",
"This week: ten things about tea",
),
(
5,
"receipts",
"move Receipts",
"Your receipt for order 1042",
),
(9, "gui", "flag", "Re: Planning the offsite"),
] {
let entry = LogEntry {
id: 0,
at: AT + minutes * 60,
rule_name: rule.into(),
folder: "INBOX".into(),
uid: 1,
message_id: None,
subject: Some(subject.into()),
action: action.into(),
trash_file: None,
};
fx.store("work").log_action(&entry).unwrap();
}
let (mut harness, _wires) = open(&fx);
harness.state_mut().select_view(View::Activity);
harness.run();
assert!(harness.query_by_label("move Receipts").is_some());
snapshot(&mut harness, "gui_activity");
}
#[test]
fn trash() {
let fx = mailbox("light");
let dir = fx.paths.trash_dir("work");
std::fs::create_dir_all(&dir).unwrap();
let raw = b"From: Example Shop <shop@example.com>\r\nSubject: Your verification code\r\n\r\n123456\r\n";
Trash::new(dir).save("INBOX", 7, raw, AT).unwrap();
let (mut harness, _wires) = open(&fx);
harness.state_mut().select_view(View::Trash);
harness.run();
assert!(harness.query_by_label("Restore").is_some());
snapshot(&mut harness, "gui_trash");
}
#[cfg(feature = "html")]
fn newsletter(theme: &str, width: f32) -> (Fixture, Harness<'static, App>, Wires) {
use super::html::view_tests::{NEWSLETTER, painted, raw, wait_for};
let fx = mailbox(theme);
let news = mail(
99,
"Postvak Weekly <news@example.org>",
"Your October roundup",
"\\Seen",
"Your October roundup",
);
fx.add("work", news);
fx.store("work")
.set_raw("INBOX", 99, &raw(NEWSLETTER), "Your October roundup")
.unwrap();
let (mut harness, wires) = open(&fx);
harness.set_size(egui::vec2(width, 800.0));
harness.state_mut().select_view(View::Folder {
account: 1,
folder: "INBOX".into(),
});
wait_for(&mut harness, painted);
(fx, harness, wires)
}
#[cfg(feature = "html")]
#[test]
fn html_light() {
let (_fx, mut harness, _wires) = newsletter("light", 1280.0);
snapshot(&mut harness, "gui_html_light");
}
#[cfg(feature = "html")]
#[test]
fn html_dark() {
let (_fx, mut harness, _wires) = newsletter("dark", 1280.0);
snapshot(&mut harness, "gui_html_dark");
}
#[cfg(feature = "html")]
#[test]
fn html_narrow() {
let (_fx, mut harness, _wires) = newsletter("light", 1060.0);
snapshot(&mut harness, "gui_html_narrow");
}