initial working read only state
This commit is contained in:
276
tui_test.go
Normal file
276
tui_test.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
func TestResolvedThreadsStartFolded(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10)
|
||||
m.details = PRDetails{PullRequest: PullRequest{Number: 7}}
|
||||
updated, _ := m.Update(detailsLoadedMsg{number: 7, details: PRDetails{
|
||||
PullRequest: PullRequest{Number: 7},
|
||||
Threads: []ReviewThread{{ID: "open"}, {ID: "done", IsResolved: true}},
|
||||
}})
|
||||
got := updated.(App)
|
||||
if got.folded["open"] {
|
||||
t.Fatal("open thread was folded")
|
||||
}
|
||||
if !got.folded["done"] {
|
||||
t.Fatal("resolved thread was not folded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectionSurvivesRefresh(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10)
|
||||
m.details = PRDetails{
|
||||
PullRequest: PullRequest{Number: 7},
|
||||
Threads: []ReviewThread{{ID: "a"}, {ID: "b"}},
|
||||
}
|
||||
m.threadIndex = 1
|
||||
updated, _ := m.Update(detailsLoadedMsg{number: 7, details: PRDetails{
|
||||
PullRequest: PullRequest{Number: 7},
|
||||
Threads: []ReviewThread{{ID: "new"}, {ID: "a"}, {ID: "b"}},
|
||||
}})
|
||||
if got := updated.(App).threadIndex; got != 2 {
|
||||
t.Fatalf("thread index = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaneFocusAndNavigation(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen = threadScreen
|
||||
m.width, m.height = 100, 12
|
||||
m.details = PRDetails{
|
||||
PullRequest: PullRequest{Number: 7},
|
||||
Threads: []ReviewThread{
|
||||
{ID: "a", Path: "a.go", Comments: []ReviewComment{{Body: strings.Repeat("word ", 100)}}},
|
||||
{ID: "b", Path: "b.go"},
|
||||
},
|
||||
}
|
||||
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
|
||||
m = updated.(App)
|
||||
if m.threadIndex != 1 {
|
||||
t.Fatalf("thread index = %d", m.threadIndex)
|
||||
}
|
||||
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("h")})
|
||||
m = updated.(App)
|
||||
if m.screen != threadScreen || m.focus != threadListPane {
|
||||
t.Fatal("h should focus the list without leaving the thread screen")
|
||||
}
|
||||
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")})
|
||||
m = updated.(App)
|
||||
if m.focus != threadDetailPane {
|
||||
t.Fatal("l did not focus the detail pane")
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewNeverExceedsTerminalWidth(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen = threadScreen
|
||||
m.width, m.height = 90, 18
|
||||
m.details = PRDetails{
|
||||
PullRequest: PullRequest{Number: 7, Title: strings.Repeat("title", 50)},
|
||||
Threads: []ReviewThread{{
|
||||
ID: "a",
|
||||
Path: strings.Repeat("very-long-directory/", 8) + "important_filename.go",
|
||||
Comments: []ReviewComment{{
|
||||
DiffHunk: "@@ -1 +1 @@\n+" + strings.Repeat("reallyLongIdentifier", 20),
|
||||
Body: strings.Repeat("comment ", 100),
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, line := range strings.Split(m.View(), "\n") {
|
||||
if got := ansi.StringWidth(line); got > m.width {
|
||||
t.Fatalf("line %d width = %d, terminal width = %d", i, got, m.width)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncatePathPreservesFilename(t *testing.T) {
|
||||
got := truncatePath("a/very/long/directory/important_filename.go", 22)
|
||||
if !strings.HasSuffix(got, "important_filename.go") {
|
||||
t.Fatalf("truncated path %q lost filename", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeReadyRequiresApproval(t *testing.T) {
|
||||
notApproved := reviewAndMergeState(PRDetails{Mergeable: "MERGEABLE", ReviewDecision: "REVIEW_REQUIRED"})
|
||||
if strings.Contains(notApproved, "merge: ready") {
|
||||
t.Fatalf("unapproved PR shown as ready: %q", notApproved)
|
||||
}
|
||||
approved := reviewAndMergeState(PRDetails{Mergeable: "MERGEABLE", ReviewDecision: "APPROVED"})
|
||||
if !strings.Contains(approved, "merge: ready") {
|
||||
t.Fatalf("approved PR not shown as ready: %q", approved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThreadCommentCountsUseSameColumn(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.width, m.height = 100, 20
|
||||
m.details = PRDetails{Threads: []ReviewThread{
|
||||
{ID: "short", Path: "a.go", Line: 12, Comments: make([]ReviewComment, 2)},
|
||||
{ID: "long", Path: "a/very/long/directory/with/a/descriptive/important_filename.go", Line: 12, Comments: make([]ReviewComment, 2)},
|
||||
}}
|
||||
plain := ansi.Strip(m.threadList(48, 10))
|
||||
var columns []int
|
||||
for _, line := range strings.Split(plain, "\n") {
|
||||
if column := strings.Index(line, "· 2"); column >= 0 {
|
||||
columns = append(columns, ansi.StringWidth(line[:column]))
|
||||
}
|
||||
}
|
||||
if len(columns) != 2 || columns[0] != columns[1] {
|
||||
t.Fatalf("comment columns = %v\n%s", columns, plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewAnchorUsesCommentSnapshotCoordinates(t *testing.T) {
|
||||
thread := ReviewThread{
|
||||
Line: 150,
|
||||
StartLine: 148,
|
||||
Comments: []ReviewComment{{
|
||||
Line: 150,
|
||||
StartLine: 148,
|
||||
OriginalLine: 42,
|
||||
OriginalStartLine: 40,
|
||||
Outdated: true,
|
||||
}},
|
||||
}
|
||||
start, end := reviewAnchor(thread)
|
||||
if start != 40 || end != 42 {
|
||||
t.Fatalf("anchor = %d-%d, want original snapshot range 40-42", start, end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutdatedDetailHighlightsOriginalCodeRange(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.width = 100
|
||||
m.details = PRDetails{Threads: []ReviewThread{{
|
||||
Path: "main.go",
|
||||
Line: 150,
|
||||
StartLine: 148,
|
||||
DiffSide: "RIGHT",
|
||||
IsOutdated: true,
|
||||
Comments: []ReviewComment{{
|
||||
DiffHunk: "@@ -38,7 +38,7 @@\n old 38\n old 39\n reviewed 40\n reviewed 41\n reviewed 42\n old 43\n old 44",
|
||||
OriginalLine: 42,
|
||||
OriginalStartLine: 40,
|
||||
OriginalCommitOID: "0123456789abcdef",
|
||||
}},
|
||||
}}}
|
||||
|
||||
var rendered strings.Builder
|
||||
selected := 0
|
||||
for _, line := range m.detailLines(80) {
|
||||
rendered.WriteString(ansi.Strip(line.fixed + line.text))
|
||||
rendered.WriteByte('\n')
|
||||
if line.selected {
|
||||
selected++
|
||||
}
|
||||
}
|
||||
output := rendered.String()
|
||||
if selected != 3 || !strings.Contains(output, "reviewed 40") ||
|
||||
!strings.Contains(output, "snapshot 0123456") {
|
||||
t.Fatalf("detail was not anchored to the original snapshot:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabHidesListAndHRestoresIt(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen = threadScreen
|
||||
m.width, m.height = 100, 20
|
||||
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyTab})
|
||||
m = updated.(App)
|
||||
if !m.listHidden || m.focus != threadDetailPane {
|
||||
t.Fatal("tab did not hide the list and focus detail")
|
||||
}
|
||||
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("h")})
|
||||
m = updated.(App)
|
||||
if m.listHidden || m.focus != threadListPane {
|
||||
t.Fatal("h did not reveal and focus the list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectedBackgroundFillsLine(t *testing.T) {
|
||||
rendered := selectedBackground("code", 12)
|
||||
if !strings.Contains(rendered, "\x1b[48;5;24m") || ansi.StringWidth(rendered) != 12 {
|
||||
t.Fatalf("selected background was not full width: %q", rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrappedDiffContinuationKeepsIndentWithoutLineNumber(t *testing.T) {
|
||||
original := " some_really_long_function_call(argument)"
|
||||
lines := wrapDiffLine(highlightedDiffLine{
|
||||
gutter: " 10 + ",
|
||||
code: original,
|
||||
selected: true,
|
||||
}, 22)
|
||||
if len(lines) < 2 {
|
||||
t.Fatalf("long code was not wrapped: %#v", lines)
|
||||
}
|
||||
|
||||
for i, line := range lines {
|
||||
if ansi.StringWidth(line.fixed+line.text) > 22 {
|
||||
t.Fatalf("wrapped line %d exceeds width: %q", i, line.fixed+line.text)
|
||||
}
|
||||
if !line.selected {
|
||||
t.Fatalf("continuation line %d lost selection state", i)
|
||||
}
|
||||
if i == 0 {
|
||||
if !strings.Contains(line.fixed, "10") {
|
||||
t.Fatalf("first line lost its line number: %q", line.fixed)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(line.fixed) != "" {
|
||||
t.Fatalf("continuation repeated a line number: %q", line.fixed)
|
||||
}
|
||||
plain := ansi.Strip(line.text)
|
||||
if !strings.HasPrefix(plain, " ↳ ") {
|
||||
t.Fatalf("continuation lacks extra indent and marker: %q", plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodeWrapPrefersSyntaxBoundaries(t *testing.T) {
|
||||
code := " result = client.fetch(first_argument, second_argument)"
|
||||
lines := wrapCodeWithIndent(code, 38)
|
||||
if len(lines) < 2 {
|
||||
t.Fatalf("code was not wrapped: %#v", lines)
|
||||
}
|
||||
plain := make([]string, len(lines))
|
||||
for i, line := range lines {
|
||||
plain[i] = ansi.Strip(line)
|
||||
}
|
||||
rendered := strings.Join(plain, "\n")
|
||||
if !strings.Contains(rendered, "first_argument") || !strings.Contains(rendered, "second_argument") {
|
||||
t.Fatalf("wrapper split identifiers despite syntax boundaries:\n%s", rendered)
|
||||
}
|
||||
if !strings.Contains(rendered, "↳ ") {
|
||||
t.Fatalf("continuation marker missing:\n%s", rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorColorsAreVariedAndDeterministic(t *testing.T) {
|
||||
if authorColor("Alice") != authorColor("alice") {
|
||||
t.Fatal("same login received different colors")
|
||||
}
|
||||
colors := map[lipgloss.Color]bool{}
|
||||
for _, login := range []string{"alice", "bob", "carol", "dave", "eve", "frank"} {
|
||||
colors[authorColor(login)] = true
|
||||
}
|
||||
if len(colors) < 2 {
|
||||
t.Fatalf("author palette did not vary: %#v", colors)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user