| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- 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",
- "7.15%",
- "6.73%",
- "57.2 / 800 flows",
- "416.11 / 6182 flows",
- "$1.88 / $26.26",
- "Tokens consumed (estimated USD value): $13.66",
- "Monthly cap: 34560 flows · $1134.33",
- }
- for _, w := range wants {
- if !strings.Contains(got, w) {
- t.Errorf("output missing %q\n---\n%s\n---", w, got)
- }
- }
- // The old monthly row must be gone from the bar region.
- barRegion := got
- if idx := strings.Index(got, "Tokens consumed"); idx >= 0 {
- barRegion = got[:idx]
- }
- for _, forbidden := range []string{"month ", " n/a", "— / ", "— /"} {
- if strings.Contains(barRegion, forbidden) {
- t.Errorf("bar region still contains %q\n---\n%s\n---", forbidden, barRegion)
- }
- }
- }
- 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")
- }
- }
|