243 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|