kunchenguid--no-mistakes
26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
588 行
20 KiB
Go
588 行
20 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/kunchenguid/no-mistakes/internal/ipc"
|
|
"github.com/kunchenguid/no-mistakes/internal/types"
|
|
"github.com/muesli/termenv"
|
|
)
|
|
|
|
func TestStaleShowDiff_ResetWhenNewFindingsArrive(t *testing.T) {
|
|
// Bug: if user was viewing diff for step A, then step B arrives with
|
|
// findings but no diff data, showDiff stays true from step A.
|
|
// This causes View() to show neither diff nor findings - a blank state.
|
|
// Fix: reset showDiff when new findings arrive via EventStepCompleted.
|
|
configureTUIColors()
|
|
run := testRun()
|
|
m := NewModel("/tmp/sock", nil, run)
|
|
m.width = 80
|
|
m.height = 50
|
|
|
|
// Simulate: Review step has findings + diff, user toggled showDiff on.
|
|
m.steps[0].Status = types.StepStatusAwaitingApproval
|
|
m.stepFindings[types.StepReview] = `{"items":[{"id":"f1","severity":"error","file":"a.go","line":1,"description":"bad"}]}`
|
|
m.stepDiffs[types.StepReview] = "diff --git a/a.go b/a.go\n--- a/a.go\n+++ b/a.go\n@@ -1 +1 @@\n-old\n+new\n"
|
|
m.showDiff = true
|
|
|
|
// Now: user approves Review, Test step completes with findings but NO diff.
|
|
m.steps[0].Status = types.StepStatusCompleted
|
|
m.steps[1].Status = types.StepStatusAwaitingApproval
|
|
findingsJSON := `{"items":[{"id":"f2","severity":"warning","file":"b.go","line":5,"description":"unused var"}]}`
|
|
status := string(types.StepStatusAwaitingApproval)
|
|
stepName := types.StepTest
|
|
m.applyEvent(ipc.Event{
|
|
Type: ipc.EventStepCompleted,
|
|
StepName: &stepName,
|
|
Status: &status,
|
|
Findings: &findingsJSON,
|
|
// No Diff field - this step has no diff data.
|
|
})
|
|
|
|
if m.showDiff {
|
|
t.Error("showDiff should be reset to false when new findings arrive without diff data")
|
|
}
|
|
|
|
// Verify the findings are actually visible in the View.
|
|
view := stripANSI(m.View())
|
|
if !strings.Contains(view, "unused var") {
|
|
t.Errorf("expected Test findings to be visible in view, got:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestStaleShowDiff_FindingsVisibleAfterStepTransition(t *testing.T) {
|
|
// Even when showDiff was true from a previous step, the new step's
|
|
// findings should be shown (not hidden by stale diff state).
|
|
configureTUIColors()
|
|
run := testRun()
|
|
m := NewModel("/tmp/sock", nil, run)
|
|
m.width = 80
|
|
m.height = 50
|
|
|
|
// Setup: user was viewing diff for Review.
|
|
m.steps[0].Status = types.StepStatusCompleted
|
|
m.steps[1].Status = types.StepStatusAwaitingApproval
|
|
m.stepFindings[types.StepTest] = `{"summary":"Test issues","items":[{"id":"t1","severity":"error","file":"test.go","line":10,"description":"missing assertion"}]}`
|
|
m.resetFindingSelection(types.StepTest)
|
|
m.showDiff = true // stale from previous step
|
|
|
|
// Apply the event that should reset showDiff.
|
|
findingsJSON := m.stepFindings[types.StepTest]
|
|
status := string(types.StepStatusAwaitingApproval)
|
|
stepName := types.StepTest
|
|
m.applyEvent(ipc.Event{
|
|
Type: ipc.EventStepCompleted,
|
|
StepName: &stepName,
|
|
Status: &status,
|
|
Findings: &findingsJSON,
|
|
})
|
|
|
|
view := stripANSI(m.View())
|
|
if !strings.Contains(view, "Findings -") {
|
|
t.Errorf("expected findings box to be visible, got:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestStaleShowDiff_DiffResetAlsoResetsOffset(t *testing.T) {
|
|
// When showDiff is reset due to new findings, diffOffset should also reset
|
|
// to prevent stale scroll position carrying over.
|
|
configureTUIColors()
|
|
run := testRun()
|
|
m := NewModel("/tmp/sock", nil, run)
|
|
m.showDiff = true
|
|
m.diffOffset = 42 // stale offset from previous diff
|
|
|
|
findingsJSON := `{"items":[{"id":"f1","severity":"info","file":"c.go","line":1,"description":"note"}]}`
|
|
status := string(types.StepStatusAwaitingApproval)
|
|
stepName := types.StepTest
|
|
m.applyEvent(ipc.Event{
|
|
Type: ipc.EventStepCompleted,
|
|
StepName: &stepName,
|
|
Status: &status,
|
|
Findings: &findingsJSON,
|
|
})
|
|
|
|
if m.diffOffset != 0 {
|
|
t.Errorf("diffOffset should be reset to 0 when new findings arrive, got %d", m.diffOffset)
|
|
}
|
|
}
|
|
|
|
func TestApplyEvent_NewApprovalPayloadClearsPreviousOverrides(t *testing.T) {
|
|
run := testRun()
|
|
m := NewModel("", nil, run)
|
|
m.stepFindings[types.StepReview] = `{"findings":[{"id":"review-1","severity":"error","description":"old","action":"auto-fix"}],"summary":"1"}`
|
|
m.findingInstructions[types.StepReview] = map[string]string{"review-1": "old note"}
|
|
m.addedFindings[types.StepReview] = []types.Finding{{ID: "user-1", Severity: "info", Description: "old added", Source: types.FindingSourceUser, Action: types.ActionAutoFix}}
|
|
m.findingSelections[types.StepReview] = map[string]bool{"review-1": true, "user-1": true}
|
|
|
|
findingsJSON := `{"findings":[{"id":"review-2","severity":"warning","description":"new","action":"auto-fix"}],"summary":"1"}`
|
|
status := string(types.StepStatusAwaitingApproval)
|
|
stepName := types.StepReview
|
|
m.applyEvent(ipc.Event{Type: ipc.EventStepCompleted, StepName: &stepName, Status: &status, Findings: &findingsJSON})
|
|
|
|
if _, ok := m.findingInstructions[types.StepReview]; ok {
|
|
t.Fatal("expected stale finding instructions to be cleared")
|
|
}
|
|
if len(m.addedFindings[types.StepReview]) != 0 {
|
|
t.Fatalf("expected stale user-added findings to be cleared, got %d", len(m.addedFindings[types.StepReview]))
|
|
}
|
|
selected := m.findingSelections[types.StepReview]
|
|
if len(selected) != 1 || !selected["review-2"] {
|
|
t.Fatalf("expected selection reset to only new finding, got %#v", selected)
|
|
}
|
|
}
|
|
|
|
func TestActionBar_ShowsDiffWhenDiffDataExists(t *testing.T) {
|
|
// The action bar SHOULD show 'd diff' when diff data exists for the current step.
|
|
configureTUIColors()
|
|
run := testRun()
|
|
run.Steps[0].Status = types.StepStatusAwaitingApproval
|
|
m := NewModel("", nil, run)
|
|
m.width = 80
|
|
m.height = 50
|
|
m.stepFindings[types.StepReview] = `{"summary":"test","items":[{"id":"f1","severity":"error","file":"foo.go","line":1,"description":"bad"}]}`
|
|
m.stepDiffs[types.StepReview] = "diff --git a/foo.go b/foo.go\n--- a/foo.go\n+++ b/foo.go\n@@ -1 +1 @@\n-old\n+new\n"
|
|
m.resetFindingSelection(types.StepReview)
|
|
|
|
view := stripANSI(m.View())
|
|
if !strings.Contains(view, "d diff") {
|
|
t.Errorf("expected 'd diff' in action bar when diff data exists, got:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestModel_HelpAutoDismiss_NavigationKey(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
run := testRun()
|
|
run.Steps[0].Status = types.StepStatusAwaitingApproval
|
|
m := NewModel("", nil, run)
|
|
m.width = 80
|
|
m.height = 40
|
|
m.showHelp = true
|
|
m.stepFindings[types.StepReview] = `{"summary":"test","items":[{"id":"f1","severity":"error","file":"foo.go","line":1,"description":"bad"},{"id":"f2","severity":"warning","file":"bar.go","line":5,"description":"ugly"}]}`
|
|
m.resetFindingSelection(types.StepReview)
|
|
|
|
// Press j (navigation) while help is open - should dismiss help.
|
|
result, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}})
|
|
m = result.(Model)
|
|
if m.showHelp {
|
|
t.Fatal("expected help to auto-dismiss when pressing navigation key j")
|
|
}
|
|
// Navigation should still take effect - cursor should have moved.
|
|
if m.findingCursor[types.StepReview] != 1 {
|
|
t.Fatalf("expected cursor to move to 1 after j, got %d", m.findingCursor[types.StepReview])
|
|
}
|
|
}
|
|
|
|
func TestModel_HelpAutoDismiss_ActionKey(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
run := testRun()
|
|
run.Steps[0].Status = types.StepStatusAwaitingApproval
|
|
m := NewModel("", nil, run)
|
|
m.width = 80
|
|
m.height = 40
|
|
m.showHelp = true
|
|
m.stepFindings[types.StepReview] = `{"summary":"test","items":[{"id":"f1","severity":"error","file":"foo.go","line":1,"description":"bad"}]}`
|
|
m.resetFindingSelection(types.StepReview)
|
|
|
|
// Press d (toggle diff/findings) while help is open - should dismiss help.
|
|
m.stepDiffs[types.StepReview] = "diff --git a/foo.go b/foo.go\n--- a/foo.go\n+++ b/foo.go\n@@ -1 +1 @@\n-old\n+new\n"
|
|
result, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}})
|
|
m = result.(Model)
|
|
if m.showHelp {
|
|
t.Fatal("expected help to auto-dismiss when pressing action key d")
|
|
}
|
|
}
|
|
|
|
func TestModel_HelpAutoDismiss_SelectionKey(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
run := testRun()
|
|
run.Steps[0].Status = types.StepStatusAwaitingApproval
|
|
m := NewModel("", nil, run)
|
|
m.width = 80
|
|
m.height = 40
|
|
m.showHelp = true
|
|
m.stepFindings[types.StepReview] = `{"summary":"test","items":[{"id":"f1","severity":"error","file":"foo.go","line":1,"description":"bad"}]}`
|
|
m.resetFindingSelection(types.StepReview)
|
|
|
|
// Press space (toggle selection) while help is open - should dismiss help.
|
|
result, _ := m.Update(tea.KeyMsg{Type: tea.KeySpace})
|
|
m = result.(Model)
|
|
if m.showHelp {
|
|
t.Fatal("expected help to auto-dismiss when pressing selection key space")
|
|
}
|
|
}
|
|
|
|
func TestErrorDisplay_WrappedInBox(t *testing.T) {
|
|
run := testRun()
|
|
m := NewModel("/tmp/sock", nil, run)
|
|
m.width = 80
|
|
m.err = &ipc.RPCError{Code: -1, Message: "connection refused"}
|
|
view := m.View()
|
|
plain := stripANSI(view)
|
|
// Error should be in a rounded-border box.
|
|
if !strings.Contains(plain, "╭") || !strings.Contains(plain, "╯") {
|
|
t.Error("expected error to be wrapped in a box with rounded corners")
|
|
}
|
|
// The error message should appear inside the box borders.
|
|
lines := strings.Split(plain, "\n")
|
|
foundInside := false
|
|
for _, line := range lines {
|
|
if strings.Contains(line, "│") && strings.Contains(line, "connection refused") {
|
|
foundInside = true
|
|
break
|
|
}
|
|
}
|
|
if !foundInside {
|
|
t.Error("expected error message to appear inside box borders (between │ chars)")
|
|
}
|
|
}
|
|
|
|
func TestErrorDisplay_HasErrorTitle(t *testing.T) {
|
|
run := testRun()
|
|
m := NewModel("/tmp/sock", nil, run)
|
|
m.width = 80
|
|
m.err = &ipc.RPCError{Code: -1, Message: "subscribe failed"}
|
|
view := m.View()
|
|
plain := stripANSI(view)
|
|
// The box should have "Error" in the top border line.
|
|
if !strings.Contains(plain, "Error") {
|
|
t.Error("expected 'Error' title in the box top border")
|
|
}
|
|
// Specifically, "Error" should be on the same line as the top-left corner.
|
|
lines := strings.Split(plain, "\n")
|
|
found := false
|
|
for _, line := range lines {
|
|
if strings.Contains(line, "╭") && strings.Contains(line, "Error") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected 'Error' title in top border line with ╭")
|
|
}
|
|
}
|
|
|
|
func TestErrorDisplay_RedStyledMessage(t *testing.T) {
|
|
run := testRun()
|
|
m := NewModel("/tmp/sock", nil, run)
|
|
m.width = 80
|
|
m.err = &ipc.RPCError{Code: -1, Message: "event stream closed"}
|
|
view := m.View()
|
|
// Verify the error message content is styled red inside the box.
|
|
redStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(ansiRed))
|
|
// The message text itself should be red-styled inside the error box.
|
|
styledMsg := redStyle.Render("event stream closed")
|
|
if !strings.Contains(view, styledMsg) {
|
|
t.Error("expected error message text to be styled red inside the error box")
|
|
}
|
|
}
|
|
|
|
func TestModel_ApplyEvent_ClearsTransientErrorOnNextStateEvent(t *testing.T) {
|
|
run := testRun()
|
|
m := NewModel("/tmp/sock", nil, run)
|
|
m.err = &ipc.RPCError{Code: -1, Message: "no step awaiting approval"}
|
|
|
|
status := string(types.StepStatusFixReview)
|
|
stepName := types.StepReview
|
|
m.applyEvent(ipc.Event{
|
|
Type: ipc.EventStepCompleted,
|
|
StepName: &stepName,
|
|
Status: &status,
|
|
})
|
|
|
|
if m.err != nil {
|
|
t.Fatalf("expected transient error to clear on next state event, got %v", m.err)
|
|
}
|
|
}
|
|
|
|
// --- Pipeline content truncation tests ---
|
|
|
|
func TestPipelineView_LongErrorTruncated(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
run := testRun()
|
|
longError := strings.Repeat("x", 200)
|
|
run.Steps[1].Status = types.StepStatusFailed
|
|
run.Steps[1].Error = &longError
|
|
|
|
boxWidth := 80
|
|
result := renderPipelineView(run, run.Steps, boxWidth, 0, 40)
|
|
plain := stripANSI(result)
|
|
|
|
// No line in the pipeline box should exceed the box width.
|
|
for _, line := range strings.Split(plain, "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
w := lipgloss.Width(line)
|
|
if w > boxWidth {
|
|
t.Errorf("pipeline line exceeds box width (%d > %d): %s", w, boxWidth, line)
|
|
}
|
|
}
|
|
|
|
// The full long error should NOT appear - it should be truncated.
|
|
if strings.Contains(plain, longError) {
|
|
t.Error("expected long error message to be truncated to fit box content width")
|
|
}
|
|
}
|
|
|
|
func TestPipelineView_LongBranchTruncated(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
longBranch := "feature/" + strings.Repeat("very-long-name-", 10)
|
|
run := testRun()
|
|
run.Branch = longBranch
|
|
|
|
boxWidth := 80
|
|
result := renderPipelineView(run, run.Steps, boxWidth, 0, 40)
|
|
plain := stripANSI(result)
|
|
|
|
// No line should exceed the box width.
|
|
for _, line := range strings.Split(plain, "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
w := lipgloss.Width(line)
|
|
if w > boxWidth {
|
|
t.Errorf("pipeline line exceeds box width (%d > %d): %s", w, boxWidth, line)
|
|
}
|
|
}
|
|
|
|
// The full long branch should NOT appear.
|
|
if strings.Contains(plain, longBranch) {
|
|
t.Error("expected long branch name to be truncated to fit box content width")
|
|
}
|
|
}
|
|
|
|
func TestPipelineView_ShortErrorNotTruncated(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
run := testRun()
|
|
shortError := "exit code 1"
|
|
run.Steps[1].Status = types.StepStatusFailed
|
|
run.Steps[1].Error = &shortError
|
|
|
|
result := renderPipelineView(run, run.Steps, 80, 0, 40)
|
|
plain := stripANSI(result)
|
|
|
|
// Short error should appear in full.
|
|
if !strings.Contains(plain, shortError) {
|
|
t.Error("expected short error message to appear in full")
|
|
}
|
|
}
|
|
|
|
func TestRenderCIView_ShowsShortPRContextInPanel(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
run := testRunWithCI()
|
|
longURL := "https://github.com/some-very-long-organization-name/some-very-long-repository-name-that-goes-on-and-on/pull/12345"
|
|
run.PRURL = &longURL
|
|
run.Steps[5].Status = types.StepStatusRunning
|
|
|
|
result := stripANSI(renderCIView(run, run.Steps, "", nil, 80))
|
|
|
|
if !strings.Contains(result, "PR #12345") {
|
|
t.Fatalf("expected CI panel to show short PR context, got: %s", result)
|
|
}
|
|
if strings.Contains(result, longURL) {
|
|
t.Fatal("expected CI panel to avoid rendering the full PR URL")
|
|
}
|
|
}
|
|
|
|
func TestRenderCIView_LongLastEventTruncated(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
run := testRunWithCI()
|
|
run.Steps[5].Status = types.StepStatusRunning
|
|
longEvent := "monitoring CI for PR #42 with a very long description " + strings.Repeat("z", 200)
|
|
logs := []string{longEvent}
|
|
|
|
result := stripANSI(renderCIView(run, run.Steps, "", logs, 80))
|
|
|
|
// No line should exceed the box width (80).
|
|
for _, line := range strings.Split(result, "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
w := lipgloss.Width(line)
|
|
if w > 80 {
|
|
t.Errorf("CI LastEvent line exceeds box width (%d > 80): %s", w, line)
|
|
}
|
|
}
|
|
|
|
// The full long event text should NOT appear in the output.
|
|
if strings.Contains(result, strings.Repeat("z", 77)) {
|
|
t.Error("expected long LastEvent to be truncated to fit box content width")
|
|
}
|
|
}
|
|
|
|
func TestRenderCIView_ShowsPRContext(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
run := testRunWithCI()
|
|
shortURL := "https://github.com/user/repo/pull/99"
|
|
run.PRURL = &shortURL
|
|
run.Steps[5].Status = types.StepStatusRunning
|
|
|
|
result := stripANSI(renderCIView(run, run.Steps, "", nil, 60))
|
|
if !strings.Contains(result, "PR #99") {
|
|
t.Fatalf("expected CI panel to show PR context, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestModel_View_ErrorBox_LongMessageTruncated(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
|
|
m := Model{
|
|
run: testRun(),
|
|
steps: testRun().Steps,
|
|
width: 80,
|
|
err: fmt.Errorf("%s", strings.Repeat("x", 200)),
|
|
}
|
|
|
|
result := m.View()
|
|
lines := strings.Split(result, "\n")
|
|
|
|
// No line should exceed the box width of 80.
|
|
for _, line := range lines {
|
|
w := lipgloss.Width(line)
|
|
if w > 80 {
|
|
t.Errorf("error box line exceeds box width (%d > 80): %s", w, line)
|
|
}
|
|
}
|
|
|
|
// The full 200-char error text should NOT appear in the output.
|
|
if strings.Contains(result, strings.Repeat("x", 200)) {
|
|
t.Error("expected long error message to be truncated to fit inside box")
|
|
}
|
|
}
|
|
|
|
func TestModel_View_ErrorBox_ShortMessageNotTruncated(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
|
|
errText := "connection refused"
|
|
m := Model{
|
|
run: testRun(),
|
|
steps: testRun().Steps,
|
|
width: 80,
|
|
err: fmt.Errorf("%s", errText),
|
|
}
|
|
|
|
result := stripANSI(m.View())
|
|
|
|
// Short error message should appear in full.
|
|
if !strings.Contains(result, errText) {
|
|
t.Error("expected short error message to appear in full")
|
|
}
|
|
}
|
|
|
|
func TestModel_View_ErrorBox_MultiLineMessageTruncated(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
|
|
longLine := strings.Repeat("y", 200)
|
|
m := Model{
|
|
run: testRun(),
|
|
steps: testRun().Steps,
|
|
width: 80,
|
|
err: fmt.Errorf("line1\n%s", longLine),
|
|
}
|
|
|
|
result := m.View()
|
|
lines := strings.Split(result, "\n")
|
|
|
|
// No line should exceed the box width of 80.
|
|
for _, line := range lines {
|
|
w := lipgloss.Width(line)
|
|
if w > 80 {
|
|
t.Errorf("error box line exceeds box width (%d > 80): %s", w, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderPipelineView_RunStatusColoredBlue(t *testing.T) {
|
|
run := testRun()
|
|
run.Status = types.RunRunning
|
|
steps := []ipc.StepResultInfo{
|
|
{StepName: types.StepReview, Status: types.StepStatusRunning},
|
|
{StepName: types.StepTest, Status: types.StepStatusPending},
|
|
}
|
|
view := renderPipelineView(run, steps, 80, 0, 40)
|
|
|
|
// The run status should stand out as the primary signal in the header.
|
|
blueStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(ansiBlue))
|
|
blueRunning := blueStyle.Render("running")
|
|
if !strings.Contains(view, blueRunning) {
|
|
t.Errorf("expected run status 'running' to be styled blue, got:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestRenderPipelineView_RunStatusColoredGreen(t *testing.T) {
|
|
run := testRun()
|
|
run.Status = types.RunCompleted
|
|
steps := []ipc.StepResultInfo{
|
|
{StepName: types.StepReview, Status: types.StepStatusCompleted},
|
|
{StepName: types.StepTest, Status: types.StepStatusCompleted},
|
|
}
|
|
view := renderPipelineView(run, steps, 80, 0, 40)
|
|
|
|
greenStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(ansiGreen))
|
|
greenCompleted := greenStyle.Render("completed")
|
|
if !strings.Contains(view, greenCompleted) {
|
|
t.Errorf("expected run status 'completed' to be styled green, got:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestRenderPipelineView_BranchStyledDim(t *testing.T) {
|
|
run := testRun()
|
|
view := renderPipelineView(run, run.Steps, 80, 0, 40)
|
|
|
|
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(ansiBrightBlack))
|
|
dimBranch := dimStyle.Render(run.Branch)
|
|
if !strings.Contains(view, dimBranch) {
|
|
t.Errorf("expected branch name to be styled dim, got:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestRenderPipelineView_HidesStepProgress(t *testing.T) {
|
|
run := testRun()
|
|
run.Status = types.RunRunning
|
|
steps := []ipc.StepResultInfo{
|
|
{StepName: types.StepReview, Status: types.StepStatusCompleted},
|
|
{StepName: types.StepTest, Status: types.StepStatusRunning},
|
|
{StepName: types.StepLint, Status: types.StepStatusPending},
|
|
{StepName: types.StepPush, Status: types.StepStatusPending},
|
|
{StepName: types.StepPR, Status: types.StepStatusPending},
|
|
}
|
|
view := renderPipelineView(run, steps, 80, 0, 40)
|
|
plain := stripANSI(view)
|
|
|
|
if strings.Contains(plain, "2/5") {
|
|
t.Errorf("expected step progress count to be hidden from pipeline header, got:\n%s", plain)
|
|
}
|
|
}
|
|
|
|
func TestPipelineView_LongBranchKeepsStatusVisible(t *testing.T) {
|
|
lipgloss.SetColorProfile(termenv.Ascii)
|
|
longBranch := "feature/" + strings.Repeat("very-long-name-", 10)
|
|
run := testRun()
|
|
run.Branch = longBranch
|
|
|
|
result := stripANSI(renderPipelineView(run, run.Steps, 40, 0, 40))
|
|
if !strings.Contains(result, "running") {
|
|
t.Fatalf("expected running status to remain visible when branch is truncated, got:\n%s", result)
|
|
}
|
|
if strings.Contains(result, longBranch) {
|
|
t.Fatal("expected long branch name to be truncated before it reaches the status")
|
|
}
|
|
}
|
|
|
|
func TestRenderBoxWithFooter_FitsRequestedWidth(t *testing.T) {
|
|
plain := stripANSI(renderBoxWithFooter("Findings", "content", 40, "↓ 1 more below (j/k)"))
|
|
for _, line := range strings.Split(plain, "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if got := lipgloss.Width(line); got != 40 {
|
|
t.Fatalf("expected every box line to be width 40, got %d for %q", got, line)
|
|
}
|
|
}
|
|
}
|