Files
terdut-tui/internal/tui/styles_test.go
T
Niklas Ye 024dc095a5
CI / test (push) Successful in 15s
Release / test (push) Successful in 4s
Release / binaries (push) Successful in 28s
Show a colour-coded team picker instead of cycling with T
T used to silently cycle Model.activeTeamID through the caller's teams with
no visible list of choices. It now opens a full picker (modelled on the
existing user picker) listing every team plus "All teams", each with a
stable identity colour from a new six-colour theme palette. The same colour
now also shows as a bullet next to "team: <name>" in the header, so the
active team stays visible without opening the picker.

Adds an Identity palette to the theme package (six hues, skipping the ones
that already mean firing/critical), Styles.TeamColor to pick one by team id,
and identity_1..6 as theme-file tokens alongside the existing twelve so a
fully custom theme can still set every token.
2026-09-27 18:14:45 +02:00

162 lines
5.9 KiB
Go

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")
}