feat: Phase 2 UX Polish — theme system, config profiles, error banner, tests
- Add Theme struct with 3 predefined themes (dark/light/druntime) - Add Profile struct for named configuration profiles - Add ErrorBanner with severity levels and auto-dismiss - Add unit tests for theme, error banner, and models - Update CHANGELOG.md and PROJECT_STATE.md
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
|
||||
)
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
config := models.DefaultConfig()
|
||||
|
||||
if config.Version != "1.0.0" {
|
||||
t.Errorf("Version = %q, want %q", config.Version, "1.0.0")
|
||||
}
|
||||
if config.DefaultPort != 22 {
|
||||
t.Errorf("DefaultPort = %d, want 22", config.DefaultPort)
|
||||
}
|
||||
if config.ConnectionTimeout != 30 {
|
||||
t.Errorf("ConnectionTimeout = %d, want 30", config.ConnectionTimeout)
|
||||
}
|
||||
if config.Theme != "dark" {
|
||||
t.Errorf("Theme = %q, want %q", config.Theme, "dark")
|
||||
}
|
||||
if len(config.Profiles) != 1 {
|
||||
t.Errorf("Profiles has %d items, want 1", len(config.Profiles))
|
||||
}
|
||||
if config.ActiveProfile != "default" {
|
||||
t.Errorf("ActiveProfile = %q, want %q", config.ActiveProfile, "default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppConfigProfiles(t *testing.T) {
|
||||
config := models.DefaultConfig()
|
||||
|
||||
// Test GetProfile
|
||||
profile := config.GetProfile("default")
|
||||
if profile == nil {
|
||||
t.Fatal("GetProfile(default) returned nil")
|
||||
}
|
||||
if profile.Name != "default" {
|
||||
t.Errorf("Profile.Name = %q, want %q", profile.Name, "default")
|
||||
}
|
||||
|
||||
// Test GetProfile for non-existent profile
|
||||
profile = config.GetProfile("nonexistent")
|
||||
if profile != nil {
|
||||
t.Error("GetProfile(nonexistent) should return nil")
|
||||
}
|
||||
|
||||
// Test GetActiveProfile
|
||||
profile = config.GetActiveProfile()
|
||||
if profile == nil {
|
||||
t.Fatal("GetActiveProfile() returned nil")
|
||||
}
|
||||
if profile.Name != "default" {
|
||||
t.Errorf("Active profile Name = %q, want %q", profile.Name, "default")
|
||||
}
|
||||
|
||||
// Test AddProfile
|
||||
newProfile := models.Profile{
|
||||
Name: "work",
|
||||
Theme: "light",
|
||||
}
|
||||
config.AddProfile(newProfile)
|
||||
if len(config.Profiles) != 2 {
|
||||
t.Errorf("After AddProfile, Profiles has %d items, want 2", len(config.Profiles))
|
||||
}
|
||||
|
||||
// Test RemoveProfile
|
||||
config.RemoveProfile("work")
|
||||
if len(config.Profiles) != 1 {
|
||||
t.Errorf("After RemoveProfile, Profiles has %d items, want 1", len(config.Profiles))
|
||||
}
|
||||
|
||||
// Test RemoveProfile for non-existent profile
|
||||
config.RemoveProfile("nonexistent")
|
||||
if len(config.Profiles) != 1 {
|
||||
t.Errorf("After RemoveProfile(nonexistent), Profiles has %d items, want 1", len(config.Profiles))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package tui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/tui"
|
||||
)
|
||||
|
||||
func TestErrorBannerShowHide(t *testing.T) {
|
||||
banner := tui.NewErrorBanner(tui.SevError)
|
||||
|
||||
// Initially not visible
|
||||
if banner.IsVisible() {
|
||||
t.Error("Banner should not be visible initially")
|
||||
}
|
||||
|
||||
// Show the banner
|
||||
banner.Show("Test Error", "Something went wrong", "Check logs", "Restart app")
|
||||
if !banner.IsVisible() {
|
||||
t.Error("Banner should be visible after Show()")
|
||||
}
|
||||
|
||||
// Verify content
|
||||
if banner.Title != "Test Error" {
|
||||
t.Errorf("Title = %q, want %q", banner.Title, "Test Error")
|
||||
}
|
||||
if banner.Detail != "Something went wrong" {
|
||||
t.Errorf("Detail = %q, want %q", banner.Detail, "Something went wrong")
|
||||
}
|
||||
if len(banner.Hints) != 2 {
|
||||
t.Errorf("Hints has %d items, want 2", len(banner.Hints))
|
||||
}
|
||||
|
||||
// Hide the banner
|
||||
banner.Hide()
|
||||
if banner.IsVisible() {
|
||||
t.Error("Banner should not be visible after Hide()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorBannerAutoDismiss(t *testing.T) {
|
||||
banner := tui.NewErrorBanner(tui.SevWarning)
|
||||
banner.AutoDismiss = true
|
||||
banner.DismissAfter = 100 * time.Millisecond
|
||||
|
||||
banner.Show("Test Warning", "Something")
|
||||
if !banner.IsVisible() {
|
||||
t.Error("Banner should be visible after Show()")
|
||||
}
|
||||
|
||||
// Wait for auto-dismiss
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
banner.Update()
|
||||
|
||||
if banner.IsVisible() {
|
||||
t.Error("Banner should be auto-dismissed after delay")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorBannerSeverity(t *testing.T) {
|
||||
tests := []struct {
|
||||
severity tui.ErrorSeverity
|
||||
name string
|
||||
}{
|
||||
{tui.SevError, "error"},
|
||||
{tui.SevWarning, "warning"},
|
||||
{tui.SevInfo, "info"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
banner := tui.NewErrorBanner(tt.severity)
|
||||
if banner.Severity != tt.severity {
|
||||
t.Errorf("NewErrorBanner(%v).Severity = %v, want %v", tt.name, banner.Severity, tt.severity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorBannerView(t *testing.T) {
|
||||
banner := tui.NewErrorBanner(tui.SevError)
|
||||
banner.Show("Error Title", "Error detail", "Hint 1", "Hint 2")
|
||||
|
||||
// Test that View returns non-empty string
|
||||
output := banner.View(80)
|
||||
if output == "" {
|
||||
t.Error("View() returned empty string")
|
||||
}
|
||||
|
||||
// Test that View returns empty string when not visible
|
||||
banner.Hide()
|
||||
output = banner.View(80)
|
||||
if output != "" {
|
||||
t.Error("View() should return empty string when not visible")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package tui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/tui"
|
||||
)
|
||||
|
||||
func TestGetTheme(t *testing.T) {
|
||||
// Test getting existing themes
|
||||
tests := []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"dark", "dark"},
|
||||
{"light", "light"},
|
||||
{"dracula", "dracula"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
theme := tui.GetTheme(tt.name)
|
||||
if theme.Name != tt.expected {
|
||||
t.Errorf("GetTheme(%q) = %q, want %q", tt.name, theme.Name, tt.expected)
|
||||
}
|
||||
}
|
||||
|
||||
// Test getting non-existing theme defaults to dark
|
||||
theme := tui.GetTheme("nonexistent")
|
||||
if theme.Name != "dark" {
|
||||
t.Errorf("GetTheme(nonexistent) = %q, want %q", theme.Name, "dark")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTheme(t *testing.T) {
|
||||
// Set theme to light
|
||||
tui.SetTheme("light")
|
||||
active := tui.GetActiveTheme()
|
||||
if active.Name != "light" {
|
||||
t.Errorf("After SetTheme(light), GetActiveTheme() = %q, want %q", active.Name, "light")
|
||||
}
|
||||
|
||||
// Set theme back to dark
|
||||
tui.SetTheme("dark")
|
||||
active = tui.GetActiveTheme()
|
||||
if active.Name != "dark" {
|
||||
t.Errorf("After SetTheme(dark), GetActiveTheme() = %q, want %q", active.Name, "dark")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetActiveTheme(t *testing.T) {
|
||||
// Default should be dark
|
||||
active := tui.GetActiveTheme()
|
||||
if active.Name != "dark" {
|
||||
t.Errorf("GetActiveTheme() = %q, want %q", active.Name, "dark")
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemeRegistry(t *testing.T) {
|
||||
// Test that all themes are registered
|
||||
expectedThemes := []string{"dark", "light", "dracula"}
|
||||
for _, name := range expectedThemes {
|
||||
if _, ok := tui.Themes[name]; !ok {
|
||||
t.Errorf("Theme %q not found in Themes map", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Test theme count
|
||||
if len(tui.Themes) != 3 {
|
||||
t.Errorf("Themes map has %d entries, want 3", len(tui.Themes))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user