use std::path::PathBuf;
use chrono::{TimeZone, Utc};
use ratatui::{backend::TestBackend, buffer::Buffer, Terminal};
use serial_test::serial;
use tsafe_tui::app::{App, EditFocus, LoginFocus, Screen, Theme};
use tsafe_tui::ui;
fn buffer_to_plain_string(buf: &Buffer) -> String {
let area = buf.area;
let mut out = String::with_capacity((area.width as usize + 1) * area.height as usize);
for y in 0..area.height {
for x in 0..area.width {
out.push_str(buf[(x, y)].symbol());
}
out.push('\n');
}
out
}
fn render_frame_with_size(app: &mut App, width: u16, height: u16) -> String {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).expect("terminal");
terminal.draw(|f| ui::render(f, app)).expect("draw");
buffer_to_plain_string(terminal.backend().buffer())
}
fn render_frame(app: &mut App) -> String {
render_frame_with_size(app, 80, 24)
}
fn normalize_pkg_version_in_snapshot(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(i) = rest.find(" v") {
out.push_str(&rest[..i + 2]);
rest = &rest[i + 2..];
let end = rest
.find(|c: char| !c.is_ascii_digit() && c != '.')
.unwrap_or(rest.len());
let ver = &rest[..end];
if is_semver_triple(ver) {
out.push_str("0.0.0");
} else {
out.push_str(ver);
}
rest = &rest[end..];
}
out.push_str(rest);
out
}
fn is_semver_triple(s: &str) -> bool {
let mut parts = s.split('.');
match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(a), Some(b), Some(c), None) if !a.is_empty() && !b.is_empty() && !c.is_empty() => {
a.chars().all(|d| d.is_ascii_digit())
&& b.chars().all(|d| d.is_ascii_digit())
&& c.chars().all(|d| d.is_ascii_digit())
}
_ => false,
}
}
fn snapshot_frame(app: &mut App) -> String {
normalize_pkg_version_in_snapshot(&render_frame(app))
}
fn base_app_dark_empty_vault() -> App {
let mut app = App::new();
app.update_rx = None;
app.update_available = None;
app.theme = Theme::Dark;
app
}
fn app_dashboard_demo_empty_secrets(mut base: App) -> App {
base.profiles = vec!["demo".to_string()];
base.profile_cursor = 0;
base.active_profile = Some("demo".to_string());
base.screen = Screen::Dashboard;
base.secret_keys = vec![];
base.secret_cursor = 0;
base.search_mode = false;
base.search_query.clear();
base
}
fn app_dashboard_demo_with_secrets(theme: Theme) -> App {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.theme = theme;
app.secret_keys = vec![
"API_URL".into(),
"prod/DB_PASS".into(),
"prod/REDIS_URL".into(),
];
app.secret_cursor = 2;
app
}
fn assert_rendered_surface(
case_name: &str,
theme: Theme,
width: u16,
height: u16,
mut app: App,
markers: &[&str],
) {
app.theme = theme;
let rendered =
normalize_pkg_version_in_snapshot(&render_frame_with_size(&mut app, width, height));
let lines: Vec<&str> = rendered.lines().collect();
assert_eq!(
lines.len(),
height as usize,
"{case_name} {theme:?} {width}x{height} rendered an unexpected row count",
);
assert!(
rendered.lines().any(|line| !line.trim().is_empty()),
"{case_name} {theme:?} {width}x{height} rendered a blank frame",
);
assert!(
!rendered.contains('\u{fffd}'),
"{case_name} {theme:?} {width}x{height} rendered replacement characters:\n{rendered}",
);
let lower = rendered.to_lowercase();
for marker in markers {
assert!(
lower.contains(&marker.to_lowercase()),
"{case_name} {theme:?} {width}x{height} missing marker '{marker}':\n{rendered}",
);
}
}
#[serial]
#[test]
fn primary_screens_render_across_theme_and_terminal_matrix() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let themes = [Theme::Dark, Theme::Light, Theme::HighContrast];
let sizes = [(60, 16), (80, 24), (100, 30)];
for theme in themes {
for (width, height) in sizes {
let mut cases: Vec<(&str, App, &[&str])> = Vec::new();
cases.push(("login", base_app_dark_empty_vault(), &["tsafe", "login"]));
let mut login_help = base_app_dark_empty_vault();
login_help.help_visible = true;
cases.push(("login_help", login_help, &["help"]));
cases.push((
"dashboard",
app_dashboard_demo_with_secrets(theme),
&["tsafe", "api_url", "prod"],
));
let mut search = app_dashboard_demo_with_secrets(theme);
search.search_mode = true;
search.search_query = "db".into();
cases.push(("dashboard_search", search, &["search", "db"]));
let mut edit = app_dashboard_demo_with_secrets(theme);
edit.screen = Screen::EditSecret { is_new: true };
edit.edit_key = "MY_KEY".into();
edit.edit_value = "hunter2".into();
edit.edit_focus = EditFocus::Value;
cases.push(("edit_secret", edit, &["add secret", "key", "value"]));
let mut confirm = app_dashboard_demo_with_secrets(theme);
confirm.screen = Screen::ConfirmDelete;
cases.push(("confirm_delete", confirm, &["confirm delete"]));
let mut rotate = app_dashboard_demo_with_secrets(theme);
rotate.screen = Screen::RotatePassword;
rotate.rotate_step = 0;
cases.push(("rotate_password", rotate, &["rotate master password"]));
let mut snapshots = app_dashboard_demo_with_secrets(theme);
snapshots.screen = Screen::SnapshotRestore;
snapshots.snapshot_paths =
vec![PathBuf::from("/tmp/tsafe-golden-snapshot.snap.json")];
cases.push(("snapshot_restore", snapshots, &["restore snapshot"]));
let mut audit = app_dashboard_demo_with_secrets(theme);
audit.screen = Screen::AuditLog;
audit.audit_lines = vec!["[2026-04-08 12:00:00] set ok MY_KEY saved".into()];
cases.push(("audit_log", audit, &["audit log", "my_key"]));
let mut history = app_dashboard_demo_with_secrets(theme);
let ts = Utc.with_ymd_and_hms(2026, 4, 8, 15, 30, 0).unwrap();
history.screen = Screen::History {
key: "MY_KEY".into(),
};
history.history_entries = vec![(0, ts), (1, ts)];
cases.push(("history", history, &["history", "my_key"]));
let mut profile = base_app_dark_empty_vault();
profile.screen = Screen::NewProfile { step: 0 };
cases.push(("new_profile", profile, &["create new profile"]));
let mut move_secret = app_dashboard_demo_with_secrets(theme);
move_secret.screen = Screen::MoveSecret {
source: "prod/API_KEY".into(),
};
move_secret.mv_dest_buf = "staging/API_KEY".into();
cases.push(("move_secret", move_secret, &["move", "prod/api_key"]));
let mut ns_bulk = app_dashboard_demo_with_secrets(theme);
ns_bulk.screen = Screen::NsBulk {
copy: true,
from: "prod".into(),
};
ns_bulk.mv_dest_buf = "staging".into();
cases.push(("namespace_bulk", ns_bulk, &["copy", "prod"]));
for (case_name, app, markers) in cases {
assert_rendered_surface(case_name, theme, width, height, app, markers);
}
}
}
});
}
#[serial]
#[test]
fn login_empty_vault_dir_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!(rendered);
});
}
#[serial]
#[test]
fn help_overlay_on_login_empty_vault_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.help_visible = true;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("help_overlay_on_login_empty_vault_dark_theme", rendered);
});
}
#[serial]
#[test]
fn dashboard_empty_secrets_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("dashboard_empty_secrets_dark_theme", rendered);
});
}
#[serial]
#[test]
fn dashboard_status_message_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.set_status("Status: vault ready.");
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("dashboard_status_message_dark_theme", rendered);
});
}
#[serial]
#[test]
fn help_overlay_on_dashboard_empty_secrets_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.help_visible = true;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!(
"help_overlay_on_dashboard_empty_secrets_dark_theme",
rendered
);
});
}
#[serial]
#[test]
fn dashboard_search_mode_empty_query_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.search_mode = true;
app.search_query.clear();
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("dashboard_search_mode_empty_query_dark_theme", rendered);
});
}
#[serial]
#[test]
fn add_secret_modal_new_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::EditSecret { is_new: true };
app.edit_key = "MY_KEY".to_string();
app.edit_value = "hunter2".to_string();
app.edit_focus = EditFocus::Key;
app.edit_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("add_secret_modal_new_dark_theme", rendered);
});
}
#[serial]
#[test]
fn add_secret_modal_value_focus_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::EditSecret { is_new: true };
app.edit_key = "MY_KEY".to_string();
app.edit_value = "hunter2".to_string();
app.edit_focus = EditFocus::Value;
app.edit_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("add_secret_modal_value_focus_dark_theme", rendered);
});
}
#[serial]
#[test]
fn confirm_delete_modal_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.secret_keys = vec!["prod/API_KEY".to_string()];
app.secret_cursor = 0;
app.screen = Screen::ConfirmDelete;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("confirm_delete_modal_dark_theme", rendered);
});
}
#[serial]
#[test]
fn new_profile_wizard_step0_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.screen = Screen::NewProfile { step: 0 };
app.new_profile_name.clear();
app.new_profile_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("new_profile_wizard_step0_dark_theme", rendered);
});
}
#[serial]
#[test]
fn rotate_password_modal_step0_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::RotatePassword;
app.rotate_step = 0;
app.rotate_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("rotate_password_modal_step0_dark_theme", rendered);
});
}
#[serial]
#[test]
fn audit_log_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::AuditLog;
app.audit_lines = vec!["[2026-04-08 12:00:00] set ok MY_KEY saved".into()];
app.audit_scroll = 0;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("audit_log_dark_theme", rendered);
});
}
#[serial]
#[test]
fn move_secret_modal_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::MoveSecret {
source: "prod/API_KEY".into(),
};
app.mv_dest_buf = "staging/API_KEY".into();
app.mv_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("move_secret_modal_dark_theme", rendered);
});
}
#[serial]
#[test]
fn ns_bulk_copy_namespace_modal_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::NsBulk {
copy: true,
from: "prod".into(),
};
app.mv_dest_buf = "staging".into();
app.mv_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("ns_bulk_copy_namespace_modal_dark_theme", rendered);
});
}
#[serial]
#[test]
fn ns_bulk_move_namespace_modal_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::NsBulk {
copy: false,
from: "oldapp".into(),
};
app.mv_dest_buf = "newapp".into();
app.mv_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("ns_bulk_move_namespace_modal_dark_theme", rendered);
});
}
#[serial]
#[test]
fn snapshot_restore_picker_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::SnapshotRestore;
app.snapshot_paths = vec![PathBuf::from("/tmp/tsafe-golden-snapshot.snap.json")];
app.snapshot_cursor = 0;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("snapshot_restore_picker_dark_theme", rendered);
});
}
#[serial]
#[test]
fn history_viewer_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
let ts = Utc.with_ymd_and_hms(2026, 4, 8, 15, 30, 0).unwrap();
app.screen = Screen::History {
key: "MY_KEY".into(),
};
app.history_entries = vec![(0, ts), (1, ts)];
app.history_scroll = 0;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("history_viewer_dark_theme", rendered);
});
}
#[serial]
#[test]
fn dashboard_error_status_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.set_status("Error: example failure message.");
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("dashboard_error_status_dark_theme", rendered);
});
}
#[serial]
#[test]
fn rotate_password_modal_step1_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::RotatePassword;
app.rotate_step = 1;
app.rotate_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("rotate_password_modal_step1_dark_theme", rendered);
});
}
#[serial]
#[test]
fn new_profile_wizard_step1_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.screen = Screen::NewProfile { step: 1 };
app.new_profile_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("new_profile_wizard_step1_dark_theme", rendered);
});
}
#[serial]
#[test]
fn dashboard_with_secrets_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.secret_keys = vec![
"API_URL".into(),
"prod/DB_PASS".into(),
"prod/REDIS_URL".into(),
];
app.secret_cursor = 2;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("dashboard_with_secrets_dark_theme", rendered);
});
}
#[serial]
#[test]
fn edit_secret_modal_existing_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::EditSecret { is_new: false };
app.edit_key = "prod/DB_PASS".into();
app.edit_value = "postgres://…".into();
app.edit_focus = EditFocus::Key;
app.edit_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("edit_secret_modal_existing_dark_theme", rendered);
});
}
#[serial]
#[test]
fn new_profile_wizard_step2_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.screen = Screen::NewProfile { step: 2 };
app.new_profile_error = None;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("new_profile_wizard_step2_dark_theme", rendered);
});
}
#[serial]
#[test]
fn login_two_profiles_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.profiles = vec!["alpha".into(), "beta".into()];
app.profile_cursor = 1;
app.login_focus = LoginFocus::Profile;
app.screen = Screen::Login;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("login_two_profiles_dark_theme", rendered);
});
}
#[serial]
#[test]
fn login_empty_vault_light_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.theme = Theme::Light;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("login_empty_vault_light_theme", rendered);
});
}
#[serial]
#[test]
fn login_empty_vault_high_contrast_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.theme = Theme::HighContrast;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("login_empty_vault_high_contrast_theme", rendered);
});
}
#[serial]
#[test]
fn dashboard_collapsed_namespace_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.secret_keys = vec!["API_URL".into(), "prod/DB_PASS".into()];
app.collapsed_namespaces.insert("prod".into());
app.secret_cursor = 0;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("dashboard_collapsed_namespace_dark_theme", rendered);
});
}
#[serial]
#[test]
fn add_secret_modal_with_error_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::EditSecret { is_new: true };
app.edit_key.clear();
app.edit_value.clear();
app.edit_focus = EditFocus::Key;
app.edit_error = Some("KEY must not be empty.".into());
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("add_secret_modal_with_error_dark_theme", rendered);
});
}
#[serial]
#[test]
fn dashboard_empty_secrets_light_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.theme = Theme::Light;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("dashboard_empty_secrets_light_theme", rendered);
});
}
#[serial]
#[test]
fn dashboard_with_secrets_light_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.theme = Theme::Light;
app.secret_keys = vec![
"API_URL".into(),
"prod/DB_PASS".into(),
"prod/REDIS_URL".into(),
];
app.secret_cursor = 2;
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("dashboard_with_secrets_light_theme", rendered);
});
}
#[serial]
#[test]
fn login_unlock_error_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.profiles = vec!["demo".into()];
app.profile_cursor = 0;
app.login_focus = LoginFocus::Password;
app.login_error = Some("Decryption failed — wrong password or corrupt vault.".into());
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("login_unlock_error_dark_theme", rendered);
});
}
#[serial]
#[test]
fn new_profile_wizard_step0_with_error_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = base_app_dark_empty_vault();
app.screen = Screen::NewProfile { step: 0 };
app.new_profile_name.clear();
app.new_profile_error = Some("Profile name must not be empty.".into());
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("new_profile_wizard_step0_with_error_dark_theme", rendered);
});
}
#[serial]
#[test]
fn rotate_password_modal_step0_with_error_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::RotatePassword;
app.rotate_step = 0;
app.rotate_error = Some("Password must not be empty.".into());
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!(
"rotate_password_modal_step0_with_error_dark_theme",
rendered
);
});
}
#[serial]
#[test]
fn move_secret_modal_with_error_dark_theme() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir.path().as_os_str()), || {
let mut app = app_dashboard_demo_empty_secrets(base_app_dark_empty_vault());
app.screen = Screen::MoveSecret {
source: "prod/API_KEY".into(),
};
app.mv_dest_buf.clear();
app.mv_error = Some("Destination key must not be empty.".into());
let rendered = snapshot_frame(&mut app);
insta::assert_snapshot!("move_secret_modal_with_error_dark_theme", rendered);
});
}
#[cfg(test)]
mod normalize_unit {
use super::*;
#[test]
fn normalizes_triple_semver_after_space_v() {
assert_eq!(
normalize_pkg_version_in_snapshot("head v12.34.56 tail"),
"head v0.0.0 tail"
);
}
#[test]
fn leaves_non_triple_version_unchanged() {
assert_eq!(
normalize_pkg_version_in_snapshot("x v1.2 y"),
"x v1.2 y"
);
}
#[test]
fn normalizes_multiple_badges() {
assert_eq!(
normalize_pkg_version_in_snapshot("a v1.0.1 b v2.0.0"),
"a v0.0.0 b v0.0.0"
);
}
}