Improve highlighting and add QoL changes
This commit is contained in:
191
tui_test.go
191
tui_test.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -10,6 +11,23 @@ import (
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
type recordingService struct {
|
||||
owner string
|
||||
repo string
|
||||
number int
|
||||
}
|
||||
|
||||
func (s *recordingService) ListPullRequests(context.Context, string, string, int, bool) ([]PullRequest, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) GetPullRequest(_ context.Context, owner, repo string, number int) (PRDetails, error) {
|
||||
s.owner, s.repo, s.number = owner, repo, number
|
||||
return PRDetails{PullRequest: PullRequest{
|
||||
Owner: owner, Repository: repo, RepoWithOwner: owner + "/" + repo, Number: number,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func TestResolvedThreadsStartFolded(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10)
|
||||
m.details = PRDetails{PullRequest: PullRequest{Number: 7}}
|
||||
@@ -26,6 +44,21 @@ func TestResolvedThreadsStartFolded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedThreadIconTakesPrecedenceOverOutdated(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.details = PRDetails{Threads: []ReviewThread{{
|
||||
ID: "done", Path: "main.go", Line: 12, IsResolved: true, IsOutdated: true,
|
||||
}}}
|
||||
|
||||
rendered := ansi.Strip(m.threadList(48, 10))
|
||||
if !strings.Contains(rendered, "✓ main.go") {
|
||||
t.Fatalf("resolved and outdated thread did not use check mark:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, "○ main.go") {
|
||||
t.Fatalf("outdated icon took precedence over resolved:\n%s", rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectionSurvivesRefresh(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10)
|
||||
m.details = PRDetails{
|
||||
@@ -73,6 +106,51 @@ func TestPaneFocusAndNavigation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextualHelpOpensAndClosesWithoutLeavingScreen(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen = threadScreen
|
||||
m.width, m.height = 70, 24
|
||||
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")})
|
||||
m = updated.(App)
|
||||
if !m.helpVisible || m.screen != threadScreen {
|
||||
t.Fatal("? did not open thread help")
|
||||
}
|
||||
plain := ansi.Strip(m.View())
|
||||
if !strings.Contains(plain, "Review thread keys") ||
|
||||
!strings.Contains(plain, "Fuzzy-search file paths") ||
|
||||
strings.Contains(plain, "Open pull request") {
|
||||
t.Fatalf("help was not contextual:\n%s", plain)
|
||||
}
|
||||
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
||||
m = updated.(App)
|
||||
if m.helpVisible || m.screen != threadScreen {
|
||||
t.Fatal("escape did not close help in place")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpCanScrollInShortTerminal(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen, m.helpVisible = threadScreen, true
|
||||
m.width, m.height = 46, 9
|
||||
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("G")})
|
||||
m = updated.(App)
|
||||
if m.helpScroll != m.helpMaxScroll() || m.helpScroll == 0 {
|
||||
t.Fatalf("help scroll = %d, max = %d", m.helpScroll, m.helpMaxScroll())
|
||||
}
|
||||
plain := ansi.Strip(m.View())
|
||||
if !strings.Contains(plain, "Quit") {
|
||||
t.Fatalf("last help bindings are not reachable:\n%s", plain)
|
||||
}
|
||||
for i, line := range strings.Split(m.View(), "\n") {
|
||||
if got := ansi.StringWidth(line); got > m.width {
|
||||
t.Fatalf("help line %d width = %d, terminal width = %d", i, got, m.width)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewNeverExceedsTerminalWidth(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen = threadScreen
|
||||
@@ -103,6 +181,74 @@ func TestTruncatePathPreservesFilename(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyFileSearchRanksAndFiltersPaths(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.searchQuery = "svcusr"
|
||||
m.details = PRDetails{Threads: []ReviewThread{
|
||||
{Path: "docs/review-user.md"},
|
||||
{Path: "internal/service/user.go"},
|
||||
{Path: "internal/service/team.go"},
|
||||
}}
|
||||
|
||||
matches := m.matchingThreadIndices()
|
||||
if len(matches) != 1 {
|
||||
t.Fatalf("matches = %v, want one fuzzy match", matches)
|
||||
}
|
||||
if matches[0] != 1 {
|
||||
t.Fatalf("best match = %q, want internal/service/user.go", m.details.Threads[matches[0]].Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyFileSearchSupportsSpaceSeparatedTerms(t *testing.T) {
|
||||
path := "logprep/ng/processor/generic_adder/rule.py"
|
||||
if _, ok := fuzzyPathScore(path, "ng generic_adder rule"); !ok {
|
||||
t.Fatalf("space-separated query did not match %q", path)
|
||||
}
|
||||
if _, ok := fuzzyPathScore(path, "ng generic_adder missing"); ok {
|
||||
t.Fatalf("query matched despite a missing term")
|
||||
}
|
||||
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen, m.searching, m.searchQuery = threadScreen, true, "ng"
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeySpace, Runes: []rune{' '}})
|
||||
if got := updated.(App).searchQuery; got != "ng " {
|
||||
t.Fatalf("space key produced search query %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSearchCanJumpOrCancel(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen = threadScreen
|
||||
m.threadIndex = 1
|
||||
m.details = PRDetails{Threads: []ReviewThread{
|
||||
{Path: "cmd/main.go"},
|
||||
{Path: "internal/api/client.go"},
|
||||
}}
|
||||
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")})
|
||||
m = updated.(App)
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("cmd")})
|
||||
m = updated.(App)
|
||||
if !m.searching || m.threadIndex != 0 {
|
||||
t.Fatalf("search did not select cmd/main.go: searching=%v index=%d", m.searching, m.threadIndex)
|
||||
}
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
||||
m = updated.(App)
|
||||
if m.searching || m.threadIndex != 1 {
|
||||
t.Fatalf("cancel did not restore original selection: searching=%v index=%d", m.searching, m.threadIndex)
|
||||
}
|
||||
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")})
|
||||
m = updated.(App)
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("cmd")})
|
||||
m = updated.(App)
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
m = updated.(App)
|
||||
if m.searching || m.threadIndex != 0 {
|
||||
t.Fatalf("enter did not keep search jump: searching=%v index=%d", m.searching, m.threadIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeReadyRequiresApproval(t *testing.T) {
|
||||
notApproved := reviewAndMergeState(PRDetails{Mergeable: "MERGEABLE", ReviewDecision: "REVIEW_REQUIRED"})
|
||||
if strings.Contains(notApproved, "merge: ready") {
|
||||
@@ -274,3 +420,48 @@ func TestAuthorColorsAreVariedAndDeterministic(t *testing.T) {
|
||||
t.Fatalf("author palette did not vary: %#v", colors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupedPRRowsAddsRepositoryHeadings(t *testing.T) {
|
||||
prs := []PullRequest{
|
||||
{RepoWithOwner: "alpha/one", Number: 1},
|
||||
{RepoWithOwner: "alpha/one", Number: 2},
|
||||
{RepoWithOwner: "beta/two", Number: 1},
|
||||
}
|
||||
rows, selectedRow := groupedPRRows(prs, 2)
|
||||
if len(rows) != 5 {
|
||||
t.Fatalf("row count = %d, want 5: %#v", len(rows), rows)
|
||||
}
|
||||
if !rows[0].header || rows[0].repository != "alpha/one" ||
|
||||
!rows[3].header || rows[3].repository != "beta/two" {
|
||||
t.Fatalf("repository headings are incorrect: %#v", rows)
|
||||
}
|
||||
if selectedRow != 4 || rows[selectedRow].prIndex != 2 {
|
||||
t.Fatalf("selected row = %d, want PR row 4", selectedRow)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailsLoadUsesSelectedPRRepository(t *testing.T) {
|
||||
service := &recordingService{}
|
||||
m := NewApp(service, "", "", false, 50, 10*time.Second)
|
||||
pr := PullRequest{Owner: "other-owner", Repository: "other-repo", Number: 17}
|
||||
msg := m.loadDetails(pr)().(detailsLoadedMsg)
|
||||
|
||||
if service.owner != pr.Owner || service.repo != pr.Repository || service.number != pr.Number {
|
||||
t.Fatalf("detail request used %s/%s#%d", service.owner, service.repo, service.number)
|
||||
}
|
||||
if msg.owner != pr.Owner || msg.repo != pr.Repository || msg.number != pr.Number {
|
||||
t.Fatalf("detail response identity = %s/%s#%d", msg.owner, msg.repo, msg.number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeopleMetadataUsesColoredHandles(t *testing.T) {
|
||||
assignees := handlesText([]string{"alice"})
|
||||
reviewers := reviewersText([]Reviewer{{Login: "bob", State: "APPROVED"}})
|
||||
if ansi.Strip(assignees) != "@alice" || ansi.Strip(reviewers) != "@bob (approved)" {
|
||||
t.Fatalf("people metadata = %q / %q", ansi.Strip(assignees), ansi.Strip(reviewers))
|
||||
}
|
||||
if authorStyle("alice").GetForeground() != authorColor("alice") ||
|
||||
authorStyle("bob").GetForeground() != authorColor("bob") {
|
||||
t.Fatal("people handles do not use the deterministic author color")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user