024dc095a5
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.
162 lines
5.9 KiB
Go
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")
|
|
}
|