Lines
95.8 %
Functions
100 %
Branches
//! Tests for the generic Form type and validate function.
use super::*;
use crate::widgets::{EditMode, Editor, SelectOption};
fn empty_editor() -> Editor {
Editor::new(EditMode::Emacs)
}
fn editor_with(s: &str) -> Editor {
Editor::with_buffer(EditMode::Emacs, s.to_string())
#[test]
fn cycle_forward_wraps_two_field_form() {
let mut form = Form::config_set(empty_editor(), empty_editor());
assert_eq!(form.focus, 0);
form.cycle(true);
assert_eq!(form.focus, 1);
fn cycle_backward_wraps_two_field_form() {
form.cycle(false);
fn cycle_forward_wraps_three_field_form() {
use crate::tabs::reports::ReportKind;
let mut form = Form::report_params(
empty_editor(),
ReportKind::Activity,
);
assert_eq!(form.focus, 2);
fn cycle_backward_wraps_three_field_form() {
fn validate_config_set_empty_key_is_error() {
let form = Form::config_set(empty_editor(), editor_with("en"));
let err = validate(&form).unwrap_err();
assert_eq!(err, "config key is required");
fn validate_config_set_non_empty_is_ok() {
let form = Form::config_set(editor_with("locale"), editor_with("en"));
match validate(&form).unwrap() {
FormSubmit::ConfigSet { key, value } => {
assert_eq!(key, "locale");
assert_eq!(value, "en");
_ => panic!("expected ConfigSet submit"),
fn validate_report_params_bad_from_date_is_error() {
let form = Form::report_params(
editor_with("not-a-date"),
editor_with("2024-12-31"),
assert!(err.starts_with("invalid from date:"), "got: {err}");
fn validate_report_params_bad_to_date_is_error() {
editor_with("2024-01-01"),
assert!(err.starts_with("invalid to date:"), "got: {err}");
fn validate_report_params_empty_chart_defaults_to_bar() {
FormSubmit::Report { chart, .. } => assert_eq!(chart, "bar"),
_ => panic!("expected Report submit"),
fn validate_report_params_good_coerces_dates_and_keeps_chart() {
editor_with("line"),
FormSubmit::Report {
kind,
from,
to,
chart,
} => {
assert_eq!(kind, ReportKind::Activity);
assert!(!from.is_empty());
assert!(!to.is_empty());
assert_eq!(chart, "line");
fn focused_editor_mut_targets_focused_field() {
let mut form = Form::config_set(editor_with("a"), editor_with("b"));
form.focused_editor_mut()
.expect("focused field")
.insert_char('x');
assert_eq!(form.fields[0].widget.value(), "ax");
.insert_char('y');
assert_eq!(form.fields[1].widget.value(), "by");
fn cycle_backward_on_three_field_form_goes_in_reverse() {
assert_eq!(form.focus, 0); // From
assert_eq!(form.focus, 2); // wraps back to Chart
assert_eq!(form.focus, 1); // To
fn commodity_create_form_has_two_fields() {
let form = Form::commodity_create(EditMode::Emacs);
assert_eq!(form.fields.len(), 2);
assert_eq!(form.fields[0].label, "Symbol");
assert_eq!(form.fields[1].label, "Name");
fn validate_commodity_create_empty_symbol_is_error() {
assert_eq!(err, "symbol is required");
fn validate_commodity_create_symbol_only_empty_name_is_error() {
let mut form = Form::commodity_create(EditMode::Emacs);
if let Widget::Text(ref mut ed) = form.fields[0].widget {
*ed = Editor::with_buffer(EditMode::Emacs, "USD".to_string());
assert_eq!(err, "name is required");
fn validate_commodity_create_with_name_is_ok() {
if let Widget::Text(ref mut ed) = form.fields[1].widget {
*ed = Editor::with_buffer(EditMode::Emacs, "US Dollar".to_string());
FormSubmit::CommodityCreate { symbol, name } => {
assert_eq!(symbol, "USD");
assert_eq!(name, "US Dollar");
other => panic!("expected CommodityCreate, got {other:?}"),
fn account_create_form_has_two_fields() {
let form = Form::account_create(EditMode::Emacs, vec![]);
assert_eq!(form.fields[0].label, "Name");
assert_eq!(form.fields[1].label, "Parent");
fn validate_account_create_empty_name_is_error() {
assert_eq!(err, "account name is required");
fn validate_account_create_with_name_no_parent_is_ok() {
let mut form = Form::account_create(EditMode::Emacs, vec![]);
*ed = Editor::with_buffer(EditMode::Emacs, "Expenses".to_string());
FormSubmit::AccountCreate { name, parent } => {
assert_eq!(name, "Expenses");
assert!(parent.is_none());
other => panic!("expected AccountCreate, got {other:?}"),
fn validate_account_create_with_parent_is_ok() {
let uuid = "550e8400-e29b-41d4-a716-446655440001";
let opts = vec![
SelectOption {
id: String::new(),
label: "(none)".to_string(),
},
id: uuid.to_string(),
label: "Root".to_string(),
];
let mut form = Form::account_create(EditMode::Emacs, opts);
*ed = Editor::with_buffer(EditMode::Emacs, "Sub".to_string());
if let Widget::Select(ref mut sw) = form.fields[1].widget {
sw.next();
assert_eq!(name, "Sub");
assert_eq!(parent.as_deref(), Some(uuid));
fn opt(id: &str, label: &str) -> SelectOption {
id: id.to_string(),
label: label.to_string(),
/// Type `text` into the focused row's amount sub-widget at column `col`.
fn type_amount(sw: &mut crate::widgets::SplitsWidget, col: usize, text: &str) {
use crate::widgets::FocusedSubWidget;
sw.col_focus = col;
if let Some(FocusedSubWidget::Amount(aw)) = sw.focused_subwidget_mut() {
for c in text.chars() {
aw.insert_char(c);
fn transaction_create_form_has_three_fields() {
let form = Form::transaction_create(EditMode::Emacs);
assert_eq!(form.fields.len(), 3);
assert_eq!(form.fields[0].label, "Date");
assert_eq!(form.fields[1].label, "Note");
assert_eq!(form.fields[2].label, "Splits");
fn validate_transaction_incomplete_row_is_error() {
assert!(err.contains("split row 1"), "row index in error: {err}");
fn validate_transaction_single_currency_row_is_ok() {
use crate::widgets::Widget;
use crate::widgets::splits::COL_VALUE;
let mut form = Form::transaction_create(EditMode::Emacs);
if let Widget::Splits(ref mut sw) = form.fields[2].widget {
sw.set_account_options(vec![opt("acc-1", "Cash"), opt("acc-2", "Food")]);
sw.set_commodity_options(vec![opt("c-1", "USD")]);
type_amount(sw, COL_VALUE, "50");
} else {
panic!("field 2 must be Splits");
FormSubmit::TransactionCreate { splits, .. } => {
assert_eq!(splits.len(), 1);
assert_eq!(splits[0].amount, "50");
assert_eq!(splits[0].from_commodity, splits[0].to_commodity);
assert!(splits[0].to_amount.is_none());
other => panic!("expected TransactionCreate, got {other:?}"),
fn validate_transaction_cross_currency_without_to_amount_is_error() {
use crate::widgets::splits::{COL_TO_COMM, COL_VALUE};
sw.set_commodity_options(vec![opt("c-1", "USD"), opt("c-2", "EUR")]);
sw.col_focus = COL_TO_COMM;
if let Some(FocusedSubWidget::Select(s)) = sw.focused_subwidget_mut() {
s.next();
assert!(
err.contains("to_amount"),
"cross-currency needs to_amount: {err}"
fn validate_transaction_cross_currency_with_to_amount_is_ok() {
use crate::widgets::splits::{COL_TO_AMOUNT, COL_TO_COMM, COL_VALUE};
type_amount(sw, COL_TO_AMOUNT, "45");
assert_ne!(splits[0].from_commodity, splits[0].to_commodity);
assert_eq!(splits[0].to_amount.as_deref(), Some("45"));