update QoL and ai integration
This commit is contained in:
159
tui_test.go
159
tui_test.go
@@ -13,6 +13,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
type recordingService struct {
|
||||
@@ -1247,11 +1248,12 @@ func TestResolveToggleConfirmsAndUsesCurrentThreadState(t *testing.T) {
|
||||
|
||||
func TestPollingMarksNewThreadCommentsUnread(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen = threadScreen
|
||||
m.screen, m.width, m.height = threadScreen, 80, 8
|
||||
initial := PRDetails{
|
||||
PullRequest: PullRequest{ID: "pr", Number: 1},
|
||||
Threads: []ReviewThread{{
|
||||
ID: "thread", Path: "a.go", Comments: []ReviewComment{{ID: "comment-1"}},
|
||||
ID: "thread", Path: "a.go",
|
||||
Comments: []ReviewComment{{ID: "comment-1", Author: "alice", Body: "Original"}},
|
||||
}},
|
||||
}
|
||||
updated, _ := m.Update(detailsLoadedMsg{number: 1, details: initial})
|
||||
@@ -1263,22 +1265,137 @@ func TestPollingMarksNewThreadCommentsUnread(t *testing.T) {
|
||||
refreshed := initial
|
||||
refreshed.Threads = []ReviewThread{{
|
||||
ID: "thread", Path: "a.go",
|
||||
Comments: []ReviewComment{{ID: "comment-1"}, {ID: "comment-2"}},
|
||||
Comments: []ReviewComment{
|
||||
{ID: "comment-1", Author: "alice", Body: "Original"},
|
||||
{ID: "comment-2", Author: "bob", Body: "New reply"},
|
||||
},
|
||||
}}
|
||||
updated, _ = m.Update(detailsLoadedMsg{number: 1, details: refreshed})
|
||||
m = updated.(App)
|
||||
if !m.unreadThreads["thread"] {
|
||||
if !m.unreadThreads["thread"] || !m.unreadComments["comment-2"] ||
|
||||
m.newThreads["thread"] {
|
||||
t.Fatal("new review comment was not marked unread")
|
||||
}
|
||||
plain := ansi.Strip(m.threadList(48, 10))
|
||||
if !strings.Contains(plain, "NEW") {
|
||||
t.Fatalf("thread list does not indicate unread update:\n%s", plain)
|
||||
}
|
||||
detail := m.detailLines(60)
|
||||
dividerFound, emphasizedRail := false, false
|
||||
for _, line := range detail {
|
||||
if strings.Contains(ansi.Strip(line.text), "NEW MESSAGES") {
|
||||
dividerFound = true
|
||||
}
|
||||
if line.anchor == "comment:comment-2:header" && ansi.Strip(line.rail) == "┃ " {
|
||||
emphasizedRail = true
|
||||
}
|
||||
}
|
||||
if !dividerFound || !emphasizedRail {
|
||||
t.Fatalf("unread detail treatment missing: divider=%t rail=%t", dividerFound, emphasizedRail)
|
||||
}
|
||||
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
|
||||
m = updated.(App)
|
||||
if !m.unreadThreads["thread"] {
|
||||
t.Fatal("moving the thread-list cursor marked the thread read")
|
||||
}
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")})
|
||||
m = updated.(App)
|
||||
if !m.unreadThreads["thread"] {
|
||||
t.Fatal("focusing the thread detail marked the thread read")
|
||||
}
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("n")})
|
||||
m = updated.(App)
|
||||
if !m.unreadThreads["thread"] || m.focus != threadDetailPane ||
|
||||
m.detailScrollAnchor() != "unread:comment-2" {
|
||||
t.Fatal("next-unread did not preserve and position the unread update")
|
||||
}
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
|
||||
m = updated.(App)
|
||||
if m.unreadThreads["thread"] || m.unreadComments["comment-2"] {
|
||||
t.Fatal("scrolling the visible final unread comment did not mark the thread read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnreadThreadClearsOnlyWhenLastUnreadCommentIsVisible(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen, m.focus, m.listHidden = threadScreen, threadDetailPane, true
|
||||
m.width, m.height = 60, 8
|
||||
m.details = PRDetails{
|
||||
PullRequest: PullRequest{ID: "pr", Number: 1},
|
||||
Threads: []ReviewThread{{
|
||||
ID: "thread", Path: "a.go",
|
||||
Comments: []ReviewComment{
|
||||
{ID: "old", Author: "alice", Body: "Old"},
|
||||
{ID: "new-1", Author: "bob", Body: strings.Repeat("First new message ", 8)},
|
||||
{ID: "new-2", Author: "carol", Body: "Last new message"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
m.unreadThreads["thread"] = true
|
||||
m.unreadComments["new-1"] = true
|
||||
m.unreadComments["new-2"] = true
|
||||
|
||||
lines := m.renderedDetailLines(m.width)
|
||||
firstHeader, lastHeader := -1, -1
|
||||
for index, line := range lines {
|
||||
switch line.anchor {
|
||||
case "comment:new-1:header":
|
||||
firstHeader = index
|
||||
case "comment:new-2:header":
|
||||
lastHeader = index
|
||||
}
|
||||
}
|
||||
if firstHeader < 0 || lastHeader <= firstHeader {
|
||||
t.Fatalf("unread comment anchors = %d, %d", firstHeader, lastHeader)
|
||||
}
|
||||
m.scroll = firstHeader
|
||||
m.acknowledgeVisibleUnread()
|
||||
if !m.unreadThreads["thread"] {
|
||||
t.Fatal("thread was read before the last unread comment became visible")
|
||||
}
|
||||
|
||||
m.scroll = max(0, lastHeader-m.detailViewportHeight()+1)
|
||||
m.acknowledgeVisibleUnread()
|
||||
if m.unreadThreads["thread"] {
|
||||
t.Fatal("visiting unread thread did not mark it read")
|
||||
t.Fatal("thread remained unread after the last unread comment became visible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualMarkReadIsFallbackForNewThread(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen, m.width, m.height = threadScreen, 80, 20
|
||||
initial := PRDetails{
|
||||
PullRequest: PullRequest{ID: "pr", Number: 1},
|
||||
Threads: []ReviewThread{{
|
||||
ID: "existing", Path: "a.go",
|
||||
Comments: []ReviewComment{{ID: "existing-comment"}},
|
||||
}},
|
||||
}
|
||||
m.trackThreadUpdates(initial)
|
||||
updatedDetails := initial
|
||||
updatedDetails.Threads = append(updatedDetails.Threads, ReviewThread{
|
||||
ID: "new-thread", Path: "b.go",
|
||||
Comments: []ReviewComment{{ID: "new-root", Author: "alice", Body: "New thread"}},
|
||||
})
|
||||
m.trackThreadUpdates(updatedDetails)
|
||||
m.details = updatedDetails
|
||||
m.threadIndex = 1
|
||||
|
||||
if !m.newThreads["new-thread"] || !m.unreadComments["new-root"] ||
|
||||
!strings.Contains(ansi.Strip(m.threadList(60, 10)), "NEW THREAD") {
|
||||
t.Fatal("new thread did not receive the distinct unread treatment")
|
||||
}
|
||||
for _, line := range m.detailLines(60) {
|
||||
if strings.Contains(ansi.Strip(line.text), "NEW MESSAGES") {
|
||||
t.Fatal("completely new thread received a partial-update divider")
|
||||
}
|
||||
}
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("m")})
|
||||
m = updated.(App)
|
||||
if m.unreadThreads["new-thread"] || m.unreadComments["new-root"] ||
|
||||
m.newThreads["new-thread"] {
|
||||
t.Fatal("manual mark-read fallback did not clear the selected thread")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1766,6 +1883,38 @@ func TestThreadDetailShowsCommentReactions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestThreadDetailHighlightsContributorMentions(t *testing.T) {
|
||||
defer applyTheme("dark")
|
||||
if err := applyTheme("dark"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previousProfile := lipgloss.ColorProfile()
|
||||
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||
t.Cleanup(func() { lipgloss.SetColorProfile(previousProfile) })
|
||||
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.width = 80
|
||||
m.details = PRDetails{Threads: []ReviewThread{{
|
||||
ID: "thread", Path: "main.go", Comments: []ReviewComment{{
|
||||
ID: "comment", Author: "reviewer",
|
||||
Body: "Could @pablu and @other-contributor check this?",
|
||||
}},
|
||||
}}}
|
||||
|
||||
var body strings.Builder
|
||||
for _, line := range m.detailLines(70) {
|
||||
if strings.Contains(line.anchor, ":body:") {
|
||||
body.WriteString(line.text)
|
||||
}
|
||||
}
|
||||
for _, login := range []string{"pablu", "other-contributor"} {
|
||||
mention := "@" + login
|
||||
if !strings.Contains(body.String(), authorStyle(login).Render(mention)) {
|
||||
t.Fatalf("thread mention %s is not author-styled: %q", mention, body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReactionSummaryWrapsBetweenBadges(t *testing.T) {
|
||||
reactions := []ReactionSummary{
|
||||
{Content: "THUMBS_UP", Count: 10},
|
||||
|
||||
Reference in New Issue
Block a user