1
//! Tests for key-event translation.
2

            
3
use super::*;
4
use crate::widgets::EditMode;
5
use sqlx::types::Uuid;
6

            
7
61
fn key(code: KeyCode) -> KeyEvent {
8
61
    KeyEvent::new(code, KeyModifiers::NONE)
9
61
}
10

            
11
5
fn ctrl(code: KeyCode) -> KeyEvent {
12
5
    KeyEvent::new(code, KeyModifiers::CONTROL)
13
5
}
14

            
15
32
fn app() -> App {
16
32
    App::new(Uuid::new_v4(), EditMode::Emacs)
17
32
}
18

            
19
#[test]
20
1
fn input_modal_types_q_but_help_closes_on_q() {
21
    use crate::form::Form;
22
    // Help (display) closes on q.
23
1
    let mut help = app();
24
1
    help.overlays.push(Modal::Help);
25
1
    assert_eq!(
26
1
        translate(&help, key(KeyCode::Char('q'))),
27
        Some(Intent::CloseTopmost)
28
    );
29
    // Form (input) types q; only Esc closes it.
30
1
    let mut input = app();
31
1
    input.overlays.push(Modal::Form(Form::config_set(
32
1
        crate::widgets::Editor::new(EditMode::Emacs),
33
1
        crate::widgets::Editor::new(EditMode::Emacs),
34
1
    )));
35
1
    assert_eq!(
36
1
        translate(&input, key(KeyCode::Char('q'))),
37
        Some(Intent::InsertChar('q'))
38
    );
39
1
    assert_eq!(
40
1
        translate(&input, key(KeyCode::Esc)),
41
        Some(Intent::CloseTopmost)
42
    );
43
1
}
44

            
45
#[test]
46
1
fn tab_layer_handles_digit_shortcuts() {
47
1
    assert_eq!(
48
1
        translate(&app(), key(KeyCode::Char('1'))),
49
        Some(Intent::SelectTab(Tab::Accounts))
50
    );
51
1
}
52

            
53
#[test]
54
1
fn tab_layer_handles_tab_key() {
55
1
    assert_eq!(translate(&app(), key(KeyCode::Tab)), Some(Intent::NextTab));
56
1
}
57

            
58
#[test]
59
1
fn modal_layer_closes_and_routes_input() {
60
1
    let mut a = app();
61
1
    a.overlays.push(crate::modal::Modal::Help);
62
    // Esc/q close the modal stack.
63
1
    assert_eq!(translate(&a, key(KeyCode::Esc)), Some(Intent::CloseTopmost));
64
1
    assert_eq!(
65
1
        translate(&a, key(KeyCode::Char('q'))),
66
        Some(Intent::CloseTopmost)
67
    );
68
    // Other keys are routed to the modal (input modals need typing, Tab to
69
    // cycle focus, Enter to submit).
70
1
    assert_eq!(
71
1
        translate(&a, key(KeyCode::Char('1'))),
72
        Some(Intent::InsertChar('1'))
73
    );
74
1
    assert_eq!(translate(&a, key(KeyCode::Tab)), Some(Intent::NextTab));
75
1
    assert_eq!(
76
1
        translate(&a, key(KeyCode::Enter)),
77
        Some(Intent::SubmitCommandLine)
78
    );
79
1
    assert_eq!(
80
1
        translate(&a, key(KeyCode::Backspace)),
81
        Some(Intent::DeleteBackward)
82
    );
83
1
}
84

            
85
#[test]
86
1
fn cmdline_insert_accepts_ctrl_a_as_home() {
87
1
    let mut a = app();
88
1
    a.open_command_line();
89
1
    assert_eq!(
90
1
        translate(&a, ctrl(KeyCode::Char('a'))),
91
        Some(Intent::MoveHome)
92
    );
93
1
}
94

            
95
#[test]
96
1
fn cmdline_insert_accepts_plain_characters() {
97
1
    let mut a = app();
98
1
    a.open_command_line();
99
1
    assert_eq!(
100
1
        translate(&a, key(KeyCode::Char('q'))),
101
        Some(Intent::InsertChar('q'))
102
    );
103
1
}
104

            
105
#[test]
106
1
fn cmdline_insert_tab_triggers_completion() {
107
1
    let mut a = app();
108
1
    a.open_command_line();
109
1
    assert_eq!(
110
1
        translate(&a, key(KeyCode::Tab)),
111
        Some(Intent::CompleteCommandLine)
112
    );
113
1
}
114

            
115
#[test]
116
1
fn cmdline_vim_normal_routes_motion_keys() {
117
1
    let mut a = app();
118
1
    a.set_edit_mode(EditMode::Vim);
119
1
    a.open_command_line();
120
1
    a.cmdline.editor.enter_normal_mode();
121
1
    assert_eq!(
122
1
        translate(&a, key(KeyCode::Char('h'))),
123
        Some(Intent::Vim(VimAction::MoveLeft))
124
    );
125
1
    assert_eq!(
126
1
        translate(&a, key(KeyCode::Char('$'))),
127
        Some(Intent::Vim(VimAction::MoveEnd))
128
    );
129
1
}
130

            
131
#[test]
132
1
fn unknown_key_returns_none() {
133
1
    assert_eq!(translate(&app(), key(KeyCode::F(12))), None);
134
1
}
135

            
136
#[test]
137
1
fn console_tab_unfocused_focuses_on_i_and_enter() {
138
1
    let mut a = app();
139
1
    a.active_tab = Tab::Console;
140
1
    assert_eq!(
141
1
        translate(&a, key(KeyCode::Char('i'))),
142
        Some(Intent::ConsoleFocus)
143
    );
144
1
    assert_eq!(
145
1
        translate(&a, key(KeyCode::Enter)),
146
        Some(Intent::ConsoleFocus)
147
    );
148
1
}
149

            
150
#[test]
151
1
fn console_tab_unfocused_still_navigates() {
152
1
    let mut a = app();
153
1
    a.active_tab = Tab::Console;
154
1
    assert_eq!(translate(&a, key(KeyCode::Char('q'))), Some(Intent::Quit));
155
1
    assert_eq!(translate(&a, key(KeyCode::Tab)), Some(Intent::NextTab));
156
1
    assert_eq!(
157
1
        translate(&a, key(KeyCode::Char('1'))),
158
        Some(Intent::SelectTab(Tab::Accounts))
159
    );
160
1
    assert_eq!(
161
1
        translate(&a, key(KeyCode::Char('6'))),
162
        Some(Intent::SelectTab(Tab::Console))
163
    );
164
1
    assert_eq!(
165
1
        translate(&a, key(KeyCode::Char(':'))),
166
        Some(Intent::OpenCommandLine)
167
    );
168
1
}
169

            
170
#[test]
171
1
fn focus_keys_are_inert_on_other_tabs() {
172
1
    let mut a = app();
173
1
    a.active_tab = Tab::Accounts;
174
1
    assert_eq!(translate(&a, key(KeyCode::Char('i'))), None);
175
1
    assert_eq!(translate(&a, key(KeyCode::Enter)), None);
176
1
}
177

            
178
#[test]
179
1
fn console_focused_ctrl_c_maps_to_interrupt() {
180
1
    let mut a = app();
181
1
    a.console_focused = true;
182
1
    assert_eq!(
183
1
        translate(&a, ctrl(KeyCode::Char('c'))),
184
        Some(Intent::ConsoleInterrupt)
185
    );
186
1
}
187

            
188
#[test]
189
1
fn console_stays_emacs_even_after_vim_toggle() {
190
1
    let mut a = app();
191
1
    a.set_edit_mode(EditMode::Vim);
192
1
    a.console_focused = true;
193
    // The console input editor is built Emacs-only, so a vim motion
194
    // key is a literal insert here (never a vim-normal action) and
195
    // Esc blurs the console rather than entering vim-normal.
196
1
    assert_eq!(a.console.input.mode(), EditMode::Emacs);
197
1
    assert_eq!(
198
1
        translate(&a, key(KeyCode::Char('h'))),
199
        Some(Intent::InsertChar('h'))
200
    );
201
1
    assert_eq!(translate(&a, key(KeyCode::Esc)), Some(Intent::ConsoleBlur));
202
1
}
203

            
204
#[test]
205
1
fn console_focused_routes_editing_and_control_keys() {
206
1
    let mut a = app();
207
1
    a.console_focused = true;
208
1
    assert_eq!(
209
1
        translate(&a, key(KeyCode::Char('x'))),
210
        Some(Intent::InsertChar('x'))
211
    );
212
1
    assert_eq!(
213
1
        translate(&a, key(KeyCode::Enter)),
214
        Some(Intent::ConsoleSubmit)
215
    );
216
1
    assert_eq!(translate(&a, key(KeyCode::Esc)), Some(Intent::ConsoleBlur));
217
1
    assert_eq!(
218
1
        translate(&a, ctrl(KeyCode::Char('c'))),
219
        Some(Intent::ConsoleInterrupt)
220
    );
221
1
    assert_eq!(
222
1
        translate(&a, key(KeyCode::Up)),
223
        Some(Intent::ConsoleHistoryPrev)
224
    );
225
1
    assert_eq!(
226
1
        translate(&a, key(KeyCode::Down)),
227
        Some(Intent::ConsoleHistoryNext)
228
    );
229
1
    assert_eq!(
230
1
        translate(&a, key(KeyCode::Backspace)),
231
        Some(Intent::DeleteBackward)
232
    );
233
1
}
234

            
235
#[test]
236
1
fn console_focused_tab_cycles_pane() {
237
1
    let mut a = app();
238
1
    a.console_focused = true;
239
1
    assert_eq!(
240
1
        translate(&a, key(KeyCode::Tab)),
241
        Some(Intent::ConsoleCyclePane)
242
    );
243
1
    assert_eq!(
244
1
        translate(&a, key(KeyCode::BackTab)),
245
        Some(Intent::ConsoleCyclePane)
246
    );
247
1
}
248

            
249
#[test]
250
1
fn scrollback_pane_routes_scroll_keys() {
251
1
    let mut a = app();
252
1
    a.console_focused = true;
253
1
    a.console.panes.focus(PaneId::Scrollback);
254
1
    assert_eq!(translate(&a, key(KeyCode::Up)), Some(Intent::ScrollLineUp));
255
1
    assert_eq!(
256
1
        translate(&a, key(KeyCode::Down)),
257
        Some(Intent::ScrollLineDown)
258
    );
259
1
    assert_eq!(
260
1
        translate(&a, key(KeyCode::PageUp)),
261
        Some(Intent::ScrollPageUp)
262
    );
263
1
    assert_eq!(
264
1
        translate(&a, key(KeyCode::PageDown)),
265
        Some(Intent::ScrollPageDown)
266
    );
267
1
    assert_eq!(translate(&a, key(KeyCode::Esc)), Some(Intent::ConsoleBlur));
268
1
    assert_eq!(
269
1
        translate(&a, ctrl(KeyCode::Char('c'))),
270
        Some(Intent::ConsoleInterrupt)
271
    );
272
1
}
273

            
274
#[test]
275
1
fn scrollback_pane_letter_key_returns_none() {
276
1
    let mut a = app();
277
1
    a.console_focused = true;
278
1
    a.console.panes.focus(PaneId::Scrollback);
279
1
    assert_eq!(translate(&a, key(KeyCode::Char('x'))), None);
280
1
    assert_eq!(translate(&a, key(KeyCode::Enter)), None);
281
1
}
282

            
283
#[test]
284
1
fn scrollback_pane_tab_cycles_back_to_prompt() {
285
1
    let mut a = app();
286
1
    a.console_focused = true;
287
1
    a.console.panes.focus(PaneId::Scrollback);
288
1
    assert_eq!(
289
1
        translate(&a, key(KeyCode::Tab)),
290
        Some(Intent::ConsoleCyclePane)
291
    );
292
1
}
293

            
294
4
fn app_with_select_modal() -> App {
295
    use crate::form::{Field, Form, FormKind};
296
    use crate::widgets::{SelectWidget, Widget};
297
4
    let mut a = app();
298
4
    let form = Form {
299
4
        fields: vec![Field {
300
4
            label: "account",
301
4
            widget: Widget::Select(SelectWidget::new()),
302
4
        }],
303
4
        focus: 0,
304
4
        kind: FormKind::ConfigSet,
305
4
        entity_id: None,
306
4
    };
307
4
    a.overlays.push(Modal::Form(form));
308
4
    a
309
4
}
310

            
311
#[test]
312
1
fn select_focused_up_routes_to_select_prev() {
313
1
    let a = app_with_select_modal();
314
1
    assert_eq!(translate(&a, key(KeyCode::Up)), Some(Intent::SelectPrev));
315
1
}
316

            
317
#[test]
318
1
fn select_focused_down_routes_to_select_next() {
319
1
    let a = app_with_select_modal();
320
1
    assert_eq!(translate(&a, key(KeyCode::Down)), Some(Intent::SelectNext));
321
1
}
322

            
323
#[test]
324
1
fn select_focused_enter_routes_to_select_confirm_not_submit() {
325
1
    let a = app_with_select_modal();
326
1
    assert_eq!(
327
1
        translate(&a, key(KeyCode::Enter)),
328
        Some(Intent::SelectConfirm)
329
    );
330
1
}
331

            
332
#[test]
333
1
fn text_focused_enter_still_maps_to_submit() {
334
    use crate::modal::Form;
335
1
    let mut a = app();
336
1
    a.overlays.push(Modal::Form(Form::config_set(
337
1
        crate::widgets::Editor::new(EditMode::Emacs),
338
1
        crate::widgets::Editor::new(EditMode::Emacs),
339
1
    )));
340
1
    assert_eq!(
341
1
        translate(&a, key(KeyCode::Enter)),
342
        Some(Intent::SubmitCommandLine)
343
    );
344
1
}
345

            
346
#[test]
347
1
fn select_focused_tab_still_cycles_outer_fields() {
348
1
    let a = app_with_select_modal();
349
1
    assert_eq!(translate(&a, key(KeyCode::Tab)), Some(Intent::NextTab));
350
1
}
351

            
352
/// An app whose topmost modal is a transaction-create form with the Splits
353
/// field focused and the focused column set to `col`.
354
7
fn app_with_splits_modal(col: usize) -> App {
355
    use crate::form::Form;
356
    use crate::widgets::Widget;
357
7
    let mut a = app();
358
7
    let mut form = Form::transaction_create(EditMode::Emacs);
359
7
    form.focus = 2; // the Splits field
360
7
    if let Widget::Splits(ref mut sw) = form.fields[2].widget {
361
7
        sw.col_focus = col;
362
7
    }
363
7
    a.overlays.push(Modal::Form(form));
364
7
    a
365
7
}
366

            
367
#[test]
368
1
fn splits_select_col_down_routes_to_select_next() {
369
    use crate::widgets::splits::COL_FROM;
370
1
    let a = app_with_splits_modal(COL_FROM);
371
    // Widget-first: on a Select column, Down drives the Select options.
372
1
    assert_eq!(translate(&a, key(KeyCode::Down)), Some(Intent::SelectNext));
373
1
    assert_eq!(translate(&a, key(KeyCode::Up)), Some(Intent::SelectPrev));
374
1
}
375

            
376
#[test]
377
1
fn splits_amount_col_down_routes_to_row_next() {
378
    use crate::widgets::splits::COL_VALUE;
379
1
    let a = app_with_splits_modal(COL_VALUE);
380
    // On an Amount column, Up/Down navigate rows, not Select options.
381
1
    assert_eq!(translate(&a, key(KeyCode::Down)), Some(Intent::RowNext));
382
1
    assert_eq!(translate(&a, key(KeyCode::Up)), Some(Intent::RowPrev));
383
1
}
384

            
385
#[test]
386
1
fn splits_tab_navigates_columns_mid_row() {
387
    use crate::widgets::splits::COL_TO;
388
    // Mid-row (not at a boundary): Tab/BackTab walk cells, not outer fields.
389
1
    let a = app_with_splits_modal(COL_TO);
390
1
    assert_eq!(translate(&a, key(KeyCode::Tab)), Some(Intent::ColNext));
391
1
    assert_eq!(translate(&a, key(KeyCode::BackTab)), Some(Intent::ColPrev));
392
1
}
393

            
394
#[test]
395
1
fn splits_backtab_at_first_cell_yields_to_outer_form() {
396
    use crate::widgets::splits::COL_FROM;
397
    // (row 0, col 0) is the first cell — BackTab yields to the outer cycle.
398
1
    let a = app_with_splits_modal(COL_FROM);
399
1
    assert_eq!(translate(&a, key(KeyCode::Tab)), Some(Intent::ColNext));
400
1
    assert_eq!(
401
1
        translate(&a, key(KeyCode::BackTab)),
402
        Some(Intent::PreviousTab)
403
    );
404
1
}
405

            
406
#[test]
407
1
fn splits_tab_at_last_cell_yields_to_outer_form() {
408
    use crate::widgets::splits::COL_TO_AMOUNT;
409
    // (last row, last col) is the last cell — Tab yields to the outer cycle.
410
1
    let a = app_with_splits_modal(COL_TO_AMOUNT);
411
1
    assert_eq!(translate(&a, key(KeyCode::Tab)), Some(Intent::NextTab));
412
1
    assert_eq!(translate(&a, key(KeyCode::BackTab)), Some(Intent::ColPrev));
413
1
}
414

            
415
#[test]
416
1
fn splits_enter_submits_the_form() {
417
    use crate::widgets::splits::COL_FROM;
418
    // Enter on a split Select column submits (no dropdown to confirm).
419
1
    let a = app_with_splits_modal(COL_FROM);
420
1
    assert_eq!(
421
1
        translate(&a, key(KeyCode::Enter)),
422
        Some(Intent::SubmitCommandLine)
423
    );
424
1
}
425

            
426
#[test]
427
1
fn splits_add_and_remove_row_shortcuts() {
428
    use crate::widgets::splits::COL_VALUE;
429
1
    let a = app_with_splits_modal(COL_VALUE);
430
1
    assert_eq!(
431
1
        translate(&a, key(KeyCode::Char('+'))),
432
        Some(Intent::SplitAddRow)
433
    );
434
1
    assert_eq!(
435
1
        translate(&a, key(KeyCode::Char('-'))),
436
        Some(Intent::SplitRemoveRow)
437
    );
438
1
    assert_eq!(
439
1
        translate(&a, ctrl(KeyCode::Char('n'))),
440
        Some(Intent::SplitAddRow)
441
    );
442
1
}