Files
superwork/tui/internal/tui/prcommits_test.go
T
2026-06-23 05:02:15 +08:00

243 lines
6.6 KiB
Go

package tui
import (
"strings"
"testing"
tea "charm.land/bubbletea/v2"
"superwork-tui/internal/gitea"
"superwork-tui/internal/issue"
)
func sendPRKey(m Model, key string) (Model, tea.Cmd) {
var msg tea.KeyMsg
switch key {
case "esc":
msg = keyMsg("esc")
case "enter":
msg = keyMsg("enter")
case " ":
msg = keyMsg(" ")
default:
msg = keyMsg(key)
}
next, cmd := m.Update(msg)
return next.(Model), cmd
}
func sampleCommits() []gitea.PullRequestCommit {
c1 := gitea.PullRequestCommit{SHA: "aaaaaaa111"}
c1.Commit.Message = "feat: add thing\n\nbody"
c2 := gitea.PullRequestCommit{SHA: "bbbbbbb222"}
c2.Commit.Message = "fix: bug"
return []gitea.PullRequestCommit{c1, c2}
}
func sampleFiles() []gitea.PullRequestFile {
return []gitea.PullRequestFile{
{Filename: "src/a.go", Status: "modified"},
{Filename: "src/b.go", Status: "added"},
}
}
func loadedPRModel() Model {
m := New()
iss := issue.Issue{Number: 12, PR: "7", WorktreePath: "wt/12"}
m.prCommitsIss = &iss
m.state = statePRCommits
m.prCommits = sampleCommits()
m.prFiles = sampleFiles()
m.prCommitsLoaded = true
m.prConfirmed = map[string]bool{}
m.prConfirmedSHAs = map[string]bool{}
return m
}
func TestPRCommitsView_ShowsCommitsAndFiles(t *testing.T) {
m := loadedPRModel()
out := m.prCommitsView()
for _, want := range []string{
"PR #7",
"feat: add thing", // first line of message only
"fix: bug",
"aaaaaaa", // short sha
"src/a.go",
"src/b.go",
"modified",
"added",
} {
if !strings.Contains(out, want) {
t.Errorf("view missing %q\n---\n%s", want, out)
}
}
if strings.Contains(out, "body") {
t.Errorf("view should show only the first line of the commit message")
}
}
func TestPRCommitsView_FirstLineOfMessageOnly(t *testing.T) {
m := loadedPRModel()
out := m.prCommitsView()
if strings.Contains(out, "feat: add thing\n\nbody") {
t.Errorf("multi-line message leaked into view")
}
}
func TestPRCommitsView_Loading(t *testing.T) {
m := New()
iss := issue.Issue{Number: 12, PR: "7"}
m.prCommitsIss = &iss
m.state = statePRCommits
m.prCommitsLoaded = false
out := m.prCommitsView()
if !strings.Contains(out, "加载") && !strings.Contains(out, "loading") {
t.Errorf("loading view missing loading indicator: %s", out)
}
}
func TestPRCommitsView_Error(t *testing.T) {
m := loadedPRModel()
m.prCommitsErr = errStub("boom")
out := m.prCommitsView()
if !strings.Contains(out, "boom") {
t.Errorf("error view missing message: %s", out)
}
}
type errStub string
func (e errStub) Error() string { return string(e) }
// --- checkbox state machine -------------------------------------------------
func TestPRCommits_SpaceTogglesFileConfirm(t *testing.T) {
m := loadedPRModel()
m.prCursor = len(m.prCommits) // first file row
m2, _ := sendPRKey(m, " ")
if !m2.prConfirmed["src/a.go"] {
t.Errorf("space should confirm src/a.go; confirmed=%v", m2.prConfirmed)
}
out := m2.prCommitsView()
if !strings.Contains(out, "[x] src/a.go") {
t.Errorf("confirmed file should render [x]: %s", out)
}
// Toggle off.
m3, _ := sendPRKey(m2, " ")
if m3.prConfirmed["src/a.go"] {
t.Errorf("space again should unconfirm src/a.go; confirmed=%v", m3.prConfirmed)
}
}
func TestPRCommits_SpaceTogglesCommitConfirm(t *testing.T) {
m := loadedPRModel()
m.prCursor = 0 // first commit row
m2, _ := sendPRKey(m, " ")
if !m2.prConfirmedSHAs["aaaaaaa111"] {
t.Errorf("space should confirm commit aaaaaaa111; confirmed=%v", m2.prConfirmedSHAs)
}
out := m2.prCommitsView()
if !strings.Contains(out, "[x] aaaaaaa") {
t.Errorf("confirmed commit should render [x]: %s", out)
}
}
func TestPRCommits_CursorSpansCommitsThenFiles(t *testing.T) {
m := loadedPRModel()
// Two commits then two files: cursor index 2 is the first file.
m2, _ := sendPRKey(m, "j") // -> commit 1
m3, _ := sendPRKey(m2, "j") // -> file 0
if m3.prCursor != 2 {
t.Errorf("prCursor = %d, want 2 (first file)", m3.prCursor)
}
m4, _ := sendPRKey(m3, " ")
if !m4.prConfirmed["src/a.go"] {
t.Errorf("space should confirm src/a.go; confirmed=%v", m4.prConfirmed)
}
// Cursor cannot move past the last row.
m5, _ := sendPRKey(m4, "j") // -> file 1 (index 3, last)
m6, _ := sendPRKey(m5, "j") // clamp
if m6.prCursor != 3 {
t.Errorf("prCursor = %d, want 3 (clamped at last row)", m6.prCursor)
}
}
func TestPRCommits_EscReturnsToDetail(t *testing.T) {
m := loadedPRModel()
m2, _ := sendPRKey(m, "esc")
if m2.state != stateDetail {
t.Errorf("state = %v, want stateDetail", m2.state)
}
}
func TestPRCommits_DiffMissingWorktreeSetsStatus(t *testing.T) {
m := loadedPRModel()
m.prCursor = len(m.prCommits) // first file row
// WorktreePath "wt/12" does not exist under any workspace root.
m2, cmd := sendPRKey(m, "enter")
if cmd != nil {
t.Errorf("expected no exec cmd when worktree missing, got %v", cmd)
}
if !strings.Contains(m2.statusMsg, "worktree") && !strings.Contains(m2.statusMsg, "工作树") {
t.Errorf("statusMsg should report missing worktree, got %q", m2.statusMsg)
}
}
// --- aggregation pure fn ----------------------------------------------------
func TestAllFilesConfirmed(t *testing.T) {
files := sampleFiles()
cases := []struct {
name string
confirmed map[string]bool
want bool
}{
{"none", map[string]bool{}, false},
{"partial", map[string]bool{"src/a.go": true}, false},
{"all", map[string]bool{"src/a.go": true, "src/b.go": true}, true},
{"extra-irrelevant", map[string]bool{"src/a.go": true, "src/b.go": true, "x": true}, true},
}
for _, tc := range cases {
if got := allFilesConfirmed(files, tc.confirmed); got != tc.want {
t.Errorf("%s: allFilesConfirmed = %v, want %v", tc.name, got, tc.want)
}
}
}
func TestAllFilesConfirmed_EmptyFiles(t *testing.T) {
if allFilesConfirmed(nil, map[string]bool{}) {
t.Errorf("empty file set should not count as all-confirmed")
}
}
func TestDiffBaseRef_UsesPRBaseRefWhenSet(t *testing.T) {
got := diffBaseRef("release/v2")
want := "origin/release/v2"
if got != want {
t.Errorf("diffBaseRef(%q) = %q, want %q", "release/v2", got, want)
}
}
func TestDiffBaseRef_FallsBackToDevBranchWhenEmpty(t *testing.T) {
got := diffBaseRef("")
// With no config file present in tests, falls back to "main".
want := "origin/main"
if got != want {
t.Errorf("diffBaseRef(%q) = %q, want %q", "", got, want)
}
}
func TestSortedTrue_OutputIsSorted(t *testing.T) {
input := map[string]bool{"b": true, "a": true, "c": true, "d": false}
got := sortedTrue(input)
want := []string{"a", "b", "c"}
if len(got) != len(want) {
t.Fatalf("sortedTrue len = %d, want %d; got %v", len(got), len(want), got)
}
for i, v := range want {
if got[i] != v {
t.Errorf("sortedTrue[%d] = %q, want %q; full: %v", i, got[i], v, got)
}
}
}