380 lines
13 KiB
Go
380 lines
13 KiB
Go
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 TestReviewerCompletionUsesEnterAndTabLeavesField(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}}
|
|
m.prEditEditors[prEditReviewersField] = newTextEditor("ali", false)
|
|
|
|
updated, _ := m.updatePREditInput(tea.KeyMsg{Type: tea.KeyTab})
|
|
m = updated.(App)
|
|
if m.prEditField != prEditAssigneesField ||
|
|
m.prEditEditors[prEditReviewersField].Text != "ali" {
|
|
t.Fatalf(
|
|
"tab field=%d reviewers=%q",
|
|
m.prEditField, m.prEditEditors[prEditReviewersField].Text,
|
|
)
|
|
}
|
|
|
|
m.prEditField = prEditReviewersField
|
|
updated, _ = m.updatePREditInput(tea.KeyMsg{Type: tea.KeyEnter})
|
|
m = updated.(App)
|
|
if m.prEditField != prEditReviewersField ||
|
|
m.prEditEditors[prEditReviewersField].Text != "alice" {
|
|
t.Fatalf(
|
|
"enter field=%d reviewers=%q",
|
|
m.prEditField, m.prEditEditors[prEditReviewersField].Text,
|
|
)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|