use crate::route::{issues, pulls};
use std::path::PathBuf;
use crate::api::create::{NewIssue, NewPr};
use crate::git::local::{Branch, Checkout, Clone, CommitMessage};
use ratatui_textarea::CursorMove;
use super::*;
use crate::create::{HeadState, Push, Submission, Submit};
use crate::ui::form::Row;
fn form(action: Action) -> (State, Vec<Cmd>) {
let mut s = with_repo();
let cmds = act(&mut s, action);
let form = Data::NewForm(Box::new(crate::fixtures::new_form()));
let mut more = fetched(&mut s, DataKey::NewForm(ghtui()), form);
more.extend(cmds);
(s, more)
}
fn composer(s: &mut State) -> &mut crate::compose::Compose {
match &mut s.overlay {
Some(Overlay::Compose(c)) => c,
_ => panic!("no composer"),
}
}
fn bug_report() -> State {
let (mut s, _) = form(Action::NewIssue);
press(&mut s, "<Enter>");
s
}
#[test]
fn a_new_issue_starts_from_its_template_and_opens_once_made() {
let (mut s, cmds) = form(Action::NewIssue);
assert_eq!(s.route(), Some(&Route::Issues(issues::NewIssue(ghtui()))));
let form = DataKey::NewForm(ghtui());
assert!(crate::fixtures::asks(&cmds, &form), "{cmds:?}");
let text = page_text(&s);
for starter in ["Bug report", "Feature request", "Blank issue"] {
assert!(text.contains(starter), "{text}");
}
press(&mut s, "<Enter>");
let c = composer(&mut s);
assert_eq!(c.text(), "[Bug]: \n\n### What happened?\n\n### Version");
c.input.move_cursor(CursorMove::Top);
c.input.move_cursor(CursorMove::End);
c.input.insert_str("it crashes");
let sent = press(&mut s, "<C-s>");
let route = Route::Issues(issues::NewIssue(ghtui()));
let new = NewIssue {
repo: NodeId::new("R_ghtui"),
title: "[Bug]: it crashes".into(),
body: "### What happened?\n\n### Version".into(),
labels: vec![NodeId::new("LA_bug")],
assignees: vec![NodeId::new("U_octo")],
milestone: None,
};
let submit = Submit {
route: route.clone(),
new: Submission::Issue(new),
};
assert_eq!(sent, [fixtures::start(submit.clone())]);
assert!(composer(&mut s).sent.sending);
let cmds = fixtures::done(&mut s, submit, Ok(42));
let issue = Route::Issues(issues::Issue {
repo: ghtui(),
number: 42,
});
assert_eq!(s.route(), Some(&issue));
assert!(s.overlay.is_none());
says(&s, "Opened issue #42");
let issue = DataKey::Issue(ghtui(), 42);
assert!(crate::fixtures::asks(&cmds, &issue), "{cmds:?}");
assert!(
s.forms.get::<crate::create::Form>(&route).is_none(),
"the form is done"
);
}
#[test]
fn a_refused_issue_says_why_in_the_composer() {
let mut s = bug_report();
let submit = fixtures::sent::<Submit>(&press(&mut s, "<C-s>"));
let refused = ApiError::GraphQl(vec!["Title can't be blank".into()]);
fixtures::done(&mut s, submit, Err(refused.into()));
let c = composer(&mut s);
assert!(!c.sent.sending);
assert_eq!(
c.sent.error.as_deref(),
Some("GitHub didn't create the issue: Title can't be blank")
);
assert_eq!(s.route(), Some(&Route::Issues(issues::NewIssue(ghtui()))));
}
#[test]
fn labels_are_picked_and_text_is_kept() {
let mut s = bug_report();
composer(&mut s).input.insert_str("\nMore.");
press(&mut s, "<Esc>");
assert!(s.overlay.is_none());
assert!(page_text(&s).contains("More."), "{}", page_text(&s));
let _ = crate::forms::row(&mut s, &Row::Labels);
press(&mut s, "<Enter><Down><Enter>");
assert!(
matches!(s.overlay, Some(Overlay::Picker(_))),
"still picking"
);
press(&mut s, "<Esc>");
let form = (s
.forms
.get::<crate::create::Form>(&Route::Issues(issues::NewIssue(ghtui()))))
.unwrap();
assert_eq!(form.labels, [NodeId::new("LA_docs")]);
}
fn commit(oid: &str, message: &str) -> CommitMessage {
CommitMessage {
oid: oid.into(),
message: message.into(),
}
}
fn local_branch(on_github: Option<&str>) -> Branch {
let oid = |s: &str| Oid::parse(&s.repeat(40)).unwrap();
Branch {
name: "fix-parser".into(),
head: oid("a"),
remote: "origin".into(),
owner: Some("gold-silver-copper".into()),
on_github: on_github.map(oid),
ahead: on_github.map(|_| 0),
behind: on_github.map(|_| 0),
tracked: on_github.is_some(),
}
}
fn head(s: &mut State, on_github: Option<&str>) -> Vec<Cmd> {
let hs = HeadState {
local: Some(local_branch(on_github)),
commits: vec![commit(
&"a".repeat(40),
"Fix the parser\n\nIt dropped the last line.",
)]
.into(),
};
fixtures::fetched(s, compare(), Data::Head(Box::new(hs)))
}
fn compare() -> DataKey {
DataKey::Head {
repo: ghtui(),
base: "main".into(),
head: "fix-parser".into(),
}
}
fn in_clone() -> (State, Vec<Cmd>) {
let (mut s, cmds) = form(Action::NewPr);
assert!(fixtures::asks(&cmds, &DataKey::Clone(ghtui())), "{cmds:?}");
let clone = Clone {
top: PathBuf::from("/src/ghtui"),
remote: "origin".into(),
branch: Some("fix-parser".into()),
};
let cmds = fixtures::cloned(&mut s, &ghtui(), Some(clone));
(s, cmds)
}
#[test]
fn a_pull_request_from_the_clones_branch_is_pushed_then_made() {
let (mut s, cmds) = in_clone();
assert_eq!(fixtures::asked(&cmds), [compare()]);
head(&mut s, None);
let route = Route::Pulls(pulls::NewPr(ghtui()));
let text = "Fix the parser\n\nIt dropped the last line.\n\n## Checklist\n\n- [ ] Tests";
assert_eq!(
s.forms.get::<crate::create::Form>(&route).unwrap().text,
text
);
assert!(press(&mut s, "<C-s>").is_empty());
fixtures::warns(
&s,
"fix-parser isn't on GitHub: :push branch pushes it to origin",
);
assert!(act(&mut s, Action::PushBranch).is_empty());
assert_eq!(overlay!(s, Ask).title, "Push fix-parser to origin?");
let push = press(&mut s, "<Enter>");
let pushed = Push {
repo: ghtui(),
branch: "fix-parser".into(),
};
assert_eq!(push, [fixtures::start(pushed.clone())]);
let again = fixtures::done(&mut s, pushed, Ok(()));
assert!(s.overlay.is_none());
says(&s, "Pushed fix-parser");
assert_eq!(fixtures::asked(&again), [compare()], "compared again");
head(&mut s, Some("a"));
press(&mut s, "<C-s>");
let choices: Vec<&str> = overlay!(s, Ask)
.choices
.iter()
.map(|(l, _)| l.as_str())
.collect();
assert_eq!(
choices,
["Create pull request", "Create draft pull request"]
);
let sent = press(&mut s, "<Tab><Enter>");
let new = NewPr {
repo: NodeId::new("R_ghtui"),
base: "main".into(),
head: "fix-parser".into(),
title: "Fix the parser".into(),
body: "It dropped the last line.\n\n## Checklist\n\n- [ ] Tests".into(),
draft: true,
};
let submit = Submit {
route,
new: Submission::Pr(new),
};
assert_eq!(sent, [fixtures::start(submit.clone())]);
let cmds = fixtures::done(&mut s, submit, Ok(7));
let pr = PrRef {
repo: ghtui(),
number: 7,
};
assert_eq!(s.route(), Some(&Route::pr(pr.clone())));
assert!(s.overlay.is_none());
assert!(crate::fixtures::asks_pr(&cmds, &pr), "{cmds:?}");
}
#[test]
fn checkout_says_what_branch_or_why_not() {
let mut s = with_pr();
let pr = PrRef::parse("gold-silver-copper/ghtui#12").unwrap();
let cmds = act(&mut s, Action::Checkout);
let work = crate::clone::Checkout {
repo: pr.repo,
pr: Some(pr.number),
name: "octocat/syntax-palette".into(),
track: None,
};
assert_eq!(cmds, [fixtures::start(work.clone())]);
let dirty = Checkout::Uncommitted(vec!["src/a.rs".into(), "b.rs".into()]);
fixtures::done(&mut s, work.clone(), Ok(dirty));
says(&s, "Not checked out: src/a.rs and 1 other has changes");
let made = Checkout::Created("octocat/syntax-palette".into());
fixtures::done(&mut s, work, Ok(made));
says(&s, "you're on octocat/syntax-palette");
fixtures::cloned(&mut s, &ghtui(), None);
assert!(act(&mut s, Action::Checkout).is_empty());
says(&s, "isn't running in a clone of gold-silver-copper/ghtui");
}
#[test]
fn checkout_on_a_row_waits_for_its_pull_request() {
let mut s = with_home();
s.data.retain(|key, _| !matches!(key, DataKey::Pr(_)));
press(&mut s, "<Down><Down>");
let pr = PrRef::parse("gold-silver-copper/ghtui#12").unwrap();
assert_eq!(act(&mut s, Action::Checkout), [fixtures::fetch_pr(&pr)]);
says(&s, "Loading gold-silver-copper/ghtui#12");
let cmds = fixtures::pr_fetched(&mut s, &pr, Ok(fixtures::pr_detail()));
assert_eq!(fixtures::sent::<crate::clone::Checkout>(&cmds).pr, Some(12));
}
pub(super) fn with_new_issue() -> State {
let mut s = bug_report();
press(&mut s, "<Esc>");
s
}
pub(super) fn with_new_pr() -> State {
let (mut s, _) = in_clone();
head(&mut s, Some("a"));
s
}