mirror of
https://github.com/yarlson/lnk.git
synced 2025-09-01 18:02:34 +02:00
feat(output): implement configurable color and emoji output
Add new output formatting system with flags for color and emoji control: - Introduce OutputConfig and Writer structs for flexible output handling - Add --colors and --emoji/--no-emoji global flags - Refactor commands to use new Writer for consistent formatting - Separate error content from presentation for better flexibility
This commit is contained in:
271
cmd/output_test.go
Normal file
271
cmd/output_test.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
func TestOutputConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
colors string
|
||||
emoji bool
|
||||
expectError bool
|
||||
expectedColors bool
|
||||
expectedEmoji bool
|
||||
}{
|
||||
{
|
||||
name: "auto mode",
|
||||
colors: "auto",
|
||||
emoji: true,
|
||||
expectError: false,
|
||||
expectedColors: false, // TTY detection will return false in tests
|
||||
expectedEmoji: true,
|
||||
},
|
||||
{
|
||||
name: "always mode",
|
||||
colors: "always",
|
||||
emoji: false,
|
||||
expectError: false,
|
||||
expectedColors: true,
|
||||
expectedEmoji: false,
|
||||
},
|
||||
{
|
||||
name: "never mode",
|
||||
colors: "never",
|
||||
emoji: true,
|
||||
expectError: false,
|
||||
expectedColors: false,
|
||||
expectedEmoji: true,
|
||||
},
|
||||
{
|
||||
name: "invalid mode",
|
||||
colors: "invalid",
|
||||
emoji: true,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Clear NO_COLOR for consistent testing
|
||||
_ = os.Unsetenv("NO_COLOR")
|
||||
|
||||
err := SetGlobalConfig(tt.colors, tt.emoji)
|
||||
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !tt.expectError {
|
||||
if globalConfig.Colors != tt.expectedColors {
|
||||
t.Errorf("expected colors %v, got %v", tt.expectedColors, globalConfig.Colors)
|
||||
}
|
||||
if globalConfig.Emoji != tt.expectedEmoji {
|
||||
t.Errorf("expected emoji %v, got %v", tt.expectedEmoji, globalConfig.Emoji)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNOCOLOREnvironmentVariable(t *testing.T) {
|
||||
// Test NO_COLOR environment variable with auto mode
|
||||
_ = os.Setenv("NO_COLOR", "1")
|
||||
defer func() { _ = os.Unsetenv("NO_COLOR") }()
|
||||
|
||||
err := SetGlobalConfig("auto", true)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if globalConfig.Colors != false {
|
||||
t.Errorf("expected colors disabled when NO_COLOR is set, got %v", globalConfig.Colors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config OutputConfig
|
||||
message Message
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "full formatting",
|
||||
config: OutputConfig{Colors: true, Emoji: true},
|
||||
message: Message{
|
||||
Text: "test message",
|
||||
Color: ColorRed,
|
||||
Emoji: "✅",
|
||||
Bold: true,
|
||||
},
|
||||
expectedOutput: "✅ \033[1m\033[31mtest message\033[0m",
|
||||
},
|
||||
{
|
||||
name: "colors only",
|
||||
config: OutputConfig{Colors: true, Emoji: false},
|
||||
message: Message{
|
||||
Text: "test message",
|
||||
Color: ColorRed,
|
||||
Emoji: "✅",
|
||||
Bold: true,
|
||||
},
|
||||
expectedOutput: "\033[1m\033[31mtest message\033[0m",
|
||||
},
|
||||
{
|
||||
name: "emoji only",
|
||||
config: OutputConfig{Colors: false, Emoji: true},
|
||||
message: Message{
|
||||
Text: "test message",
|
||||
Color: ColorRed,
|
||||
Emoji: "✅",
|
||||
Bold: true,
|
||||
},
|
||||
expectedOutput: "✅ test message",
|
||||
},
|
||||
{
|
||||
name: "no formatting",
|
||||
config: OutputConfig{Colors: false, Emoji: false},
|
||||
message: Message{
|
||||
Text: "test message",
|
||||
Color: ColorRed,
|
||||
Emoji: "✅",
|
||||
Bold: true,
|
||||
},
|
||||
expectedOutput: "test message",
|
||||
},
|
||||
{
|
||||
name: "plain message",
|
||||
config: OutputConfig{Colors: true, Emoji: true},
|
||||
message: Plain("plain text"),
|
||||
expectedOutput: "plain text",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
writer := NewWriter(&buf, tt.config)
|
||||
|
||||
writer.Write(tt.message)
|
||||
if err := writer.Err(); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if buf.String() != tt.expectedOutput {
|
||||
t.Errorf("expected %q, got %q", tt.expectedOutput, buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPredefinedMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
creator func(string) Message
|
||||
text string
|
||||
}{
|
||||
{"Success", Success, "operation succeeded"},
|
||||
{"Error", Error, "something failed"},
|
||||
{"Warning", Warning, "be careful"},
|
||||
{"Info", Info, "useful information"},
|
||||
{"Target", Target, "target reached"},
|
||||
{"Rocket", Rocket, "launching"},
|
||||
{"Sparkles", Sparkles, "amazing"},
|
||||
{"Link", Link, "connected"},
|
||||
{"Plain", Plain, "no formatting"},
|
||||
{"Bold", Bold, "emphasis"},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer := NewWriter(&buf, OutputConfig{Colors: true, Emoji: true})
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf.Reset()
|
||||
msg := tt.creator(tt.text)
|
||||
|
||||
writer.Write(msg)
|
||||
if err := writer.Err(); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, tt.text) {
|
||||
t.Errorf("output should contain text %q, got %q", tt.text, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructuredErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *core.LnkError
|
||||
config OutputConfig
|
||||
contains []string
|
||||
notContains []string
|
||||
}{
|
||||
{
|
||||
name: "structured error with full formatting",
|
||||
err: &core.LnkError{
|
||||
Message: "Something went wrong",
|
||||
Suggestion: "Try this instead",
|
||||
Path: "/some/path",
|
||||
ErrorType: "test_error",
|
||||
},
|
||||
config: OutputConfig{Colors: true, Emoji: true},
|
||||
contains: []string{"❌", "Something went wrong", "/some/path", "💡", "Try this instead"},
|
||||
},
|
||||
{
|
||||
name: "structured error without emojis",
|
||||
err: &core.LnkError{
|
||||
Message: "Something went wrong",
|
||||
Suggestion: "Try this instead",
|
||||
Path: "/some/path",
|
||||
ErrorType: "test_error",
|
||||
},
|
||||
config: OutputConfig{Colors: true, Emoji: false},
|
||||
contains: []string{"Something went wrong", "/some/path", "Try this instead"},
|
||||
notContains: []string{"❌", "💡"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
w := NewWriter(&buf, tt.config)
|
||||
|
||||
// Test the component messages directly
|
||||
_ = w.Write(Error(tt.err.Message))
|
||||
if tt.err.Path != "" {
|
||||
_ = w.WriteString("\n ")
|
||||
_ = w.Write(Colored(tt.err.Path, ColorRed))
|
||||
}
|
||||
if tt.err.Suggestion != "" {
|
||||
_ = w.WriteString("\n ")
|
||||
_ = w.Write(Info(tt.err.Suggestion))
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
for _, expected := range tt.contains {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("output should contain %q, got %q", expected, output)
|
||||
}
|
||||
}
|
||||
for _, notExpected := range tt.notContains {
|
||||
if strings.Contains(output, notExpected) {
|
||||
t.Errorf("output should not contain %q, got %q", notExpected, output)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user