use std::fs;
use std::path::PathBuf;
use ratatui::backend::TestBackend;
use ratatui::crossterm::event::KeyCode;
use ratatui::Terminal;
use super::*;
use crate::tui::api::ApiCall;
use crate::tui::effect::Effect;
use crate::tui::i18n::Lang;
use crate::tui::model::short_id;
use crate::tui::msg::Msg;
use crate::tui::testing::*;
fn render(m: &Model, width: u16, height: u16) -> String {
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal.draw(|f| view(f, m)).unwrap();
let buffer = terminal.backend().buffer().clone();
let mut lines = Vec::new();
for y in 0..buffer.area.height {
let mut line = String::new();
for x in 0..buffer.area.width {
line.push_str(buffer[(x, y)].symbol());
}
lines.push(line.trim_end().to_owned());
}
lines.join("\n") + "\n"
}
fn snapshot(name: &str, text: &str) {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("snapshots");
let path = dir.join(format!("{name}.txt"));
if std::env::var_os("TABA_UPDATE_SNAPSHOTS").is_some() {
fs::create_dir_all(&dir).unwrap();
fs::write(&path, text).unwrap();
return;
}
let expected = fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("missing snapshot {name}; run with TABA_UPDATE_SNAPSHOTS=1"));
let normalize = |s: &str| s.replace("\r\n", "\n");
assert_eq!(
normalize(text),
normalize(&expected),
"snapshot {name} differs\n--- got ---\n{text}"
);
}
fn with_history(lang: Lang) -> Sim {
let mut sim = Sim::root_with_status(lang);
for i in 1..=24u64 {
let now = 100 + 5_000 * i;
let used = 805_306_368 + i * 8_000_000;
let rejected = i * 3;
let body = status_json(false)
.replacen("805306368", &used.to_string(), 1)
.replacen(
"rateLimitRejected\":5.0",
&format!("rateLimitRejected\":{rejected}.0"),
1,
);
let effects = sim.tick(now);
if let Some(call) = effects.iter().find(|e| matches!(e, Effect::Call { .. })) {
sim.answer(call, ok(&body));
}
}
sim.tick(125_000);
sim
}
const SIZES: [(u16, u16); 2] = [(80, 24), (120, 40)];
#[test]
fn the_cluster_tab_in_both_sizes_and_languages() {
for lang in [Lang::EnUs, Lang::PtBr] {
let sim = with_history(lang);
for (w, h) in SIZES {
snapshot(
&format!("cluster-{}-{w}x{h}", lang.code()),
&render(&sim.m, w, h),
);
}
}
}
#[test]
fn an_incomplete_read_shows_the_seal_the_condition_and_the_unreachable_node() {
for lang in [Lang::EnUs, Lang::PtBr] {
let sim = Sim::with_status_json(lang, &status_json(true));
snapshot(
&format!("partial-{}-80x24", lang.code()),
&render(&sim.m, 80, 24),
);
}
let sim = Sim::with_status_json(Lang::EnUs, &status_json(true));
let screen = render(&sim.m, 120, 40);
assert!(screen.contains("incomplete"), "{screen}");
assert!(screen.contains("timeout"), "{screen}");
snapshot("partial-en-US-120x40", &screen);
}
#[test]
fn a_plain_token_sees_the_node_list_and_why_the_rest_is_missing() {
for lang in [Lang::EnUs, Lang::PtBr] {
let sim = Sim::plain_with_nodes(lang);
let screen = render(&sim.m, 80, 24);
assert!(
!screen.contains("Memory") && !screen.contains("Memória"),
"{screen}"
);
snapshot(&format!("plain-{}-80x24", lang.code()), &screen);
}
}
#[test]
fn before_the_first_answer_and_when_the_server_is_unreachable() {
let mut sim = Sim::new(Lang::EnUs);
snapshot("loading-en-US-80x24", &render(&sim.m, 80, 24));
let effects = sim.tick(0);
sim.answer(&effects[0], offline());
let screen = render(&sim.m, 80, 24);
assert!(
screen.contains("offline") && screen.contains("taba.example"),
"{screen}"
);
snapshot("offline-en-US-80x24", &screen);
let mut sim = Sim::root_with_status(Lang::EnUs);
let effects = sim.tick(2_100);
sim.answer(&effects[0], offline());
let screen = render(&sim.m, 80, 24);
assert!(screen.contains("old data"), "{screen}");
assert!(screen.contains(&short_id(NODE_A)), "{screen}");
snapshot("stale-en-US-80x24", &screen);
}
#[test]
fn a_terminal_below_80_by_24_shows_a_warning_instead_of_the_screen() {
let sim = Sim::root_with_status(Lang::EnUs);
for (w, h) in [(79, 24), (80, 23), (40, 10)] {
let screen = render(&sim.m, w, h);
let flat = screen.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(flat.contains("at least 80x24"), "{screen}");
assert!(!screen.contains(&short_id(NODE_A)), "{screen}");
}
snapshot("too-small-en-US-79x24", &render(&sim.m, 79, 24));
let pt = Sim::root_with_status(Lang::PtBr);
let flat = render(&pt.m, 70, 20)
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
assert!(flat.contains("pelo menos 80x24"), "{flat}");
assert!(render(&sim.m, 80, 24).contains(&short_id(NODE_A)));
}
#[test]
fn the_node_detail_in_both_languages() {
for lang in [Lang::EnUs, Lang::PtBr] {
let mut sim = with_history(lang);
sim.press('i');
let effects = sim.tick(126_000);
for effect in &effects {
let Effect::Call { call, .. } = effect else {
continue;
};
match call {
ApiCall::NodeMetrics(_) => {
sim.answer(
effect,
ok("# TYPE taba_storage_memory_messages gauge\ntaba_storage_memory_messages 1200\n# TYPE taba_ratelimit_rejected_total counter\ntaba_ratelimit_rejected_total{category=\"publish\"} 5\n"),
);
}
ApiCall::NodeBackup(_) => {
sim.answer(
effect,
ok(r#"{"s3Reachable":true,"outboxPending":4,"lastSuccessAt":"2026-10-15T11:58:00Z","segments":12}"#),
);
}
_ => {}
}
}
for (w, h) in SIZES {
snapshot(
&format!("detail-{}-{w}x{h}", lang.code()),
&render(&sim.m, w, h),
);
}
}
}
#[test]
fn a_node_without_backup_says_so() {
let mut sim = Sim::root_with_status(Lang::EnUs);
sim.press('i');
let effects = sim.tick(300);
let backup = effects
.iter()
.find(|e| {
matches!(
e,
Effect::Call {
call: ApiCall::NodeBackup(_),
..
}
)
})
.unwrap()
.clone();
sim.answer(&backup, fail(422, "backup.not-configured", "{}"));
assert!(render(&sim.m, 80, 40).contains("S3 backup is not configured"));
}
#[test]
fn a_drain_in_progress_shows_its_pending_messages() {
let mut sim = Sim::root_with_status(Lang::EnUs);
sim.press('i');
sim.press('d');
sim.code(KeyCode::Right);
let effects = sim.code(KeyCode::Enter);
sim.answer(
&effects[0],
ok(r#"{"drainState":"draining","pendingMessages":40}"#),
);
let polled = sim.tick(300);
let status = polled
.iter()
.find(|e| {
matches!(
e,
Effect::Call {
call: ApiCall::DrainStatus(_),
..
}
)
})
.unwrap()
.clone();
sim.answer(
&status,
ok(r#"{"drainState":"draining","pendingMessages":12}"#),
);
let screen = render(&sim.m, 80, 40);
assert!(screen.contains("12 messages still in memory"), "{screen}");
}
#[test]
fn the_charts_of_a_node() {
for lang in [Lang::EnUs, Lang::PtBr] {
let mut sim = with_history(lang);
sim.press('m');
for (w, h) in SIZES {
snapshot(
&format!("monitor-{}-{w}x{h}", lang.code()),
&render(&sim.m, w, h),
);
}
}
let mut sim = with_history(Lang::EnUs);
sim.press('m');
let screen = render(&sim.m, 100, 30);
assert!(screen.contains("Charts"), "{screen}");
assert!(screen.contains("total "), "{screen}");
assert!(screen.contains("Messages in memory"), "{screen}");
assert!(screen.contains("collected"), "{screen}");
}
#[test]
fn the_charts_are_a_list_with_the_selected_one_beside_it_or_on_the_whole_screen() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut sim = with_history(lang);
sim.press('m');
let screen = render(&sim.m, 100, 30);
assert!(
screen.contains("Channel round trip") || screen.contains("Ida e volta"),
"{screen}"
);
snapshot(&format!("cluster-charts-{code}-100x30"), &screen);
snapshot(
&format!("cluster-charts-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.code(KeyCode::Down);
sim.code(KeyCode::Down);
sim.code(KeyCode::Enter);
assert!(sim.m.cluster.monitor.full);
snapshot(
&format!("cluster-chart-full-{code}-100x30"),
&render(&sim.m, 100, 30),
);
}
let mut sim = with_history(Lang::EnUs);
sim.press('m');
for _ in 0..10 {
sim.code(KeyCode::Down);
}
assert_eq!(
sim.m.cluster.monitor.chart,
crate::tui::screens::cluster::MON_CHARTS.len() - 1,
"the cursor stops at the last chart"
);
sim.code(KeyCode::Home);
assert_eq!(sim.m.cluster.monitor.chart, 0);
}
#[test]
fn every_chart_draws_with_and_without_data_and_frozen() {
let mut sim = Sim::root_with_status(Lang::EnUs);
sim.press('m');
for i in 0..crate::tui::screens::cluster::MON_CHARTS.len() {
sim.m.cluster.monitor.chart = i;
let screen = render(&sim.m, 80, 24);
assert!(!screen.is_empty());
}
let mut sim = with_history(Lang::EnUs);
sim.press('m');
for i in 0..crate::tui::screens::cluster::MON_CHARTS.len() {
sim.m.cluster.monitor.chart = i;
sim.m.cluster.monitor.frozen = Some(60_000);
sim.m.cluster.monitor.fixed_scale = i % 2 == 0;
let screen = render(&sim.m, 80, 24);
assert!(screen.contains("paused"), "{screen}");
}
}
#[test]
fn the_popups() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut sim = Sim::root_with_status(lang);
sim.press('?');
snapshot(&format!("help-{code}-80x24"), &render(&sim.m, 80, 24));
sim.code(KeyCode::Esc);
sim.press(':');
for c in "dr".chars() {
sim.press(c);
}
snapshot(&format!("palette-{code}-80x24"), &render(&sim.m, 80, 24));
sim.code(KeyCode::Esc);
sim.press('l');
snapshot(&format!("language-{code}-80x24"), &render(&sim.m, 80, 24));
sim.code(KeyCode::Esc);
sim.press('d');
snapshot(
&format!("confirm-drain-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.code(KeyCode::Esc);
sim.press('S');
for c in "prod".chars() {
sim.press(c);
}
snapshot(
&format!("confirm-stop-{code}-80x24"),
&render(&sim.m, 80, 24),
);
}
let mut sim = Sim::with_status_json(Lang::EnUs, &status_json(true));
sim.code(KeyCode::Tab);
sim.code(KeyCode::Enter);
snapshot("condition-en-US-80x24", &render(&sim.m, 80, 24));
}
#[test]
fn the_reports_of_the_actions() {
let mut sim = Sim::root_with_status(Lang::EnUs);
sim.press('B');
sim.code(KeyCode::Right);
let effects = sim.code(KeyCode::Enter);
sim.answer(
&effects[0],
ok(&format!(
r#"{{"complete":false,"nodes":[{{"nodeId":"{NODE_A}","complete":true}},{{"nodeId":"{NODE_B}","complete":false,"pending":true,"outboxPending":9,"memoryRemaining":3}},{{"nodeId":"{NODE_C}","complete":false,"error":"timeout"}}]}}"#
)),
);
snapshot("report-flush-en-US-80x24", &render(&sim.m, 80, 24));
sim.code(KeyCode::Esc);
sim.press('S');
for c in "prod-eu".chars() {
sim.press(c);
}
let effects = sim.code(KeyCode::Enter);
sim.answer(
&effects[0],
ok(&format!(
r#"{{"stopped":["{NODE_B}","{NODE_A}"],"stopList":false,"warnings":["S3 is down"],"nodes":[{{"nodeId":"{NODE_B}","drained":true}},{{"nodeId":"{NODE_A}","drained":true}}]}}"#
)),
);
snapshot("report-stop-en-US-80x24", &render(&sim.m, 80, 24));
sim.code(KeyCode::Esc);
sim.press('b');
sim.code(KeyCode::Right);
let effects = sim.code(KeyCode::Enter);
sim.answer(
&effects[0],
fail(
502,
"forward.bad-gateway",
r#"{"message":"node did not answer"}"#,
),
);
snapshot("report-failure-en-US-80x24", &render(&sim.m, 80, 24));
}
#[test]
fn the_export_popup_asks_for_the_path() {
let mut sim = with_history(Lang::EnUs);
sim.press('m');
sim.press('e');
let screen = render(&sim.m, 80, 24);
assert!(screen.contains("taba-00000001-memory.csv"), "{screen}");
assert!(screen.contains("never overwritten"), "{screen}");
}
#[test]
fn the_footer_shows_the_keys_of_the_screen_and_dims_what_cannot_run() {
let sim = Sim::root_with_status(Lang::EnUs);
let screen = render(&sim.m, 80, 24);
let footer = screen.lines().last().unwrap();
assert!(
footer.contains("drain") && footer.contains("quit") && footer.contains("help"),
"{footer}"
);
assert!(footer.contains("en-US"), "{footer}");
assert!(footer.contains("2s"), "{footer}");
let mut paused = Sim::root_with_status(Lang::EnUs);
paused.press('p');
let footer = render(&paused.m, 80, 24).lines().last().unwrap().to_owned();
assert!(footer.contains("paused"), "{footer}");
let mut stale = Sim::root_with_status(Lang::PtBr);
let effects = stale.tick(2_100);
stale.answer(&effects[0], offline());
let footer = render(&stale.m, 80, 24).lines().last().unwrap().to_owned();
assert!(
footer.contains("dado velho") && footer.contains("nova tentativa"),
"{footer}"
);
}
#[test]
fn the_selected_row_is_a_band_not_a_reversed_cell_and_no_color_still_marks_it() {
use ratatui::style::Modifier;
let sim = Sim::root_with_status(Lang::EnUs);
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
terminal.draw(|f| view(f, &sim.m)).unwrap();
let buffer = terminal.backend().buffer().clone();
let row = (0..24)
.find(|y| {
(0..80)
.map(|x| buffer[(x, *y)].symbol())
.collect::<String>()
.contains(&short_id(NODE_A))
})
.unwrap();
let cell = &buffer[(10, row)];
assert!(cell.modifier.contains(Modifier::BOLD));
assert!(!cell.modifier.contains(Modifier::REVERSED));
assert_ne!(cell.bg, ratatui::style::Color::Reset);
let mut plain = Sim::root_with_status(Lang::EnUs);
plain.m.theme = crate::tui::theme::Theme {
color: false,
ascii: true,
};
let screen = render(&plain.m, 80, 24);
assert!(screen.contains("> "), "{screen}");
assert!(
!screen.contains('✔')
&& !screen.contains('▶')
&& !screen.contains('█')
&& !screen.contains('⟳'),
"{screen}"
);
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
terminal.draw(|f| view(f, &plain.m)).unwrap();
let colored = terminal
.backend()
.buffer()
.content()
.iter()
.any(|c| c.fg != ratatui::style::Color::Reset || c.bg != ratatui::style::Color::Reset);
assert!(!colored, "NO_COLOR must not paint");
}
#[test]
fn the_status_line_says_the_worst_condition_in_words_and_a_symbol() {
let sim = Sim::with_status_json(Lang::EnUs, &status_json(true));
let first = render(&sim.m, 80, 24).lines().next().unwrap().to_owned();
assert!(
first.contains("prod") && first.contains("taba.example"),
"{first}"
);
assert!(
first.contains("member unreachable") && first.contains('▲'),
"{first}"
);
assert!(first.contains("UTC"), "{first}");
let healthy = Sim::root_with_status(Lang::PtBr);
let first = render(&healthy.m, 80, 24)
.lines()
.next()
.unwrap()
.to_owned();
assert!(first.contains("saudável") && first.contains('✔'), "{first}");
}
#[test]
fn a_long_detail_scrolls_and_shows_its_bar_only_when_it_overflows() {
let mut sim = Sim::root_with_status(Lang::EnUs);
sim.code(KeyCode::Enter);
let top = render(&sim.m, 80, 24);
assert!(
top.contains('█') || top.contains('║') || top.contains('▲') || top.contains('↑'),
"{top}"
);
sim.code(KeyCode::End);
sim.send(Msg::Scroll(3));
let bottom = render(&sim.m, 80, 24);
assert_ne!(top, bottom);
sim.code(KeyCode::Home);
assert_eq!(render(&sim.m, 80, 24), top);
}
#[test]
fn a_node_without_a_memory_limit_shows_its_use_and_says_so() {
let status = status_json(false).replace(
"\"memoryLimitBytes\":1073741824",
"\"memoryLimitBytes\":null",
);
let mut sim = Sim::with_status_json(Lang::EnUs, &status);
let table = render(&sim.m, 80, 24);
assert!(
table.contains("768.0 MiB") && !table.contains(" 75%"),
"{table}"
);
sim.press('i');
let detail = render(&sim.m, 80, 40);
assert!(detail.contains("(no limit)"), "{detail}");
}
#[test]
fn a_flush_on_a_cluster_without_s3_says_there_is_nothing_to_send() {
let mut sim = Sim::root_with_status(Lang::EnUs);
sim.press('B');
sim.code(KeyCode::Right);
let effects = sim.code(KeyCode::Enter);
sim.answer(&effects[0], fail(422, "backup.not-configured", "{}"));
let screen = render(&sim.m, 100, 30);
assert!(screen.contains("backup.not-configured"), "{screen}");
assert!(screen.contains("nothing to send"), "{screen}");
assert!(screen.contains("request id: req-1"), "{screen}");
}
#[test]
fn a_stopped_cluster_says_so_and_why() {
for (planned, expected) in [(true, "planned stop"), (false, "every node is in drain")] {
let status = status_json(false).replace(
r#""syncingBlocks":0}"#,
&format!(
r#""syncingBlocks":0,"servingNodes":0,"stopped":true,"plannedStop":{planned},"stoppedAt":"2026-10-15T12:00:00.000Z"}}"#
),
);
let sim = Sim::with_status_json(Lang::EnUs, &status);
let screen = render(&sim.m, 120, 30);
assert!(screen.contains(expected), "{screen}");
let pt = Sim::with_status_json(Lang::PtBr, &status);
let screen = render(&pt.m, 120, 30);
assert!(screen.contains("Cluster parado"), "{screen}");
}
let sim = Sim::root_with_status(Lang::EnUs);
assert!(!render(&sim.m, 120, 30).contains("Cluster stopped"));
}
#[test]
fn the_tokens_tab_in_both_languages_and_without_the_root_token() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let sim = Sim::on_tokens(lang);
let screen = render(&sim.m, 80, 24);
snapshot(&format!("tokens-{code}-80x24"), &screen);
assert!(
screen.contains("orders-app") && screen.contains("billing"),
"{screen}"
);
snapshot(&format!("tokens-{code}-120x40"), &render(&sim.m, 120, 40));
}
let mut sim = Sim::plain_with_nodes(Lang::EnUs);
sim.press('3');
let screen = render(&sim.m, 80, 24);
assert!(screen.contains("must be the root token"), "{screen}");
assert!(screen.contains("app"), "{screen}");
snapshot("tokens-plain-en-US-80x24", &screen);
}
#[test]
fn the_token_forms() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut sim = Sim::on_tokens(lang);
sim.press('n');
for c in "reports".chars() {
sim.press(c);
}
snapshot(
&format!("token-create-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.code(KeyCode::Esc);
sim.press('e');
snapshot(&format!("token-edit-{code}-80x24"), &render(&sim.m, 80, 24));
}
}
#[test]
fn the_secret_is_shown_once_and_hidden_after_a_while() {
let mut sim = Sim::on_tokens(Lang::EnUs);
sim.m.now = 2_000;
sim.m.modal = Some(crate::tui::model::Modal::Secret(
crate::tui::model::Secret {
id: "0191-z".into(),
name: "reports".into(),
token: "tb_0191-z.s3cretvalue".into(),
shown_at: 2_000,
copied: false,
saved: false,
warned: false,
replaces: Some(("0191-a".into(), "orders-app".into())),
revoke_old: false,
},
));
let shown = render(&sim.m, 80, 24);
assert!(shown.contains("tb_0191-z.s3cretvalue"), "{shown}");
snapshot("secret-en-US-80x24", &shown);
sim.m.now = 40_000;
let hidden = render(&sim.m, 80, 24);
assert!(!hidden.contains("s3cretvalue"), "{hidden}");
assert!(hidden.contains("tb_0191-z."), "{hidden}");
sim.m.now = 3_000;
sim.m.focused = false;
assert!(!render(&sim.m, 80, 24).contains("s3cretvalue"));
snapshot("secret-hidden-en-US-80x24", &hidden);
}
#[test]
fn the_profiles_tab_and_its_form() {
use crate::profile::{ConfigFile, Profile};
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut file = ConfigFile::default();
for (name, url, token) in [
(
"dev",
"http://localhost:8080",
Some(("token-file", "dev.token")),
),
(
"prod",
"https://taba.example:8080",
Some(("token", "tb_secret.value")),
),
("lab", "https://lab.example", None),
] {
let mut profile = Profile::default();
profile.set("url", url).unwrap();
if let Some((field, value)) = token {
profile.set(field, value).unwrap();
}
file.add(name, profile).unwrap();
}
file.default = Some("prod".to_owned());
let mut options = options(lang);
options.profiles = file;
let mut sim = Sim::with_options(options);
sim.press('4');
sim.press('j');
let screen = render(&sim.m, 80, 24);
assert!(!screen.contains("tb_secret"), "{screen}");
snapshot(&format!("profiles-{code}-80x24"), &screen);
sim.press('e');
let form = render(&sim.m, 80, 24);
assert!(!form.contains("tb_secret"), "{form}");
snapshot(&format!("profile-edit-{code}-80x24"), &form);
}
let mut sim = Sim::new(Lang::EnUs);
sim.press('4');
snapshot("profiles-empty-en-US-80x24", &render(&sim.m, 80, 24));
}
#[test]
fn the_choice_of_a_profile_at_the_start() {
use crate::profile::{ConfigFile, Profile};
for lang in [Lang::EnUs, Lang::PtBr] {
let mut file = ConfigFile::default();
for (name, url) in [
("dev", "http://localhost:8080"),
("prod", "https://taba.example:8080"),
] {
let mut profile = Profile::default();
profile.set("url", url).unwrap();
file.add(name, profile).unwrap();
}
file.default = Some("prod".to_owned());
let mut options = options(lang);
options.profiles = file;
options.choose_profile = true;
let sim = Sim::with_options(options);
let screen = render(&sim.m, 80, 24);
assert!(
screen.contains("prod") && screen.contains("dev"),
"{screen}"
);
snapshot(&format!("profile-pick-{}-80x24", lang.code()), &screen);
}
}
#[test]
fn the_footer_of_the_tokens_tab_dims_what_the_token_cannot_do() {
let mut sim = Sim::plain_with_nodes(Lang::EnUs);
sim.press('3');
let footer = render(&sim.m, 120, 40).lines().last().unwrap().to_owned();
assert!(
footer.contains("new") && footer.contains("revoke"),
"{footer}"
);
let ready = Sim::on_tokens(Lang::EnUs);
let footer = render(&ready.m, 120, 40).lines().last().unwrap().to_owned();
assert!(
footer.contains("clone") && footer.contains("language"),
"{footer}"
);
}
#[test]
fn the_first_profile_form_opens_when_there_is_none() {
for lang in [Lang::EnUs, Lang::PtBr] {
let mut options = options(lang);
options.choose_profile = true;
let sim = Sim::with_options(options);
snapshot(
&format!("profile-first-{}-80x24", lang.code()),
&render(&sim.m, 80, 24),
);
}
}
#[test]
fn the_topics_tab_in_both_sizes_and_languages() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut sim = Sim::on_topics(lang);
let screen = render(&sim.m, 80, 24);
assert!(
screen.contains("orders") && screen.contains("fanout") && screen.contains("backlog"),
"{screen}"
);
snapshot(&format!("topics-{code}-80x24"), &screen);
snapshot(&format!("topics-{code}-120x40"), &render(&sim.m, 120, 40));
sim.press('g');
snapshot(
&format!("topics-distributor-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.press('v');
sim.press('j');
sim.press('j');
snapshot(
&format!("topics-dead-letter-{code}-80x24"),
&render(&sim.m, 80, 24),
);
}
}
#[test]
fn the_topic_forms_and_the_delete_confirmation() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut sim = Sim::on_topics(lang);
sim.press('n');
for c in "invoices".chars() {
sim.press(c);
}
snapshot(
&format!("topic-create-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.code(KeyCode::Esc);
sim.press('j');
sim.press('e');
snapshot(&format!("topic-edit-{code}-80x24"), &render(&sim.m, 80, 24));
sim.code(KeyCode::Esc);
sim.press('d');
for c in "ord".chars() {
sim.press(c);
}
snapshot(
&format!("topic-delete-{code}-80x24"),
&render(&sim.m, 80, 24),
);
}
}
#[test]
fn the_three_charts_of_a_queue() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut sim = Sim::on_topic_charts(lang);
sim.press('-');
let screen = render(&sim.m, 80, 24);
assert!(
screen.contains("customer-17") || screen.contains("orders"),
"{screen}"
);
snapshot(&format!("topic-charts-{code}-80x24"), &screen);
snapshot(
&format!("topic-charts-{code}-120x40"),
&render(&sim.m, 120, 40),
);
}
}
#[test]
fn the_menu_of_an_item_names_what_it_acts_on() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut sim = Sim::on_topics(lang);
sim.press('j');
sim.code(KeyCode::Enter);
let screen = render(&sim.m, 80, 24);
let flat = screen.split_whitespace().collect::<Vec<_>>().join(" ");
let (topic, message) = match lang {
Lang::EnUs => ("Edit the topic", "Publish a message"),
Lang::PtBr => ("Editar o tópico", "Publicar uma mensagem"),
};
assert!(flat.contains(topic) && flat.contains(message), "{screen}");
snapshot(&format!("menu-topic-{code}-80x24"), &screen);
}
let mut sim = Sim::on_topics(Lang::EnUs);
sim.code(KeyCode::Enter);
let screen = render(&sim.m, 120, 40);
assert!(screen.contains("Topic fanout"), "{screen}");
snapshot("menu-distributor-en-US-120x40", &screen);
}
#[test]
fn under_the_list_the_three_charts_of_the_selected_queue_sit_side_by_side() {
for lang in [Lang::EnUs, Lang::PtBr] {
let mut sim = Sim::on_topics(lang);
sim.press('j');
for (step, now) in [2_000u64, 7_000, 12_000, 17_000, 22_000]
.into_iter()
.enumerate()
{
let effects = sim.tick(now);
let asked = effects
.iter()
.find(|e| {
matches!(
e,
Effect::Call {
call: ApiCall::TopicDetail(_),
..
}
)
})
.expect("the strip asks for a reading")
.clone();
sim.answer(&asked, ok(&topic_sample_json(step as u64)));
}
let code = lang.code();
for (w, h) in SIZES {
let screen = render(&sim.m, w, h);
assert!(screen.contains("orders"), "{screen}");
snapshot(&format!("topics-strip-{code}-{w}x{h}"), &screen);
}
sim.press('i');
snapshot(
&format!("topic-summary-{code}-80x24"),
&render(&sim.m, 80, 24),
);
}
}
#[test]
fn the_charts_of_a_topic_are_a_list_with_the_selected_one_beside_it_or_on_the_whole_screen() {
let mut sim = Sim::on_topic_charts(Lang::EnUs);
let screen = render(&sim.m, 120, 40);
assert!(screen.contains("Topics › orders · Charts"), "{screen}");
assert!(screen.contains("Flow per second"), "{screen}");
snapshot("topic-charts-list-en-US-120x40", &screen);
sim.code(KeyCode::Down);
sim.code(KeyCode::Enter);
assert!(sim.m.topics.monitor.as_ref().unwrap().monitor.full);
snapshot("topic-chart-full-en-US-120x40", &render(&sim.m, 120, 40));
sim.code(KeyCode::Esc);
assert!(sim.m.topics.monitor.is_some());
assert!(!sim.m.topics.monitor.as_ref().unwrap().monitor.full);
sim.code(KeyCode::Esc);
assert!(sim.m.topics.monitor.is_none());
}
#[test]
fn the_messages_of_a_topic() {
for lang in [Lang::EnUs, Lang::PtBr] {
let code = lang.code();
let mut sim = Sim::on_topic_messages(lang);
let effects = sim.tick(2_000);
let asked = effects
.iter()
.find(|e| {
matches!(
e,
Effect::Call {
call: ApiCall::TopicDetail(_),
..
}
)
})
.expect("the open topic is sampled")
.clone();
sim.answer(&asked, ok(&topic_sample_json(0)));
let screen = render(&sim.m, 80, 24);
assert!(
screen.contains("Topics › orders · Messages")
|| screen.contains("Tópicos › orders · Mensagens"),
"{screen}"
);
snapshot(&format!("messages-empty-{code}-80x24"), &screen);
sim.press('r');
snapshot(
&format!("messages-receive-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.code(KeyCode::Enter);
let effects = sim.code(KeyCode::Enter);
let read = effects
.iter()
.find(|e| {
matches!(
e,
Effect::Call {
call: ApiCall::Receive(..),
..
}
)
})
.expect("a reading")
.clone();
sim.answer(&read, ok(&messages_json(4)));
sim.press('j');
let effects = sim.press('a');
let ack = effects.first().expect("an acknowledgement").clone();
sim.answer(&ack, ok("{}"));
snapshot(
&format!("messages-rows-{code}-80x24"),
&render(&sim.m, 80, 24),
);
snapshot(
&format!("messages-rows-{code}-120x40"),
&render(&sim.m, 120, 40),
);
sim.code(KeyCode::Home);
sim.code(KeyCode::Enter);
snapshot(
&format!("message-held-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.press('f');
snapshot(
&format!("message-formatted-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.press('b');
snapshot(
&format!("message-base64-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.press('b');
sim.m.now += 40_000;
let screen = render(&sim.m, 80, 24);
assert!(
screen.contains("time ran out") || screen.contains("tempo acabou"),
"{screen}"
);
snapshot(&format!("message-expired-{code}-80x24"), &screen);
sim.m.now -= 40_000;
sim.code(KeyCode::Esc);
sim.press('p');
for c in "{".chars() {
sim.press(c);
}
sim.code(KeyCode::Enter);
for c in " \"order\": 7".chars() {
sim.press(c);
}
sim.code(KeyCode::Enter);
sim.press('}');
snapshot(
&format!("messages-publish-{code}-80x24"),
&render(&sim.m, 80, 24),
);
sim.code(KeyCode::Esc);
sim.press('P');
snapshot(
&format!("messages-prune-{code}-80x24"),
&render(&sim.m, 80, 24),
);
}
let mut sim = Sim::on_topics(Lang::EnUs);
sim.press('x');
snapshot("messages-distributor-en-US-80x24", &render(&sim.m, 80, 24));
}