package main import ( "slices" "strings" "testing" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/ansi" ) func TestReviewerCompletionSupportsMultipleEligibleUsers(t *testing.T) { m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second) m.width = 80 m.details = PRDetails{ PullRequest: PullRequest{Author: "author"}, ViewerLogin: "current", } m.prEditField = prEditReviewersField m.prEditEditors[prEditReviewersField] = newTextEditor("alice, bo", false) m.prEditUsers = []RepositoryUser{ {Login: "alice", Name: "Alice", CanReview: true}, {Login: "bob", Name: "Bob", CanReview: true}, {Login: "author", Name: "Author", CanReview: true}, {Login: "carol", Name: "Carol", CanAssign: true}, } suggestions := m.userSuggestions() if len(suggestions) != 1 || suggestions[0].user.Login != "bob" { t.Fatalf("reviewer suggestions = %#v", suggestions) } if !m.completeUserSuggestion() || m.prEditEditors[prEditReviewersField].Text != "alice, bob" { t.Fatalf("completed reviewers = %q", m.prEditEditors[prEditReviewersField].Text) } displayEditor := m.prEditDisplayEditor(prEditReviewersField, 76) if displayEditor.Text != "@alice, @bob" { t.Fatalf("completed reviewer display = %q", displayEditor.Text) } if displayEditor.Cursor != len([]rune(displayEditor.Text)) { t.Fatalf("completed reviewer display cursor = %d", displayEditor.Cursor) } view := ansi.Strip(strings.Join( m.prEditFieldLines("reviewers", prEditReviewersField, 80), "\n", )) if !strings.Contains(view, "@alice, @bob") || !strings.Contains(view, "no matching eligible users") { t.Fatalf("completed reviewer field is inconsistent:\n%s", view) } } func TestReviewerInputCommitsMultipleEligibleUsersWithSpace(t *testing.T) { m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second) m.writeMode = writePREdit m.prEditField = prEditReviewersField m.details = PRDetails{PullRequest: PullRequest{Author: "author"}} m.prEditUsers = []RepositoryUser{ {Login: "alice", CanReview: true}, {Login: "bob", CanReview: true}, {Login: "carol", CanReview: true}, } m.prEditEditors[prEditReviewersField] = newTextEditor("bob", false) suggestions := m.userSuggestions() if len(suggestions) != 2 || suggestions[0].user.Login != "alice" || suggestions[1].user.Login != "carol" { t.Fatalf("suggestions after complete reviewer = %#v", suggestions) } view := ansi.Strip(strings.Join( m.prEditFieldLines("reviewers", prEditReviewersField, 80), "\n", )) if !strings.Contains(view, "space starts next reviewer") { t.Fatalf("multi-reviewer completion help missing:\n%s", view) } updated, _ := m.updatePREditInput(tea.KeyMsg{Type: tea.KeySpace}) m = updated.(App) if got := m.prEditEditors[prEditReviewersField].Text; got != "bob, " { t.Fatalf("space after complete reviewer produced %q", got) } if !m.completeUserSuggestion() { t.Fatal("next reviewer suggestion was not completed") } if got := m.prEditEditors[prEditReviewersField].Text; got != "bob, alice" { t.Fatalf("multiple reviewer input = %q", got) } suggestions = m.userSuggestions() if len(suggestions) != 1 || suggestions[0].user.Login != "carol" { t.Fatalf("already selected reviewers remained in suggestions: %#v", suggestions) } display := m.prEditDisplayEditor(prEditReviewersField, 76) if display.Text != "@bob, @alice" { t.Fatalf("multiple reviewer display = %q", display.Text) } m.prEditEditors[prEditReviewersField] = newTextEditor("bo", false) updated, _ = m.updatePREditInput(tea.KeyMsg{Type: tea.KeySpace}) m = updated.(App) if got := m.prEditEditors[prEditReviewersField].Text; got != "bo" { t.Fatalf("space after incomplete reviewer produced %q", got) } } func TestReviewerDisplayColorsOnlyCompleteEligibleNames(t *testing.T) { m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second) m.details = PRDetails{PullRequest: PullRequest{Author: "author"}} m.prEditUsers = []RepositoryUser{ {Login: "bob", CanReview: true}, {Login: "assignee-only", CanAssign: true}, {Login: "author", CanReview: true}, } m.prEditEditors[prEditReviewersField] = newTextEditor("bo, assignee-only, author", false) display := m.prEditDisplayEditor(prEditReviewersField, 76) if display.Text != "bo, assignee-only, author" || len(display.protectedStyles) != 0 { t.Fatalf("partial or ineligible reviewers were decorated: %#v", display) } m.prEditEditors[prEditReviewersField] = newTextEditor("bob, @BOB", false) display = m.prEditDisplayEditor(prEditReviewersField, 76) if display.Text != "@bob, @BOB" { t.Fatalf("eligible reviewer display = %q", display.Text) } if len(display.protectedStyles) != 2 { t.Fatalf("eligible reviewer styles = %#v", display.protectedStyles) } for _, style := range display.protectedStyles { if style.color != string(authorColor("bob")) { t.Fatalf("eligible reviewer color = %q, want normal author color", style.color) } } } func TestReviewerSuggestionsPreferRecentRepositoryActivity(t *testing.T) { now := time.Now() m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second) m.details = PRDetails{PullRequest: PullRequest{Author: "author"}} m.prEditField = prEditReviewersField m.prEditEditors[prEditReviewersField] = newTextEditor("", false) m.prEditUsers = []RepositoryUser{ { Login: "old-contributor", CanReview: true, RecentCommits: 30, LastContributionAt: now.AddDate(-2, 0, 0), }, { Login: "active-contributor", CanReview: true, RecentCommits: 3, RecentAdditions: 50, LastContributionAt: now.Add(-24 * time.Hour), }, {Login: "never-contributed", CanReview: true}, } suggestions := m.userSuggestions() if len(suggestions) != 3 || suggestions[0].user.Login != "active-contributor" { t.Fatalf("activity-ranked reviewers = %#v", suggestions) } } func TestPREditStartsWithCurrentRequestedReviewers(t *testing.T) { m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second) m.loading = false m.details = PRDetails{ PullRequest: PullRequest{ ID: "pr", Owner: "o", Repository: "r", Title: "Title", }, BaseRef: "main", RequestedReviewers: []string{"alice", "bob"}, Assignees: []string{"carol"}, Permissions: ViewerPermissions{CanUpdatePR: true, CanAssign: true}, } m.startPREdit() if got := m.prEditEditors[prEditReviewersField].Text; got != "alice, bob" { t.Fatalf("reviewer field = %q", got) } if got := m.prEditEditors[prEditAssigneesField].Text; got != "carol" { t.Fatalf("assignee field = %q", got) } } func TestPREditShowsCompletedAndTeamReviewersReadOnly(t *testing.T) { m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second) m.loading = false m.details = PRDetails{ PullRequest: PullRequest{ ID: "pr", Owner: "o", Repository: "r", Title: "Title", }, BaseRef: "main", RequestedReviewers: []string{"pending-user", "rerequested-user"}, Reviewers: []Reviewer{ {Login: "approved-user", State: "APPROVED"}, {Login: "backend-team", State: "REVIEW_REQUESTED"}, {Login: "commented-user", State: "COMMENTED"}, {Login: "pending-user", State: "REVIEW_REQUESTED"}, {Login: "rerequested-user", State: "APPROVED"}, }, Permissions: ViewerPermissions{CanUpdatePR: true, CanAssign: true}, } m.startPREdit() if got := m.prEditEditors[prEditReviewersField].Text; got != "pending-user, rerequested-user" { t.Fatalf("editable reviewer requests = %q", got) } view := ansi.Strip(strings.Join( m.prEditFieldLines("reviewers", prEditReviewersField, 80), "\n", )) for _, expected := range []string{ "pending requests editable", "[@approved-user · approved]", "[@backend-team · review requested]", "[@commented-user · commented]", } { if !strings.Contains(view, expected) { t.Fatalf("reviewer field does not show %q:\n%s", expected, view) } } for _, editable := range []string{"pending-user", "rerequested-user"} { if strings.Contains(view, "[@"+editable) { t.Fatalf("pending request @%s was rendered as a protected token:\n%s", editable, view) } } displayEditor := m.prEditDisplayEditor(prEditReviewersField, 76) if len(displayEditor.protectedStyles) != 3 { t.Fatalf("protected reviewer styles = %#v", displayEditor.protectedStyles) } firstStyle := displayEditor.protectedStyles[0] displayRunes := []rune(displayEditor.Text) if got := string(displayRunes[firstStyle.start:firstStyle.end]); got != "@approved-user" { t.Fatalf("first protected author span = %q", got) } if firstStyle.color != string(darkenColor(authorColor("approved-user"))) { t.Fatalf("protected author color = %q, want darkened deterministic color", firstStyle.color) } m.prEditEditors[prEditReviewersField] = newTextEditor("pending-user", false) view = ansi.Strip(strings.Join( m.prEditFieldLines("reviewers", prEditReviewersField, 80), "\n", )) if !strings.Contains(view, "[@rerequested-user · approved]") { t.Fatalf("removed re-review request did not retain its submitted review read-only:\n%s", view) } m.prEditEditors[prEditReviewersField] = newTextEditor("", false) for range 20 { m.prEditEditors[prEditReviewersField].handleKey( tea.KeyMsg{Type: tea.KeyBackspace}, false, ) m.prEditEditors[prEditReviewersField].handleKey( tea.KeyMsg{Type: tea.KeyDelete}, false, ) } if m.prEditEditors[prEditReviewersField].Text != "" || !strings.Contains( ansi.Strip(strings.Join( m.prEditFieldLines("reviewers", prEditReviewersField, 80), "\n", )), "[@approved-user · approved]", ) { t.Fatal("editing the reviewer field modified a protected reviewer token") } } func TestReviewerSuggestionsExcludeExistingReviewers(t *testing.T) { m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second) m.details = PRDetails{ PullRequest: PullRequest{Author: "author"}, Reviewers: []Reviewer{ {Login: "approved-user", State: "APPROVED"}, {Login: "commented-user", State: "COMMENTED"}, }, } m.prEditField = prEditReviewersField m.prEditEditors[prEditReviewersField] = newTextEditor("", false) m.prEditUsers = []RepositoryUser{ {Login: "approved-user", CanReview: true}, {Login: "commented-user", CanReview: true}, {Login: "new-user", CanReview: true}, } suggestions := m.userSuggestions() if len(suggestions) != 1 || suggestions[0].user.Login != "new-user" { t.Fatalf("reviewer suggestions include existing reviewers: %#v", suggestions) } } func TestPeopleOnlyPREditSkipsCoreMetadataMutation(t *testing.T) { service := &recordingPRService{} m := NewApp(service, "o", "r", false, 50, time.Second) m.screen, m.loading, m.width, m.height = dashboardScreen, false, 80, 30 m.details = PRDetails{ PullRequest: PullRequest{ ID: "pr", Owner: "o", Repository: "r", Number: 4, Title: "Title", Author: "author", }, BaseRef: "main", RequestedReviewers: []string{"alice"}, Permissions: ViewerPermissions{CanUpdatePR: true, CanAssign: true}, } m.startPREdit() m.prEditUsers = []RepositoryUser{ {Login: "alice", CanReview: true}, {Login: "bob", CanReview: true, CanAssign: true}, } m.prEditUsersLoading = false m.prEditEditors[prEditReviewersField] = newTextEditor("alice, bob", false) m.prEditEditors[prEditAssigneesField] = newTextEditor("bob", false) if err := m.validatePREdit(); err != nil { t.Fatal(err) } m.writeMode = writePREditConfirm updated, command := m.updatePREditInput(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("y")}) m = updated.(App) if command == nil { t.Fatal("people update did not create a command") } msg := command().(pullRequestUpdatedMsg) if msg.err != nil { t.Fatal(msg.err) } if service.updateID != "" { t.Fatalf("people-only edit rewrote core metadata for %q", service.updateID) } if !slices.Equal(service.people.Reviewers, []string{"alice", "bob"}) || !slices.Equal(service.people.Assignees, []string{"bob"}) { t.Fatalf("people update = %#v", service.people) } } func TestApplyingUserReviewersPreservesTeamRequestsAndCompletedReviews(t *testing.T) { m := App{details: PRDetails{ RequestedReviewers: []string{"old-user"}, Reviewers: []Reviewer{ {Login: "old-user", State: "REVIEW_REQUESTED"}, {Login: "backend-team", State: "REVIEW_REQUESTED"}, {Login: "approved-user", State: "APPROVED"}, }, }} m.applyPREditPeople(PullRequestPeople{ Reviewers: []string{"new-user"}, Assignees: []string{"assignee"}, }) got := make(map[string]string) for _, reviewer := range m.details.Reviewers { got[reviewer.Login] = reviewer.State } if len(got) != 3 || got["backend-team"] != "REVIEW_REQUESTED" || got["approved-user"] != "APPROVED" || got["new-user"] != "REVIEW_REQUESTED" { t.Fatalf("reviewers after update = %#v", m.details.Reviewers) } }