package render import ( "bytes" "encoding/json" "errors" "os" "path/filepath" "regexp" "strings" "testing" "github.com/kotoyuuko/zenmux-usage-cli/internal/api" ) var ansiRE = regexp.MustCompile(`\x1b\[[0-9;]*m`) func stripANSI(s string) string { return ansiRE.ReplaceAllString(s, "") } func loadSample(t *testing.T) *api.Response { t.Helper() data, err := os.ReadFile(filepath.Join("..", "api", "testdata", "sample.json")) if err != nil { t.Fatalf("read sample: %v", err) } var r api.Response if err := json.Unmarshal(data, &r); err != nil { t.Fatalf("parse sample: %v", err) } return &r } func TestProgressBar(t *testing.T) { cases := []struct { pct float64 want string }{ {0, strings.Repeat(barEmpty, barWidth)}, {1, strings.Repeat(barFilled, barWidth)}, {0.5, strings.Repeat(barFilled, 15) + strings.Repeat(barEmpty, 15)}, {-0.1, strings.Repeat(barEmpty, barWidth)}, {1.5, strings.Repeat(barFilled, barWidth)}, } for _, tc := range cases { got := ProgressBar(tc.pct, barWidth) if got != tc.want { t.Errorf("ProgressBar(%v) got %q want %q", tc.pct, got, tc.want) } } } func TestBandColor(t *testing.T) { cases := []struct { pct float64 want string }{ {0.10, "green"}, {0.59, "green"}, {0.60, "yellow"}, {0.85, "yellow"}, {0.86, "red"}, {1.00, "red"}, } for _, tc := range cases { attr := BandColor(tc.pct) var got string switch attr { case 32: // FgGreen got = "green" case 33: // FgYellow got = "yellow" case 31: // FgRed got = "red" } if got != tc.want { t.Errorf("BandColor(%v) got %s want %s", tc.pct, got, tc.want) } } } func TestRenderAccount_Snapshot(t *testing.T) { resp := loadSample(t) var buf bytes.Buffer setGlobalColor(false) RenderAccount(&buf, "personal", resp, false) got := stripANSI(buf.String()) wants := []string{ "━━━ personal ━━━", "Ultra plan ($200/mo)", "healthy", "$0.03283/flow", "5 hour", "7 day", "month", "7.15%", "6.73%", " n/a", "57.2 / 800 flows", "416.11 / 6182 flows", "— / 34560 flows", "$1.88 / $26.26", "— / $1134.33", "Tokens consumed (estimated USD value): $13.66", } for _, w := range wants { if !strings.Contains(got, w) { t.Errorf("output missing %q\n---\n%s\n---", w, got) } } } func TestRenderAll_MultiAccountWithError(t *testing.T) { resp := loadSample(t) results := []AccountResult{ {Name: "personal", Response: resp}, {Name: "work", Err: errors.New("authentication rejected: HTTP 401")}, } var buf bytes.Buffer RenderAll(&buf, results, false) got := stripANSI(buf.String()) if !strings.Contains(got, "━━━ personal ━━━") { t.Error("missing personal header") } if !strings.Contains(got, "━━━ work ━━━") { t.Error("missing work header") } if !strings.Contains(got, "error: authentication rejected") { t.Error("missing error line for work account") } // Personal block comes before work block. pIdx := strings.Index(got, "personal") wIdx := strings.Index(got, "work") if pIdx < 0 || wIdx < 0 || pIdx >= wIdx { t.Errorf("account ordering wrong: personal@%d work@%d", pIdx, wIdx) } } func TestRenderAccount_UsesColorWhenEnabled(t *testing.T) { resp := loadSample(t) var buf bytes.Buffer setGlobalColor(true) defer setGlobalColor(false) RenderAccount(&buf, "personal", resp, true) if !ansiRE.MatchString(buf.String()) { t.Error("expected ANSI escape sequences when useColor=true") } } func TestRenderAccount_NoColorWhenDisabled(t *testing.T) { resp := loadSample(t) var buf bytes.Buffer setGlobalColor(false) RenderAccount(&buf, "personal", resp, false) if ansiRE.MatchString(buf.String()) { t.Errorf("expected no ANSI when useColor=false, got %q", buf.String()) } } func TestBandColor_HighUsageRed(t *testing.T) { // Sanity: a 90% usage window should render with a red bar/percent. resp := &api.Response{ Success: true, Data: api.Data{ Plan: api.Plan{Tier: "pro", AmountUSD: 50}, AccountStatus: "healthy", EffectiveUSDPerFlow: 0.01, Quota5Hour: api.QuotaWindow{ UsagePercentage: 0.92, MaxFlows: 100, UsedFlows: 92, UsedValueUSD: 0.92, MaxValueUSD: 1.00, }, Quota7Day: api.QuotaWindow{ UsagePercentage: 0.70, MaxFlows: 1000, UsedFlows: 700, UsedValueUSD: 7.00, MaxValueUSD: 10.00, }, QuotaMonthly: api.QuotaMonthly{MaxFlows: 4000, MaxValueUSD: 40}, }, } var buf bytes.Buffer setGlobalColor(true) defer setGlobalColor(false) RenderAccount(&buf, "n", resp, true) out := buf.String() // 31 = FgRed, 33 = FgYellow. Both should appear. if !strings.Contains(out, "\x1b[31m") { t.Error("expected red ANSI somewhere in output for 92% bar") } if !strings.Contains(out, "\x1b[33m") { t.Error("expected yellow ANSI somewhere in output for 70% bar") } }