1
//! Tests for the generic Form type and validate function.
2

            
3
use super::*;
4
use crate::widgets::{EditMode, Editor, SelectOption};
5

            
6
17
fn empty_editor() -> Editor {
7
17
    Editor::new(EditMode::Emacs)
8
17
}
9

            
10
14
fn editor_with(s: &str) -> Editor {
11
14
    Editor::with_buffer(EditMode::Emacs, s.to_string())
12
14
}
13

            
14
#[test]
15
1
fn cycle_forward_wraps_two_field_form() {
16
1
    let mut form = Form::config_set(empty_editor(), empty_editor());
17
1
    assert_eq!(form.focus, 0);
18
1
    form.cycle(true);
19
1
    assert_eq!(form.focus, 1);
20
1
    form.cycle(true);
21
1
    assert_eq!(form.focus, 0);
22
1
}
23

            
24
#[test]
25
1
fn cycle_backward_wraps_two_field_form() {
26
1
    let mut form = Form::config_set(empty_editor(), empty_editor());
27
1
    assert_eq!(form.focus, 0);
28
1
    form.cycle(false);
29
1
    assert_eq!(form.focus, 1);
30
1
    form.cycle(false);
31
1
    assert_eq!(form.focus, 0);
32
1
}
33

            
34
#[test]
35
1
fn cycle_forward_wraps_three_field_form() {
36
    use crate::tabs::reports::ReportKind;
37
1
    let mut form = Form::report_params(
38
1
        empty_editor(),
39
1
        empty_editor(),
40
1
        empty_editor(),
41
1
        ReportKind::Activity,
42
    );
43
1
    assert_eq!(form.focus, 0);
44
1
    form.cycle(true);
45
1
    assert_eq!(form.focus, 1);
46
1
    form.cycle(true);
47
1
    assert_eq!(form.focus, 2);
48
1
    form.cycle(true);
49
1
    assert_eq!(form.focus, 0);
50
1
}
51

            
52
#[test]
53
1
fn cycle_backward_wraps_three_field_form() {
54
    use crate::tabs::reports::ReportKind;
55
1
    let mut form = Form::report_params(
56
1
        empty_editor(),
57
1
        empty_editor(),
58
1
        empty_editor(),
59
1
        ReportKind::Activity,
60
    );
61
1
    form.cycle(false);
62
1
    assert_eq!(form.focus, 2);
63
1
    form.cycle(false);
64
1
    assert_eq!(form.focus, 1);
65
1
    form.cycle(false);
66
1
    assert_eq!(form.focus, 0);
67
1
}
68

            
69
#[test]
70
1
fn validate_config_set_empty_key_is_error() {
71
1
    let form = Form::config_set(empty_editor(), editor_with("en"));
72
1
    let err = validate(&form).unwrap_err();
73
1
    assert_eq!(err, "config key is required");
74
1
}
75

            
76
#[test]
77
1
fn validate_config_set_non_empty_is_ok() {
78
1
    let form = Form::config_set(editor_with("locale"), editor_with("en"));
79
1
    match validate(&form).unwrap() {
80
1
        FormSubmit::ConfigSet { key, value } => {
81
1
            assert_eq!(key, "locale");
82
1
            assert_eq!(value, "en");
83
        }
84
        _ => panic!("expected ConfigSet submit"),
85
    }
86
1
}
87

            
88
#[test]
89
1
fn validate_report_params_bad_from_date_is_error() {
90
    use crate::tabs::reports::ReportKind;
91
1
    let form = Form::report_params(
92
1
        editor_with("not-a-date"),
93
1
        editor_with("2024-12-31"),
94
1
        empty_editor(),
95
1
        ReportKind::Activity,
96
    );
97
1
    let err = validate(&form).unwrap_err();
98
1
    assert!(err.starts_with("invalid from date:"), "got: {err}");
99
1
}
100

            
101
#[test]
102
1
fn validate_report_params_bad_to_date_is_error() {
103
    use crate::tabs::reports::ReportKind;
104
1
    let form = Form::report_params(
105
1
        editor_with("2024-01-01"),
106
1
        editor_with("not-a-date"),
107
1
        empty_editor(),
108
1
        ReportKind::Activity,
109
    );
110
1
    let err = validate(&form).unwrap_err();
111
1
    assert!(err.starts_with("invalid to date:"), "got: {err}");
112
1
}
113

            
114
#[test]
115
1
fn validate_report_params_empty_chart_defaults_to_bar() {
116
    use crate::tabs::reports::ReportKind;
117
1
    let form = Form::report_params(
118
1
        editor_with("2024-01-01"),
119
1
        editor_with("2024-12-31"),
120
1
        empty_editor(),
121
1
        ReportKind::Activity,
122
    );
123
1
    match validate(&form).unwrap() {
124
1
        FormSubmit::Report { chart, .. } => assert_eq!(chart, "bar"),
125
        _ => panic!("expected Report submit"),
126
    }
127
1
}
128

            
129
#[test]
130
1
fn validate_report_params_good_coerces_dates_and_keeps_chart() {
131
    use crate::tabs::reports::ReportKind;
132
1
    let form = Form::report_params(
133
1
        editor_with("2024-01-01"),
134
1
        editor_with("2024-12-31"),
135
1
        editor_with("line"),
136
1
        ReportKind::Activity,
137
    );
138
1
    match validate(&form).unwrap() {
139
        FormSubmit::Report {
140
1
            kind,
141
1
            from,
142
1
            to,
143
1
            chart,
144
        } => {
145
1
            assert_eq!(kind, ReportKind::Activity);
146
1
            assert!(!from.is_empty());
147
1
            assert!(!to.is_empty());
148
1
            assert_eq!(chart, "line");
149
        }
150
        _ => panic!("expected Report submit"),
151
    }
152
1
}
153

            
154
#[test]
155
1
fn focused_editor_mut_targets_focused_field() {
156
1
    let mut form = Form::config_set(editor_with("a"), editor_with("b"));
157
1
    form.focused_editor_mut()
158
1
        .expect("focused field")
159
1
        .insert_char('x');
160
1
    assert_eq!(form.fields[0].widget.value(), "ax");
161
1
    form.cycle(true);
162
1
    form.focused_editor_mut()
163
1
        .expect("focused field")
164
1
        .insert_char('y');
165
1
    assert_eq!(form.fields[1].widget.value(), "by");
166
1
}
167

            
168
#[test]
169
1
fn cycle_backward_on_three_field_form_goes_in_reverse() {
170
    use crate::tabs::reports::ReportKind;
171
1
    let mut form = Form::report_params(
172
1
        empty_editor(),
173
1
        empty_editor(),
174
1
        empty_editor(),
175
1
        ReportKind::Activity,
176
    );
177
1
    assert_eq!(form.focus, 0); // From
178
1
    form.cycle(false);
179
1
    assert_eq!(form.focus, 2); // wraps back to Chart
180
1
    form.cycle(false);
181
1
    assert_eq!(form.focus, 1); // To
182
1
}
183

            
184
#[test]
185
1
fn commodity_create_form_has_two_fields() {
186
1
    let form = Form::commodity_create(EditMode::Emacs);
187
1
    assert_eq!(form.fields.len(), 2);
188
1
    assert_eq!(form.fields[0].label, "Symbol");
189
1
    assert_eq!(form.fields[1].label, "Name");
190
1
}
191

            
192
#[test]
193
1
fn validate_commodity_create_empty_symbol_is_error() {
194
1
    let form = Form::commodity_create(EditMode::Emacs);
195
1
    let err = validate(&form).unwrap_err();
196
1
    assert_eq!(err, "symbol is required");
197
1
}
198

            
199
#[test]
200
1
fn validate_commodity_create_symbol_only_empty_name_is_error() {
201
1
    let mut form = Form::commodity_create(EditMode::Emacs);
202
1
    if let Widget::Text(ref mut ed) = form.fields[0].widget {
203
1
        *ed = Editor::with_buffer(EditMode::Emacs, "USD".to_string());
204
1
    }
205
1
    let err = validate(&form).unwrap_err();
206
1
    assert_eq!(err, "name is required");
207
1
}
208

            
209
#[test]
210
1
fn validate_commodity_create_with_name_is_ok() {
211
1
    let mut form = Form::commodity_create(EditMode::Emacs);
212
1
    if let Widget::Text(ref mut ed) = form.fields[0].widget {
213
1
        *ed = Editor::with_buffer(EditMode::Emacs, "USD".to_string());
214
1
    }
215
1
    if let Widget::Text(ref mut ed) = form.fields[1].widget {
216
1
        *ed = Editor::with_buffer(EditMode::Emacs, "US Dollar".to_string());
217
1
    }
218
1
    match validate(&form).unwrap() {
219
1
        FormSubmit::CommodityCreate { symbol, name } => {
220
1
            assert_eq!(symbol, "USD");
221
1
            assert_eq!(name, "US Dollar");
222
        }
223
        other => panic!("expected CommodityCreate, got {other:?}"),
224
    }
225
1
}
226

            
227
#[test]
228
1
fn account_create_form_has_two_fields() {
229
1
    let form = Form::account_create(EditMode::Emacs, vec![]);
230
1
    assert_eq!(form.fields.len(), 2);
231
1
    assert_eq!(form.fields[0].label, "Name");
232
1
    assert_eq!(form.fields[1].label, "Parent");
233
1
}
234

            
235
#[test]
236
1
fn validate_account_create_empty_name_is_error() {
237
1
    let form = Form::account_create(EditMode::Emacs, vec![]);
238
1
    let err = validate(&form).unwrap_err();
239
1
    assert_eq!(err, "account name is required");
240
1
}
241

            
242
#[test]
243
1
fn validate_account_create_with_name_no_parent_is_ok() {
244
1
    let mut form = Form::account_create(EditMode::Emacs, vec![]);
245
1
    if let Widget::Text(ref mut ed) = form.fields[0].widget {
246
1
        *ed = Editor::with_buffer(EditMode::Emacs, "Expenses".to_string());
247
1
    }
248
1
    match validate(&form).unwrap() {
249
1
        FormSubmit::AccountCreate { name, parent } => {
250
1
            assert_eq!(name, "Expenses");
251
1
            assert!(parent.is_none());
252
        }
253
        other => panic!("expected AccountCreate, got {other:?}"),
254
    }
255
1
}
256

            
257
#[test]
258
1
fn validate_account_create_with_parent_is_ok() {
259
1
    let uuid = "550e8400-e29b-41d4-a716-446655440001";
260
1
    let opts = vec![
261
1
        SelectOption {
262
1
            id: String::new(),
263
1
            label: "(none)".to_string(),
264
1
        },
265
1
        SelectOption {
266
1
            id: uuid.to_string(),
267
1
            label: "Root".to_string(),
268
1
        },
269
    ];
270
1
    let mut form = Form::account_create(EditMode::Emacs, opts);
271
1
    if let Widget::Text(ref mut ed) = form.fields[0].widget {
272
1
        *ed = Editor::with_buffer(EditMode::Emacs, "Sub".to_string());
273
1
    }
274
1
    if let Widget::Select(ref mut sw) = form.fields[1].widget {
275
1
        sw.next();
276
1
    }
277
1
    match validate(&form).unwrap() {
278
1
        FormSubmit::AccountCreate { name, parent } => {
279
1
            assert_eq!(name, "Sub");
280
1
            assert_eq!(parent.as_deref(), Some(uuid));
281
        }
282
        other => panic!("expected AccountCreate, got {other:?}"),
283
    }
284
1
}
285

            
286
11
fn opt(id: &str, label: &str) -> SelectOption {
287
11
    SelectOption {
288
11
        id: id.to_string(),
289
11
        label: label.to_string(),
290
11
    }
291
11
}
292

            
293
/// Type `text` into the focused row's amount sub-widget at column `col`.
294
4
fn type_amount(sw: &mut crate::widgets::SplitsWidget, col: usize, text: &str) {
295
    use crate::widgets::FocusedSubWidget;
296
4
    sw.col_focus = col;
297
4
    if let Some(FocusedSubWidget::Amount(aw)) = sw.focused_subwidget_mut() {
298
8
        for c in text.chars() {
299
8
            aw.insert_char(c);
300
8
        }
301
    }
302
4
}
303

            
304
#[test]
305
1
fn transaction_create_form_has_three_fields() {
306
1
    let form = Form::transaction_create(EditMode::Emacs);
307
1
    assert_eq!(form.fields.len(), 3);
308
1
    assert_eq!(form.fields[0].label, "Date");
309
1
    assert_eq!(form.fields[1].label, "Note");
310
1
    assert_eq!(form.fields[2].label, "Splits");
311
1
}
312

            
313
#[test]
314
1
fn validate_transaction_incomplete_row_is_error() {
315
1
    let form = Form::transaction_create(EditMode::Emacs);
316
1
    let err = validate(&form).unwrap_err();
317
1
    assert!(err.contains("split row 1"), "row index in error: {err}");
318
1
}
319

            
320
#[test]
321
1
fn validate_transaction_single_currency_row_is_ok() {
322
    use crate::widgets::Widget;
323
    use crate::widgets::splits::COL_VALUE;
324
1
    let mut form = Form::transaction_create(EditMode::Emacs);
325
1
    if let Widget::Splits(ref mut sw) = form.fields[2].widget {
326
1
        sw.set_account_options(vec![opt("acc-1", "Cash"), opt("acc-2", "Food")]);
327
1
        sw.set_commodity_options(vec![opt("c-1", "USD")]);
328
1
        type_amount(sw, COL_VALUE, "50");
329
1
    } else {
330
        panic!("field 2 must be Splits");
331
    }
332
1
    match validate(&form).unwrap() {
333
1
        FormSubmit::TransactionCreate { splits, .. } => {
334
1
            assert_eq!(splits.len(), 1);
335
1
            assert_eq!(splits[0].amount, "50");
336
1
            assert_eq!(splits[0].from_commodity, splits[0].to_commodity);
337
1
            assert!(splits[0].to_amount.is_none());
338
        }
339
        other => panic!("expected TransactionCreate, got {other:?}"),
340
    }
341
1
}
342

            
343
#[test]
344
1
fn validate_transaction_cross_currency_without_to_amount_is_error() {
345
    use crate::widgets::FocusedSubWidget;
346
    use crate::widgets::Widget;
347
    use crate::widgets::splits::{COL_TO_COMM, COL_VALUE};
348
1
    let mut form = Form::transaction_create(EditMode::Emacs);
349
1
    if let Widget::Splits(ref mut sw) = form.fields[2].widget {
350
1
        sw.set_account_options(vec![opt("acc-1", "Cash"), opt("acc-2", "Food")]);
351
1
        sw.set_commodity_options(vec![opt("c-1", "USD"), opt("c-2", "EUR")]);
352
1
        sw.col_focus = COL_TO_COMM;
353
1
        if let Some(FocusedSubWidget::Select(s)) = sw.focused_subwidget_mut() {
354
1
            s.next();
355
1
        }
356
1
        type_amount(sw, COL_VALUE, "50");
357
    }
358
1
    let err = validate(&form).unwrap_err();
359
1
    assert!(
360
1
        err.contains("to_amount"),
361
        "cross-currency needs to_amount: {err}"
362
    );
363
1
}
364

            
365
#[test]
366
1
fn validate_transaction_cross_currency_with_to_amount_is_ok() {
367
    use crate::widgets::FocusedSubWidget;
368
    use crate::widgets::Widget;
369
    use crate::widgets::splits::{COL_TO_AMOUNT, COL_TO_COMM, COL_VALUE};
370
1
    let mut form = Form::transaction_create(EditMode::Emacs);
371
1
    if let Widget::Splits(ref mut sw) = form.fields[2].widget {
372
1
        sw.set_account_options(vec![opt("acc-1", "Cash"), opt("acc-2", "Food")]);
373
1
        sw.set_commodity_options(vec![opt("c-1", "USD"), opt("c-2", "EUR")]);
374
1
        sw.col_focus = COL_TO_COMM;
375
1
        if let Some(FocusedSubWidget::Select(s)) = sw.focused_subwidget_mut() {
376
1
            s.next();
377
1
        }
378
1
        type_amount(sw, COL_VALUE, "50");
379
1
        type_amount(sw, COL_TO_AMOUNT, "45");
380
    }
381
1
    match validate(&form).unwrap() {
382
1
        FormSubmit::TransactionCreate { splits, .. } => {
383
1
            assert_eq!(splits.len(), 1);
384
1
            assert_ne!(splits[0].from_commodity, splits[0].to_commodity);
385
1
            assert_eq!(splits[0].to_amount.as_deref(), Some("45"));
386
        }
387
        other => panic!("expected TransactionCreate, got {other:?}"),
388
    }
389
1
}