#[allow(clippy::wildcard_imports)]
use super::super::*;
#[allow(clippy::wildcard_imports)]
use super::helpers::*;
use std::fs;
#[test]
fn new_issue_form_without_a_cycle_joins_the_single_running_cycle() {
let (_tmp, mut app) = make_app();
app.store.config.cycles.insert(
"October 2026".into(),
crate::config::PlanningCycle {
description: None,
start_date: Some("2020-01-01".into()),
due_date: Some("2999-12-31".into()),
},
);
app.store.config.planning.default_cycle = None;
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for c in "Automatic".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Board), "the form submitted");
let issue = app
.store
.list()
.unwrap()
.into_iter()
.find(|issue| issue.title == "Automatic")
.expect("created issue");
assert_eq!(
issue.cycles,
Some(vec!["October 2026".to_string()]),
"the TUI form assigns the single running cycle"
);
}
#[test]
fn n_opens_form() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
assert!(matches!(app.mode, Mode::Form));
assert!(app.form.is_some());
}
#[test]
fn form_submit_creates_issue() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
if let Mode::Form = &app.mode {
for c in "My Issue".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
}
assert!(matches!(app.mode, Mode::Board));
assert!(
app.status_msg
.as_deref()
.is_some_and(|m| m.starts_with("Created ")),
"got {:?}",
app.status_msg
);
}
#[test]
fn form_submit_selects_created_issue() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
if let Mode::Form = &app.mode {
for c in "Selected Issue".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
}
assert!(matches!(app.mode, Mode::Board));
let card = app.board.get_selected_card().expect("selected card");
assert_eq!(card.title, "Selected Issue");
assert_eq!(app.board.active_col, 0);
}
#[test]
fn form_submit_selects_created_issue_in_assignee_segment() {
let (tmp, mut app) = make_app();
let dir = tmp.path().join("issues");
write_issue(&dir, "2026-01-01-a.md", 1, "Old", "todo", None);
let config = app.store.config.clone();
app.board = BoardState::build(&app.store, config.default_config());
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
if let Mode::Form = &app.mode {
for c in "New".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
}
app.form.as_mut().unwrap().assignee = "alice".to_string();
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Board));
let card = app.board.get_selected_card().expect("selected card");
assert_eq!(
card.title, "New",
"the just-created issue is selected, not the previously selected one"
);
assert!(card.filename.starts_with("alice/"), "got {}", card.filename);
}
#[test]
fn form_empty_title_rejected() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Form));
assert_eq!(app.status_msg.as_deref(), Some("Title is required"));
}
#[test]
fn form_esc_cancels() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
assert!(matches!(app.mode, Mode::Form));
let _ = app.handle_key(press(KeyCode::Esc)).unwrap();
assert!(matches!(app.mode, Mode::Board));
}
#[test]
fn form_arrows_navigate_fields() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
use crate::ui::form::FormFocus::*;
assert_eq!(form_focus(&app), Title);
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
assert_eq!(form_focus(&app), Status);
let _ = app.handle_key(press(KeyCode::Up)).unwrap();
assert_eq!(form_focus(&app), Title);
for _ in 0..crate::ui::form::FormFocus::RING.len() {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
assert_eq!(form_focus(&app), Title, "Down wraps forward");
let _ = app.handle_key(press(KeyCode::Up)).unwrap();
assert_eq!(
form_focus(&app),
*crate::ui::form::FormFocus::RING
.last()
.expect("RING is non-empty"),
"Up wraps backward, from the last field in the ring"
);
}
#[test]
fn form_j_and_k_type_their_letters() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
let _ = app.handle_key(press(KeyCode::Char('j'))).unwrap();
let _ = app.handle_key(press(KeyCode::Char('k'))).unwrap();
let form = app.form.as_ref().unwrap();
assert_eq!(form.title, "jk", "j and k must type, not navigate");
assert_eq!(form.focus, crate::ui::form::FormFocus::Title);
}
#[test]
fn form_invalid_date_order_is_rejected() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for c in "Date check".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
for _ in 0..9 {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
assert!(matches!(app.mode, Mode::DatePicker { .. }));
let _ = app.handle_key(press(KeyCode::PageDown)).unwrap();
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Form));
assert_eq!(
app.status_msg.as_deref(),
Some("Due date must be >= start date")
);
}
#[test]
fn form_date_field_masked_typing_and_picker() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for c in "Dated".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
for _ in 0..9 {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
assert!(app.form.as_ref().unwrap().is_date_focus());
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::DatePicker { .. }));
let _ = app.handle_key(press(KeyCode::Esc)).unwrap();
assert!(matches!(app.mode, Mode::Form));
for c in "20260910".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
assert_eq!(app.form.as_ref().unwrap().start_date, "2026-09-10");
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Board));
let issues = app.store.list().unwrap();
let issue = issues
.iter()
.find(|i| i.title == "Dated")
.expect("the form created the issue");
assert_eq!(issue.start_date.as_deref(), Some("2026-09-10"));
}
#[test]
fn form_date_picker_select_and_clear() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for c in "Dated".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
for _ in 0..9 {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
assert!(matches!(app.mode, Mode::DatePicker { .. }));
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Form));
assert_eq!(app.form.as_ref().unwrap().start_date, today());
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
let _ = app.handle_key(press(KeyCode::Esc)).unwrap();
assert_eq!(
app.form.as_ref().unwrap().start_date,
today(),
"Esc leaves the date unchanged"
);
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
let _ = app.handle_key(press(KeyCode::Char('c'))).unwrap();
assert_eq!(
app.form.as_ref().unwrap().start_date,
"",
"c clears the date field"
);
}
#[test]
fn date_picker_day_click_changes_selected_date() {
let (_tmp, mut app) = make_app();
app.frame_size.set(Rect::new(0, 0, 80, 24));
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for _ in 0..9 {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
app.form.as_mut().unwrap().start_date = "2026-03-15".into();
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
assert!(matches!(app.mode, Mode::DatePicker { .. }));
let (x, y) = drawn_date_picker_day(&app, "2026-03-16");
let action = app.handle_mouse(down_left(x, y)).unwrap();
assert_eq!(
action,
Action::DatePickerHit(DatePickerHit::Day("2026-03-16".into()))
);
if let Action::DatePickerHit(DatePickerHit::Day(date)) = action {
app.date_picker.as_mut().unwrap().select_date_str(&date);
}
assert_eq!(app.date_picker.as_ref().unwrap().selected(), "2026-03-16");
}
#[test]
fn date_picker_ok_click_submits_selected_date() {
let (_tmp, mut app) = make_app();
app.frame_size.set(Rect::new(0, 0, 80, 24));
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for _ in 0..9 {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
app.form.as_mut().unwrap().start_date = "2026-03-15".into();
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
let (x, y) = drawn_date_picker_day(&app, "2026-03-16");
let _ = app.handle_mouse(down_left(x, y)).unwrap();
app.date_picker
.as_mut()
.unwrap()
.select_date_str("2026-03-16");
let picker = app.date_picker.as_ref().unwrap();
let rect = picker.popup_rect(app.frame_size.get());
let ok_x = rect.x + rect.width / 2;
let ok_y = rect.y + rect.height.saturating_sub(1);
let action = app.handle_mouse(down_left(ok_x, ok_y)).unwrap();
assert_eq!(action, Action::DatePickerHit(DatePickerHit::Submit));
let _ = app.handle_date_picker_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Form));
assert_eq!(app.form.as_ref().unwrap().start_date, "2026-03-16");
}
#[test]
fn form_non_date_field_still_opens_list_picker() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for _ in 0..6 {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
assert!(
app.form_picker.is_some(),
"cycles field opens the list picker"
);
assert!(!matches!(app.mode, Mode::DatePicker { .. }));
}
#[test]
fn form_picker_inline_create_cycle_persists_and_populates_field() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for c in "Picker cycle".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
for _ in 0..6 {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
assert!(app.form_picker.is_some(), "picker should open on cycles");
let _ = app.handle_key(press(KeyCode::Char('+'))).unwrap();
for c in "Cycle 42".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
let form = app.form.as_ref().unwrap();
assert!(
form.cycles.split(',').any(|c| c.trim() == "Cycle 42"),
"created cycle should populate form field, got {:?}",
form.cycles
);
assert!(
app.store.config.cycles.contains_key("Cycle 42"),
"new cycle should be persisted in config"
);
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Board));
let issue = app.store.list().unwrap().into_iter().next().unwrap();
assert!(
issue
.cycles
.unwrap_or_default()
.iter()
.any(|c| c == "Cycle 42")
);
}
#[test]
fn form_picker_selects_existing_label() {
let (tmp, mut app) = make_app();
let dir = tmp.path().join("issues");
let seed = "---\nnumber: 1\ntitle: \"Seed\"\nstatus: \"todo\"\nlabels: [\"bug\"]\npriority: \"medium\"\ncreated: \"2026-01-01\"\nupdated: \"2026-01-01\"\n---\n\nBody text.";
fs::write(dir.join("2026-01-01-seed.md"), seed).unwrap();
app.refresh_corpus().unwrap();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
for c in "Picker labels".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
for _ in 0..3 {
let _ = app.handle_key(press(KeyCode::Down)).unwrap();
}
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
let _ = app.handle_key(press(KeyCode::F(2))).unwrap();
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Board));
let issues = app.store.list().unwrap();
let created = issues
.into_iter()
.find(|i| i.title == "Picker labels")
.expect("created issue");
assert!(created.labels.iter().any(|l| l == "bug"));
}
#[test]
fn new_issue_form_is_the_form_the_board_key_opens() {
let (_tmp, mut by_key) = make_app();
by_key.mode = Mode::Board;
let _ = by_key.handle_key(press(KeyCode::Char('n'))).unwrap();
let (_tmp2, mut by_command) = make_app();
by_command.open_new_issue_form();
assert!(matches!(by_command.mode, Mode::Form));
assert!(matches!(by_key.mode, Mode::Form));
let key_form = by_key.form.as_ref().expect("n opens a form");
let cmd_form = by_command.form.as_ref().expect("the command opens a form");
assert_eq!(cmd_form.title, key_form.title);
assert_eq!(cmd_form.status, key_form.status);
assert_eq!(cmd_form.assignee, key_form.assignee);
assert_eq!(cmd_form.focus, key_form.focus);
}
#[test]
fn startup_tips_are_shown_only_by_bare_tix() {
let (_tmp, mut bare) = make_app();
assert!(bare.store.config.tips_on_startup);
Startup::Bare.prepare(&mut bare);
assert!(matches!(bare.mode, Mode::Tips { .. }));
assert!(bare.tips.is_some());
let (_tmp, mut new) = make_app();
assert!(new.store.config.tips_on_startup);
Startup::NewIssue.prepare(&mut new);
assert!(matches!(new.mode, Mode::Form));
assert!(new.tips.is_none(), "`tix new` must not open the tips modal");
let (_tmp, mut disabled) = make_app();
disabled.store.config.tips_on_startup = false;
Startup::Bare.prepare(&mut disabled);
assert!(matches!(disabled.mode, Mode::Board));
assert!(
disabled.tips.is_none(),
"a bare launch must respect tips_on_startup = false"
);
}
#[test]
fn every_printable_character_is_typeable_in_a_free_text_prompt() {
let (_tmp, app) = make_app();
let free_text = [
InputContext::Comment,
InputContext::Asset,
InputContext::Filter,
InputContext::MoveIntoDone,
];
for context in free_text {
for ch in ['q', 'Q'] {
let event = KeyEvent::new(KeyCode::Char(ch), crossterm::event::KeyModifiers::NONE);
assert_ne!(
app.bindings.action(context, event),
Some(crate::keybindings::TuiAction::Cancel),
"{context:?} must be able to type `{ch}`: it is bound to cancel, so \
pressing it leaves the mode and the rest of the input is applied as \
commands underneath"
);
}
}
for context in free_text {
let mut stolen: Vec<char> = Vec::new();
for code in 0x20u8..0x7f {
let ch = code as char;
let event = KeyEvent::new(KeyCode::Char(ch), crossterm::event::KeyModifiers::NONE);
if app.bindings.action(context, event).is_some() {
stolen.push(ch);
}
}
assert!(
stolen.is_empty(),
"{context:?} steals these printable characters, so they cannot be \
typed: {stolen:?}"
);
}
let (tmp, mut app) = make_app();
let dir = tmp.path().join("issues");
write_issue(&dir, "2026-01-01-a.md", 1, "A", "todo", None);
app.board = BoardState::build(&app.store, app.store.config.default_config());
open_detail(&mut app);
let _ = app.handle_key(press(KeyCode::Char('c'))).unwrap();
assert!(
matches!(app.mode, Mode::Comment { .. }),
"enter the comment prompt"
);
for c in "has a q in it".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
if let Mode::Comment { ref buffer, .. } = app.mode {
assert_eq!(
buffer, "has a q in it",
"every character reached the buffer"
);
} else {
panic!("the prompt must still be open, got {:?}", app.mode);
}
let _ = app.handle_key(press(KeyCode::Esc)).unwrap();
assert!(
!matches!(app.mode, Mode::Comment { .. }),
"esc must still cancel, got {:?}",
app.mode
);
}
#[test]
fn form_creation_without_a_cycle_selection_joins_the_default_cycle() {
let (_tmp, mut app) = make_app();
app.handle_key(press(KeyCode::Char('n'))).unwrap();
assert!(matches!(app.mode, Mode::Form));
app.form.as_mut().unwrap().title = "Fresh".into();
app.handle_key(press(KeyCode::Enter)).unwrap();
let issue = app
.store
.list()
.unwrap()
.into_iter()
.find(|issue| issue.title == "Fresh")
.expect("the form created the issue");
assert_eq!(issue.cycles, Some(vec!["Default".to_string()]));
}
#[test]
fn f1_opens_the_new_issue_mode_as_content() {
let (_tmp, mut app) = make_shell_app();
app.handle_key(press(KeyCode::F(1))).unwrap();
assert!(matches!(app.mode, Mode::Form));
assert_eq!(app.top_mode, TopMode::NewIssue);
let cells = draw_board(&app, 80, 24);
let row0 = flush_line(&cells, 80, 0);
assert!(
row0.starts_with("F1"),
"navigation still occupies the left column: {row0:?}"
);
assert!(
row0.contains("New Issue"),
"the form block is the mode content: {row0:?}"
);
}
#[test]
fn new_issue_defaults_to_todo_and_creates_there() {
let (_tmp, mut app) = make_shell_app();
app.handle_key(press(KeyCode::F(1))).unwrap();
let form = app.form.as_ref().unwrap();
assert_eq!(form.target, crate::ui::form::NewIssueTarget::Todo);
assert_eq!(
form.status, "todo",
"the default target seeds the status row"
);
type_text(&mut app, "Todo bound");
app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Board));
let issue = app
.store
.list()
.unwrap()
.into_iter()
.find(|i| i.title == "Todo bound")
.expect("the mode created the issue");
assert_eq!(issue.status.to_string(), "todo");
}
#[test]
fn new_issue_mode_rejects_an_empty_title() {
let (_tmp, mut app) = make_shell_app();
app.handle_key(press(KeyCode::F(1))).unwrap();
app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Form), "the mode stays open");
assert_eq!(app.status_msg.as_deref(), Some("Title is required"));
assert!(app.store.list().unwrap().is_empty(), "nothing was written");
}
#[test]
fn status_picker_options_are_the_board_columns_plus_an_off_board_current_value() {
let (_tmp, mut app) = make_app();
let columns = app.active_board_config().columns.clone();
assert_eq!(columns, vec!["todo", "in progress", "in review", "done"]);
app.handle_key(press(KeyCode::F(1))).unwrap();
let ids = |app: &App| -> Vec<String> {
app.picker_options(FormPickerField::Status)
.into_iter()
.map(|item| item.id)
.collect()
};
assert_eq!(
ids(&app),
columns,
"the options are the board's columns, in board order"
);
app.form.as_mut().unwrap().status = "legacy".to_string();
let mut expected = columns.clone();
expected.push("legacy".to_string());
assert_eq!(ids(&app), expected);
assert_eq!(
ids(&app)
.iter()
.filter(|id| id.as_str() == "legacy")
.count(),
1,
"the current value is appended once, not duplicated"
);
app.form.as_mut().unwrap().status = "done".to_string();
assert_eq!(ids(&app), columns);
}
#[test]
fn priority_picker_options_are_built_ins_then_priorities_in_use() {
let (tmp, mut app) = make_app();
let dir = tmp.path().join("issues");
fs::write(
dir.join("2026-01-01-urgent.md"),
"---\nnumber: 1\ntitle: \"Urgent\"\nstatus: \"todo\"\nlabels: []\n\
priority: \"urgent\"\ncreated: \"2026-01-01\"\nupdated: \"2026-01-01\"\n---\n\n\
Body.",
)
.unwrap();
app.refresh_corpus().unwrap();
app.handle_key(press(KeyCode::F(1))).unwrap();
let ids = |app: &App| -> Vec<String> {
app.picker_options(FormPickerField::Priority)
.into_iter()
.map(|item| item.id)
.collect()
};
assert_eq!(
ids(&app),
vec!["low", "medium", "high", "urgent"],
"built-ins first, then a priority the store uses"
);
app.form.as_mut().unwrap().priority = "medium".to_string();
assert_eq!(ids(&app), vec!["low", "medium", "high", "urgent"]);
app.form.as_mut().unwrap().priority = "blocker".to_string();
assert_eq!(
ids(&app),
vec!["low", "medium", "high", "urgent", "blocker"]
);
}
#[test]
fn the_other_form_rows_keep_their_behaviour() {
let (_tmp, mut app) = make_app();
app.handle_key(press(KeyCode::F(1))).unwrap();
type_text(&mut app, "Scoped");
app.handle_key(press(KeyCode::Down)).unwrap(); app.handle_key(press(KeyCode::Down)).unwrap(); app.handle_key(press(KeyCode::Down)).unwrap(); assert_eq!(
app.form.as_ref().unwrap().focus,
crate::ui::form::FormFocus::Labels
);
type_text(&mut app, "bug");
assert_eq!(
app.form.as_ref().unwrap().labels,
"bug",
"Labels still accepts typed text"
);
app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(
matches!(app.mode, Mode::Board),
"Enter still submits from a plain text row"
);
let issue = app
.store
.list()
.unwrap()
.into_iter()
.find(|i| i.title == "Scoped")
.expect("the mode created the issue");
assert_eq!(issue.labels, vec!["bug".to_string()]);
}
#[test]
fn clone_opens_the_new_issue_mode_and_creates_a_distinct_issue() {
let (tmp, mut app) = make_shell_app();
let dir = tmp.path().join("issues");
fs::write(
dir.join("2026-01-01-source.md"),
"---\nnumber: 7\ntitle: \"Source issue\"\nstatus: \"todo\"\nlabels: [\"bug\"]\n\
priority: \"high\"\ncreated: \"2026-01-01\"\nupdated: \"2026-01-01\"\n---\n\n\
Original body.",
)
.unwrap();
app.refresh_corpus().unwrap();
app.select_card_by_filename("2026-01-01-source.md");
app.handle_key(press(KeyCode::Char('C'))).unwrap();
assert!(
matches!(app.mode, Mode::Form),
"clone lands in the New Issue mode, got {:?}",
app.mode
);
assert_eq!(app.top_mode, TopMode::NewIssue);
let form = app.form.as_ref().unwrap();
assert_eq!(form.title, "Source issue");
assert_eq!(form.labels, "bug");
assert_eq!(form.priority, "high");
assert_eq!(form.body, "Original body.", "the body is copied too");
app.handle_key(press(KeyCode::Enter)).unwrap();
let issues = app.store.list().unwrap();
assert_eq!(issues.len(), 2, "clone creates a second file");
let created = issues
.iter()
.find(|i| i.title == "Source issue" && i.number != 7)
.expect("a distinct issue was created");
assert_eq!(created.labels, vec!["bug".to_string()]);
assert_eq!(created.body, "Original body.");
let source = issues.iter().find(|i| i.number == 7).unwrap();
assert_eq!(
source.body, "Original body.",
"the source is untouched by the clone"
);
}
#[test]
fn template_prefills_body_and_labels_before_submission() {
let (_tmp, mut app) = make_templated_app();
app.handle_key(press(KeyCode::F(1))).unwrap();
type_text(&mut app, "Templated");
for _ in 0..13 {
app.handle_key(press(KeyCode::Down)).unwrap();
}
assert_eq!(
app.form.as_ref().unwrap().focus,
crate::ui::form::FormFocus::Template
);
app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(
app.form_picker.is_some(),
"Enter on Template opens the picker"
);
app.handle_key(press(KeyCode::Down)).unwrap();
app.handle_key(press(KeyCode::Enter)).unwrap();
let form = app.form.as_ref().unwrap();
assert_eq!(form.template, "bug");
assert_eq!(form.labels, "bug", "the template pre-fills labels");
assert!(
form.body.contains("Steps to reproduce"),
"the template pre-fills the body, got {:?}",
form.body
);
assert!(
app.form_picker.is_none(),
"choosing closes the template picker"
);
app.form.as_mut().unwrap().focus = crate::ui::form::FormFocus::Title;
app.handle_key(press(KeyCode::Enter)).unwrap();
let issue = app
.store
.list()
.unwrap()
.into_iter()
.find(|i| i.title == "Templated")
.expect("the mode created the issue");
assert_eq!(issue.labels, vec!["bug".to_string()]);
assert!(issue.body.contains("Steps to reproduce"));
}
#[test]
fn new_issue_submits_without_a_template() {
let (_tmp, mut app) = make_shell_app();
app.handle_key(press(KeyCode::F(1))).unwrap();
assert!(app.store.config.templates.is_empty());
assert_eq!(
app.form.as_ref().unwrap().template,
"",
"no template is selected by default"
);
type_text(&mut app, "No template");
app.handle_key(press(KeyCode::Enter)).unwrap();
let issue = app
.store
.list()
.unwrap()
.into_iter()
.find(|i| i.title == "No template")
.expect("submitting without a template works");
assert_eq!(issue.labels, [] as [std::string::String; 0]);
assert_eq!(issue.body, "");
}
#[test]
fn template_selector_is_mouse_operable() {
let (_tmp, mut app) = make_templated_app();
app.handle_key(press(KeyCode::F(1))).unwrap();
let _ = draw_board(&app, 80, 24);
let row =
crate::ui::form::field_row_rect(app.frame_size.get(), crate::ui::form::FormFocus::Template)
.expect("the Template row is visible");
let action = app.handle_mouse(down_left(row.x + 3, row.y)).unwrap();
assert_eq!(action, Action::TemplateOpen);
app.apply_new_issue_action(action).unwrap();
assert!(
app.form_picker
.as_ref()
.is_some_and(|p| p.field == FormPickerField::Template),
"the click opened the template picker"
);
let _ = draw_board(&app, 80, 24);
let option = {
let regions = app.frame_hit_regions.borrow();
regions
.iter()
.find(|r| r.action == Action::TemplatePickOption(1))
.map(|r| r.rect)
.expect("the bug option recorded a click region")
};
let action = app.handle_mouse(down_left(option.x + 1, option.y)).unwrap();
assert_eq!(action, Action::TemplatePickOption(1));
app.apply_new_issue_action(action).unwrap();
let form = app.form.as_ref().unwrap();
assert_eq!(form.template, "bug");
assert_eq!(form.labels, "bug");
assert!(form.body.contains("Steps to reproduce"));
assert!(app.form_picker.is_none(), "the click closed the picker");
}
#[test]
fn rail_enter_switches_modes_and_opens_a_template() {
let (_tmp, mut app) = make_branchy_app();
let _ = app.handle_key(press(KeyCode::Char('h'))).unwrap();
for _ in 0..4 {
let _ = app.handle_key(press(KeyCode::Up)).unwrap();
}
assert_eq!(app.nav_focus_index(), Some(0));
assert_eq!(
app.nav_rows()[0],
Action::SwitchMode(TopMode::NewIssue),
"the New Issue trunk is the first rail row"
);
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(
matches!(app.mode, Mode::Form),
"a trunk Enter switches modes"
);
assert_eq!(app.top_mode, TopMode::NewIssue);
assert_eq!(
app.nav_focus_index(),
None,
"Enter hands the keyboard to the opened form"
);
let (_tmp, mut app) = make_branchy_app();
let _ = app.handle_key(press(KeyCode::Char('h'))).unwrap();
for _ in 0..4 {
let _ = app.handle_key(press(KeyCode::Up)).unwrap();
}
let _ = app.handle_key(press(KeyCode::Char('j'))).unwrap();
assert_eq!(
app.nav_rows()[app.nav_focus_index().unwrap()],
Action::NewFromTemplate("bug".into())
);
let _ = app.handle_key(press(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Form));
assert_eq!(app.top_mode, TopMode::NewIssue);
assert_eq!(app.form.as_ref().map(|f| f.template.as_str()), Some("bug"));
assert_eq!(app.nav_focus_index(), None);
}
#[test]
fn form_title_supports_cursor_and_word_editing() {
let (_tmp, mut app) = make_app();
let _ = app.handle_key(press(KeyCode::Char('n'))).unwrap();
assert!(matches!(app.mode, Mode::Form));
for c in "hello world".chars() {
let _ = app.handle_key(press(KeyCode::Char(c))).unwrap();
}
assert_eq!(app.form.as_ref().unwrap().title, "hello world");
let _ = app
.handle_key(press_with_modifiers(
KeyCode::Left,
crossterm::event::KeyModifiers::CONTROL,
))
.unwrap();
let _ = app.handle_key(press(KeyCode::Char('X'))).unwrap();
assert_eq!(app.form.as_ref().unwrap().title, "hello Xworld");
let _ = app
.handle_key(press_with_modifiers(
KeyCode::Char('a'),
crossterm::event::KeyModifiers::CONTROL,
))
.unwrap();
let _ = app.handle_key(press(KeyCode::Char('>'))).unwrap();
assert_eq!(app.form.as_ref().unwrap().title, ">hello Xworld");
let _ = app
.handle_key(press_with_modifiers(
KeyCode::Char('e'),
crossterm::event::KeyModifiers::CONTROL,
))
.unwrap();
let _ = app.handle_key(press(KeyCode::Backspace)).unwrap();
assert_eq!(app.form.as_ref().unwrap().title, ">hello Xworl");
let _ = app.handle_key(press(KeyCode::Left)).unwrap();
let _ = app.handle_key(press(KeyCode::Delete)).unwrap();
assert_eq!(app.form.as_ref().unwrap().title, ">hello Xwor");
}