use crate::act::{By, Subject};
use crate::api::edit::{Edit, Named, Reaction, ReactionContent, Written};
use super::*;
use crate::fixtures::{self};
use crate::ui::page::Link;
fn issue() -> Subject {
Subject::Issue(ghtui(), 14)
}
fn on_hubot(s: &mut State) {
let selected = |s: &State| match s.screen() {
Screen::Page(p) => {
matches!(p.selected_link(), Some(Link::Quote { author, .. }) if author == "hubot")
}
Screen::Diff(_) => false,
};
for _ in 0..200 {
if selected(s) {
return;
}
press(s, "<Down>");
}
assert!(selected(s), "hubot's comment is selected");
}
fn sent(cmds: &[Cmd]) -> (&Change, &By) {
fixtures::sent_change(cmds)
}
fn hooray(count: u64, mine: bool) -> Reaction {
Reaction {
content: ReactionContent::Hooray,
count,
mine,
}
}
#[test]
fn a_comment_is_edited_in_place_and_followed() {
let mut s = issue_with(|_| {});
on_hubot(&mut s);
let before = screen_text(&s);
let comment = before.lines().position(|l| l.contains("hubot commented"));
let opened = press(&mut s, "e");
assert!(
fixtures::asked(&opened) == [DataKey::Mentionable(ghtui())] && opened.len() == 1,
"{opened:?}"
);
let Some(Overlay::Compose(c)) = &s.overlay else {
panic!("e opens the composer")
};
assert_eq!(c.text(), "Agreed. 120 columns, centered?");
let screen = screen_text(&s);
let editor = screen
.lines()
.position(|l| l.contains("Edit hubot's comment"));
assert!(
comment.zip(editor).is_some_and(|(c, e)| c.abs_diff(e) <= 1),
"the editor takes the comment's place: {comment:?} {editor:?}\n{screen}"
);
press(&mut s, "!");
let cmds = press(&mut s, "<C-s>");
let edit = Edit::Body {
of: Written::IssueComment,
id: NodeId::new("IC_1"),
body: "Agreed. 120 columns, centered?!".into(),
};
assert_eq!(sent(&cmds), (&Change::Edit(edit), &By::Edit(issue())));
answer(&mut s, &cmds, Ok(()));
assert!(s.overlay.is_none());
says(&s, "Saved");
issue_shows(&mut s, |_| {});
assert!(s.following.is_some());
press(&mut s, "e");
says(&s, "hasn't shown the last change");
issue_shows(&mut s, |d| {
d.first_comment().unwrap().body = "Agreed. 120 columns, centered?!".into();
});
assert!(s.following.is_none());
}
#[test]
fn edits_are_offered_only_where_github_allows_them() {
let mut s = issue_with(|d| {
let mine = d.first_comment().unwrap().editable.as_mut().unwrap();
(mine.can_update, mine.can_delete) = (false, false);
});
on_hubot(&mut s);
let why = |s: &State, action| {
let row = s.doables().into_iter().find(|d| d.action == action);
row.and_then(|d| d.unavailable).unwrap_or_default()
};
assert!(why(&s, Action::EditComment).contains("doesn't let you edit hubot's comment"));
assert!(why(&s, Action::DeleteComment).contains("doesn't let you delete"));
assert!(
why(&s, Action::React).is_empty(),
"reacting is still offered"
);
press(&mut s, "e");
assert!(s.overlay.is_none());
says(&s, "GitHub doesn't let you edit hubot's comment");
press(&mut s, "<Home>");
if let Screen::Page(p) = s.screen_mut() {
p.selected = None;
}
assert!(why(&s, Action::EditComment).is_empty());
press(&mut s, "D");
says(&s, "Select a comment first");
press(&mut s, "e");
let Some(Overlay::Compose(c)) = &s.overlay else {
panic!("the description opens")
};
assert!(c.text().starts_with("On a 200-column terminal"));
let mut list = super::with_issues();
press(&mut list, "e");
says(&list, "works on a comment, an issue or a pull request");
}
#[test]
fn deleting_a_comment_asks_first() {
let mut s = issue_with(|_| {});
on_hubot(&mut s);
assert!(press(&mut s, "D").is_empty());
let Some(Overlay::Ask(c)) = &s.overlay else {
panic!("D asks first")
};
assert!(c.title.starts_with("Delete hubot's comment"), "{}", c.title);
issue_shows(&mut s, |_| {});
assert!(matches!(s.overlay, Some(Overlay::Ask(_))));
let cmds = press(&mut s, "<Enter>");
let delete = Edit::Delete {
of: Written::IssueComment,
id: NodeId::new("IC_1"),
};
assert_eq!(sent(&cmds), (&Change::Edit(delete), &By::Confirm(issue())));
answer(&mut s, &cmds, Ok(()));
assert!(s.overlay.is_none());
says(&s, "Deleted");
issue_shows(&mut s, |_| {});
assert!(s.following.is_some());
on_hubot(&mut s);
assert!(press(&mut s, "D").is_empty());
assert!(s.overlay.is_none());
says(&s, "GitHub hasn't shown the last change yet");
issue_shows(&mut s, |d| {
d.timeline
.items
.retain(|i| !matches!(i, crate::api::timeline::Item::Comment(_)));
});
assert!(s.following.is_none());
}
#[test]
fn reactions_show_under_the_description_and_each_comment() {
let s = issue_with(|d| {
d.editable.reactions = vec![Reaction {
content: ReactionContent::ThumbsUp,
count: 3,
mine: false,
}];
let comment = d.first_comment().unwrap().editable.as_mut().unwrap();
comment.reactions = vec![
hooray(2, true),
Reaction {
content: ReactionContent::Eyes,
count: 1,
mine: false,
},
];
});
let screen = screen_text(&s);
let row = |text: &str| screen.lines().position(|l| l.contains(text));
let (yours, hubots) = (row("👍 3"), row("🎉 2 👀 1"));
let hubot = row("hubot commented");
assert!(yours < hubot && hubot < hubots, "{screen}");
}
#[test]
fn reacting_toggles_yours() {
let mut s = issue_with(|d| d.editable.reactions = vec![hooray(2, true)]);
press(&mut s, "<Home>");
if let Screen::Page(p) = s.screen_mut() {
p.selected = None;
}
press(&mut s, "+");
let rows: Vec<String> = match &s.overlay {
Some(Overlay::Picker(p)) => s.picker_rows(p).into_iter().map(|(r, _)| r.label).collect(),
_ => panic!("+ opens the reactions"),
};
assert_eq!(rows.len(), 8);
assert!(
rows[3].starts_with('✓') && rows[3].contains("Hooray"),
"{rows:?}"
);
let cmds = press(&mut s, "<Down><Down><Down><Enter>");
let take_back = Edit::React {
subject: NodeId::new("I_14"),
content: ReactionContent::Hooray,
on: false,
};
assert_eq!(sent(&cmds), (&Change::Edit(take_back), &By::Edit(issue())));
assert!(s.overlay.is_none());
answer(&mut s, &cmds, Ok(()));
issue_shows(&mut s, |d| d.editable.reactions = vec![hooray(1, false)]);
assert!(s.following.is_none());
on_hubot(&mut s);
press(&mut s, "+");
let cmds = press(&mut s, "<Enter>");
let give = Edit::React {
subject: NodeId::new("IC_1"),
content: ReactionContent::ThumbsUp,
on: true,
};
assert_eq!(sent(&cmds).0, &Change::Edit(give));
}
#[test]
fn the_labels_picker_lists_whats_set_first() {
let mut s = issue_with(|_| {});
let cmds = act(&mut s, Action::EditLabels);
let key = DataKey::Sidebar(ghtui(), 14);
assert!(crate::fixtures::asks(&cmds, &key), "{cmds:?}");
fixtures::served(&mut s, &cmds);
press(&mut s, "<Down><Down><Enter><Up><Enter>");
let screen = screen_text(&s);
let row = |text: &str| screen.lines().position(|l| l.contains(text));
let set = row("✓ bug");
let (added, unset) = (row("✓ documentation"), row("good first issue"));
assert!(set.is_some() && set < added && added < unset, "{screen}");
assert!(screen.contains("removing") && screen.contains("adding"));
}
#[test]
fn labels_are_checked_then_applied() {
let mut s = issue_with(|_| {});
let cmds = act(&mut s, Action::EditLabels);
fixtures::served(&mut s, &cmds);
press(&mut s, "<Down><Down><Enter><Up><Enter>");
let cmds = press(&mut s, "<C-s>");
let named = |id: &str, name: &str| Named {
id: NodeId::new(id),
name: name.into(),
};
let labels = Edit::Labels {
subject: NodeId::new("I_14"),
add: vec![named("L_docs", "documentation")],
remove: vec![named("L_ui", "ui")],
};
assert_eq!(sent(&cmds), (&Change::Edit(labels), &By::Edit(issue())));
says(&s, "Changing the labels");
answer(&mut s, &cmds, Ok(()));
issue_shows(&mut s, |_| {});
assert!(s.following.is_some());
let label = |name: &str| crate::api::model::Label {
name: name.into(),
color: "0075ca".into(),
};
issue_shows(&mut s, |d| d.labels = vec![label("bug")].into());
assert!(s.following.is_some(), "documentation isn't there yet");
issue_shows(&mut s, |d| {
d.labels = vec![label("bug"), label("documentation")].into();
});
assert!(s.following.is_none());
act(&mut s, Action::EditLabels);
assert!(press(&mut s, "<C-s>").is_empty());
says(&s, "Nothing changed");
}
#[test]
fn reviewers_include_teams() {
let mut s = with_pr();
let cmds = act(&mut s, Action::RequestReviews);
fixtures::served(&mut s, &cmds);
let rows: Vec<String> = match &s.overlay {
Some(Overlay::Picker(p)) => s.picker_rows(p).into_iter().map(|(r, _)| r.label).collect(),
_ => panic!("asking for reviews opens the reviewers"),
};
let names = [
"✓ defunkt",
"✓ octo-org/core",
" hubot",
" monalisa",
" octo-org/docs",
];
assert_eq!(rows, names);
let cmds = press(&mut s, "<Down><Enter><Down><Enter><C-s>");
let Change::Edit(Edit::Reviewers { users, teams, .. }) = sent(&cmds).0 else {
panic!("{cmds:?}")
};
let names = |list: &[Named]| list.iter().map(|n| n.name.clone()).collect::<Vec<_>>();
assert_eq!(names(users), ["defunkt", "hubot"]);
assert!(teams.is_empty());
answer(&mut s, &cmds, Ok(()));
let mut activity = fixtures::activity();
activity.requested = ["defunkt", "hubot"]
.map(|who| crate::api::timeline::Requested {
who: crate::api::browse::Reviewer::User(who.into()),
code_owner: false,
})
.to_vec()
.into();
fetched(
&mut s,
DataKey::PrActivity(pr()),
Data::PrActivity(Box::new(activity)),
);
assert!(s.following.is_none());
}
#[test]
fn milestone_and_title() {
let mut s = issue_with(|_| {});
let cmds = act(&mut s, Action::EditMilestone);
fixtures::served(&mut s, &cmds);
let cmds = press(&mut s, "<Down><Down><Enter>");
let Change::Edit(Edit::Milestone { milestone, .. }) = sent(&cmds).0 else {
panic!("{cmds:?}")
};
assert_eq!(milestone.as_ref().map(|m| m.id.as_str()), Some("M_4"));
answer(&mut s, &cmds, Ok(()));
issue_shows(&mut s, |d| {
if let Some(m) = &mut d.milestone {
m.title = "1.0".into();
}
});
assert!(s.following.is_none());
act(&mut s, Action::EditTitle);
let cmds = press(&mut s, "!<Enter>");
let title = Edit::Title {
pr: false,
id: NodeId::new("I_14"),
title: "Pages wrap at the terminal width!".into(),
};
assert_eq!(sent(&cmds), (&Change::Edit(title), &By::Edit(issue())));
}