fix: reviewers not correctly shown on dashboard edit screen

This commit is contained in:
2026-07-30 11:26:21 +02:00
parent bcad515698
commit e045bd39b2
7 changed files with 549 additions and 15 deletions

View File

@@ -34,11 +34,101 @@ func TestReviewerCompletionSupportsMultipleEligibleUsers(t *testing.T) {
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, "comma separates users") {
t.Fatalf("reviewer completion help missing:\n%s", view)
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)
}
}
}
@@ -86,6 +176,111 @@ func TestPREditStartsWithCurrentRequestedReviewers(t *testing.T) {
}
}
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)