package tui import ( "testing" "git.ryuvia.com/niklas/terdut-tui/internal/api" "git.ryuvia.com/niklas/terdut-tui/internal/theme" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/lipgloss" ) // Assertions here read the colours off the styles rather than off rendered // output: under `go test` stdout is not a TTY, so lipgloss strips every escape // sequence and rendered strings would all compare equal. func wantFg(t *testing.T, s lipgloss.Style, want lipgloss.Color, what string) { t.Helper() if got := s.GetForeground(); got != want { t.Errorf("%s foreground = %v, want %v", what, got, want) } } func TestStyles_TokensReachTheStyles(t *testing.T) { th := theme.GruvboxDark s := newStyles(th) wantFg(t, s.Header, th.Primary, "header") wantFg(t, s.Firing, th.Firing, "firing") wantFg(t, s.Resolved, th.Resolved, "resolved") wantFg(t, s.Muted, th.Muted, "muted") wantFg(t, s.Status, th.Accent, "status") wantFg(t, s.Error, th.Error, "error") wantFg(t, s.SevWarning, th.SevWarning, "sev warning") // The two inverted spots need the pair, not just a foreground. wantFg(t, s.TabActive, th.OnPrimary, "active tab") if got := s.TabActive.GetBackground(); got != th.Primary { t.Errorf("active tab background = %v, want %v", got, th.Primary) } // Attributes the old package-level styles carried must survive. if !s.Firing.GetBold() { t.Error("firing lost its bold") } if !s.Snoozed.GetItalic() { t.Error("snoozed lost its italic") } if top, right, bottom, left := s.TabActive.GetPadding(); top != 0 || right != 2 || bottom != 0 || left != 2 { t.Errorf("active tab padding = %d %d %d %d, want 0 2 0 2", top, right, bottom, left) } } func TestStyles_EachBuiltinIsFullyPopulated(t *testing.T) { for _, th := range []theme.Theme{theme.GruvboxDark, theme.GruvboxLight} { s := newStyles(th) for what, style := range map[string]lipgloss.Style{ "header": s.Header, "tab inactive": s.TabInactive, "footer": s.Footer, "status": s.Status, "error": s.Error, "firing": s.Firing, "resolved": s.Resolved, "muted": s.Muted, "accent": s.Accent, "alert name": s.AlertName, "bold": s.Bold, "selected": s.Selected, "triggered": s.Triggered, "acknowledged": s.Acknowledged, "snoozed": s.Snoozed, "sev critical": s.SevCritical, "sev error": s.SevError, "sev warning": s.SevWarning, "sev info": s.SevInfo, } { if style.GetForeground() == (lipgloss.NoColor{}) { t.Errorf("%s: %s has no foreground", th.Name, what) } } for i := range th.Identity { if s.TeamColor(int64(i)).GetForeground() == (lipgloss.NoColor{}) { t.Errorf("%s: identity %d has no foreground", th.Name, i) } } } } // TestStyles_TeamColorIsStableAndDistinct checks the property the header badge // and the team picker both rely on: the same id always reads the same colour, // and ids that differ (up to the size of the palette) read as different // colours rather than all collapsing to one. func TestStyles_TeamColorIsStableAndDistinct(t *testing.T) { s := newStyles(theme.GruvboxDark) seen := make(map[lipgloss.Color]bool) for id := int64(0); id < 6; id++ { seen[s.TeamColor(id).GetForeground().(lipgloss.Color)] = true } if len(seen) != 6 { t.Errorf("expected 6 distinct colours across 6 ids, got %d", len(seen)) } // ids repeat past the palette size, and a negative id (never sent by the // server, but cheap to guard) must not panic. if s.TeamColor(6).GetForeground() != s.TeamColor(0).GetForeground() { t.Error("the palette should wrap rather than index out of range") } if got := s.TeamColor(-1).GetForeground(); got == (lipgloss.NoColor{}) { t.Error("a negative id should still resolve to a colour, not panic or fall back to none") } } func TestStyles_SeverityFallsBackToMuted(t *testing.T) { s := newStyles(theme.GruvboxDark) for _, sev := range []string{"critical", "CRITICAL", "error", "warning", "info"} { if s.Severity(sev).GetForeground() == s.Muted.GetForeground() { t.Errorf("severity %q should have its own colour", sev) } } for _, sev := range []string{"", "page", "unknown"} { if s.Severity(sev).GetForeground() != s.Muted.GetForeground() { t.Errorf("severity %q should fall back to muted", sev) } } } func TestStyles_IncidentStatusFallsBackToMuted(t *testing.T) { s := newStyles(theme.GruvboxDark) for _, status := range []string{api.StatusTriggered, api.StatusAcknowledged, api.StatusResolved} { if s.IncidentStatus(status).GetForeground() == s.Muted.GetForeground() { t.Errorf("status %q should have its own colour", status) } } if s.IncidentStatus("invented-later").GetForeground() != s.Muted.GetForeground() { t.Error("an unknown status should fall back to muted") } } // The bubbles components ship their own palettes — a pink selected row, grey // help text, a 240 placeholder. These check we replaced them. func TestStyles_BubblesComponentsFollowTheTheme(t *testing.T) { th := theme.GruvboxDark s := newStyles(th) ts := s.Table() wantFg(t, ts.Selected, th.OnPrimary, "table selection") if got := ts.Selected.GetBackground(); got != th.Primary { t.Errorf("table selection background = %v, want %v", got, th.Primary) } wantFg(t, ts.Header, th.Muted, "table header") if _, right, _, left := ts.Cell.GetPadding(); right != 1 || left != 1 { t.Error("table cell padding was lost") } // A foreground on Cell would emit a reset mid-row and truncate the // selection highlight, so it must stay unset. if ts.Cell.GetForeground() != (lipgloss.NoColor{}) { t.Error("table cells must not carry a foreground") } h := s.Help() wantFg(t, h.ShortKey, th.Text, "help key") wantFg(t, h.ShortDesc, th.Muted, "help description") wantFg(t, h.FullSeparator, th.Muted, "help separator") in := s.Input(textinput.New()) wantFg(t, in.PlaceholderStyle, th.Muted, "input placeholder") wantFg(t, in.TextStyle, th.Text, "input text") wantFg(t, in.PromptStyle, th.Primary, "input prompt") wantFg(t, in.Cursor.Style, th.Primary, "input cursor") }