ghtui 0.2.5

A keyboard-driven terminal client for GitHub, with a pull request diff viewer that goes further than GitHub's
use crate::act::{By, Subject};
use crate::api::browse::{Comment, DiscussionDetail, DiscussionsOf};
use crate::api::discussion::Discuss;
use crate::api::edit::{Edit, ReactionContent};

use super::*;

fn about() -> Subject {
    Subject::Discussion(DiscussionsOf::Repo(ghtui()), 14603)
}

/// The discussion as GitHub has it, changed by `f`.
fn deliver(s: &mut State, f: impl FnOnce(&mut DiscussionDetail)) -> Cmds {
    let mut d = fixtures::discussion();
    f(&mut d);
    let key = DataKey::Discussion(DiscussionsOf::Repo(ghtui()), 14603);
    fetched(s, key, Data::from(d))
}

/// `c` with no comment selected writes an answer (it's a Q&A); on a
/// comment or one of its replies, a reply in that comment's thread. The
/// page follows GitHub until it shows what you wrote.
#[test]
fn c_answers_or_replies_in_the_thread() {
    let mut s = with_discussion();
    press(&mut s, "c");
    let target = fixtures::composer(&mut s).target.clone();
    let title = target.title(&s);
    assert_eq!(title, "Answer on “How do I review a PR offline?”");
    let cmds = press(&mut s, "Use git<C-s>");
    let comment = Change::Discussion(Discuss::Comment {
        discussion: NodeId::new("D_14603"),
        reply_to: None,
        body: "Use git".into(),
    });
    assert_eq!(fixtures::sent_change(&cmds), (&comment, &By::Edit(about())));
    answer(&mut s, &cmds, Ok(()));
    says(&s, "Comment posted");
    assert!(s.following.is_some(), "until GitHub shows it");
    deliver(&mut s, |d| {
        let mut mine = d.comments.items.last().cloned().unwrap();
        mine.comment = Comment {
            author: "octocat".into(),
            body: "Use git".into(),
            ..mine.comment
        };
        d.comments.items.push(mine);
    });
    assert!(s.following.is_none());

    // On a reply, the reply goes in its comment's thread.
    for (author, text) in [("hubot", "Fetch"), ("octocat", "That works")] {
        select_comment(&mut s, author, text);
        press(&mut s, "c");
        let cmds = press(&mut s, "Thanks<C-s>");
        let reply = Change::Discussion(Discuss::Comment {
            discussion: NodeId::new("D_14603"),
            reply_to: Some(NodeId::new("DC_18765024")),
            body: "Thanks".into(),
        });
        assert_eq!(fixtures::sent_change(&cmds).0, &reply, "on {author}'s");
        press(&mut s, "<Esc><Esc>");
        s.overlay = None;
    }
}

/// `,!` marks a comment the answer, or unmarks the one that is; a reply
/// can't be one, and GitHub's word on who may decides.
#[test]
fn the_answer_is_marked_and_unmarked() {
    let mut s = with_discussion();
    select_comment(&mut s, "monalisa", "Drafts");
    let cmds = act(&mut s, Action::MarkAnswer);
    let mark = Change::Discussion(Discuss::Answer {
        comment: NodeId::new("DC_18765100"),
        on: true,
    });
    assert_eq!(fixtures::sent_change(&cmds), (&mark, &By::Act(about())));
    answer(&mut s, &cmds, Ok(()));
    says(&s, "Marked as the answer");
    deliver(&mut s, |d| {
        let monalisa = d.comments.items.last().cloned().unwrap();
        d.answer = Some(Box::new(monalisa));
    });
    assert!(s.following.is_none());

    let mut s = with_discussion();
    select_comment(&mut s, "hubot", "Fetch");
    let cmds = act(&mut s, Action::MarkAnswer);
    let unmark = Change::Discussion(Discuss::Answer {
        comment: NodeId::new("DC_18765024"),
        on: false,
    });
    assert_eq!(fixtures::sent_change(&cmds).0, &unmark);

    select_comment(&mut s, "octocat", "That works");
    assert!(act(&mut s, Action::MarkAnswer).is_empty());
    says(&s, "A reply can't be the answer, only a comment");
    deliver(&mut s, |d| d.answerable = false);
    select_comment(&mut s, "monalisa", "Drafts");
    act(&mut s, Action::MarkAnswer);
    says(&s, "Q&A doesn't take answers");
}

/// `,+` reacts to the selected comment (or the post), and the page
/// follows GitHub until it shows the reaction; `,e` and `,D` go by what
/// GitHub lets you do.
#[test]
fn comments_are_reacted_to_edited_and_deleted() {
    let mut s = with_discussion();
    select_comment(&mut s, "monalisa", "Drafts");
    press(&mut s, "+");
    assert!(matches!(s.overlay, Some(Overlay::Picker(_))));
    let cmds = press(&mut s, "<Enter>");
    let react = Change::Edit(Edit::React {
        subject: NodeId::new("DC_18765100"),
        content: ReactionContent::ThumbsUp,
        on: true,
    });
    assert_eq!(fixtures::sent_change(&cmds), (&react, &By::Edit(about())));
    answer(&mut s, &cmds, Ok(()));
    assert!(s.following.is_some());
    deliver(&mut s, |d| {
        if let Some(e) = d
            .comments
            .items
            .last_mut()
            .and_then(|c| c.comment.editable.as_mut())
        {
            e.reactions.push(crate::api::edit::Reaction {
                content: ReactionContent::ThumbsUp,
                count: 1,
                mine: true,
            });
        }
    });
    assert!(s.following.is_none());

    press(&mut s, "e");
    says(&s, "GitHub doesn't let you edit monalisa's comment");
    select_comment(&mut s, "octocat", "That works");
    press(&mut s, "e");
    let c = fixtures::composer(&mut s);
    assert_eq!(c.text(), "That works, thanks!");
    s.overlay = None;
    press(&mut s, "D");
    let c = overlay!(s, Ask);
    assert_eq!(c.title, "Delete octocat's reply?");
    let cmds = press(&mut s, "<Enter>");
    let delete = Change::Discussion(Discuss::Delete {
        id: NodeId::new("DC_18765030"),
    });
    assert_eq!(
        fixtures::sent_change(&cmds),
        (&delete, &By::Confirm(about()))
    );
}