package color import ( "strings" "testing" ) func TestWrapANSIDisabled(t *testing.T) { s := "this is a very long line that would normally be wrapped if enabled" if got := WrapANSI(s, 0); got != s { t.Errorf("WrapANSI(_, 0) = %q, want unchanged", got) } } func TestWrapANSIShortLineUntouched(t *testing.T) { s := " indented menu item" if got := WrapANSI(s, 80); got != s { t.Errorf("short line was modified: got %q, want %q", got, s) } } func TestWrapANSIWrapsLongLine(t *testing.T) { s := "alpha bravo charlie delta echo foxtrot golf hotel india juliet" got := WrapANSI(s, 20) lines := strings.Split(got, "\r\n") if len(lines) < 2 { t.Fatalf("expected multiple lines, got %d: %q", len(lines), got) } for _, l := range lines { if VisibleLen(l) > 20 { t.Errorf("line exceeds width: %q (%d)", l, VisibleLen(l)) } } if strings.Join(strings.Fields(got), " ") != s { t.Errorf("words not preserved: %q", got) } } func TestWrapANSIIgnoresColorCodes(t *testing.T) { colored := FgCode("xterm256", 182) + "hello world" + Reset if VisibleLen(colored) > 80 { t.Fatalf("test setup wrong, visible len = %d", VisibleLen(colored)) } if got := WrapANSI(colored, 80); got != colored { t.Errorf("colored line within width was wrapped: %q", got) } } func TestWrapANSISplitsOnNewlines(t *testing.T) { got := WrapANSI("first line\nsecond line", 80) want := "first line\r\nsecond line" if got != want { t.Errorf("WrapANSI newline handling = %q, want %q", got, want) } } func TestParseHexIndex(t *testing.T) { cases := []struct { in string want int }{ {"00", 0}, {"0F", 15}, {"0f", 15}, {"D0", 208}, {"FF", 255}, {"F", 15}, } for _, c := range cases { if got := Parse(c.in).Fg; got != c.want { t.Errorf("Parse(%q).Fg = %d, want %d", c.in, got, c.want) } } } func TestParseHexRejectsInvalid(t *testing.T) { for _, in := range []string{"ZZ", "100", "G1"} { if got := Parse(in).Fg; got != -1 { t.Errorf("Parse(%q).Fg = %d, want -1 (rejected)", in, got) } } } func TestParseHexBgAndGradient(t *testing.T) { s := Parse("D0 bg:00 g:C4,52 bold") if s.Fg != 208 { t.Errorf("Fg = %d, want 208", s.Fg) } if s.Bg != 0 { t.Errorf("Bg = %d, want 0", s.Bg) } if !s.Bold { t.Error("Bold not set") } if len(s.Gradient) != 2 || s.Gradient[0] != 196 || s.Gradient[1] != 82 { t.Errorf("Gradient = %v, want [196 82]", s.Gradient) } } func TestAverage(t *testing.T) { // black (0) and white (15) average to a mid grey. mid := Average(Parse("00"), Parse("0F")) if mid.Fg < 0 { t.Fatalf("expected a foreground, got %d", mid.Fg) } r, g, b := Xterm256ToRGB(mid.Fg) if r > 200 || r < 55 || g > 200 || g < 55 || b > 200 || b < 55 { t.Errorf("midpoint of black/white not grey: rgb=%d,%d,%d (idx %d)", r, g, b, mid.Fg) } // one side colorless -> use the other. if got := Average(NoColor(), Parse("D0")); got.Fg != 208 { t.Errorf("Average(none, D0).Fg = %d, want 208", got.Fg) } // both colorless -> NoColor. if got := Average(NoColor(), NoColor()); got.Fg != -1 { t.Errorf("Average(none, none).Fg = %d, want -1", got.Fg) } } func TestExpandDialogTags_NoQuotes(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("xterm256", dialogSpec, "The Netrunner looks up.") if strings.Contains(got, "\033") { t.Errorf("text without quotes should not contain ANSI codes, got %q", got) } } func TestExpandDialogTags_AllQuoted(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("xterm256", dialogSpec, `"Hello there."`) expectFg := FgCode("xterm256", 208) if !strings.Contains(got, expectFg+`"Hello there."`+Reset) && !strings.Contains(got, `"`+expectFg+`Hello there."`+Reset) { t.Errorf("fully quoted text should be dialog-colored, got %q", got) } if !strings.Contains(got, `"`) { t.Error("quote characters should be present") } } func TestExpandDialogTags_Mixed(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("xterm256", dialogSpec, `He says, "Hello, world!"`) expectFg := FgCode("xterm256", 208) if !strings.Contains(got, `He says, `) { t.Errorf("narrative text missing, got %q", got) } if !strings.Contains(got, expectFg+`"Hello, world!"`+Reset) { t.Errorf("quoted text should be dialog-colored, got %q", got) } } func TestExpandDialogTags_InlineTagsInQuotes(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("xterm256", dialogSpec, `"Please {0B bold}look{/} carefully."`) inlineFg := FgCode("xterm256", 11) if !strings.Contains(got, inlineFg+`look`+Reset) { t.Errorf("inline tag inside quotes should override dialog color, got %q", got) } } func TestExpandDialogTags_InlineTagsOutsideQuotes(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("xterm256", dialogSpec, `{0B bold}Notice:{/} "Something moved."`) inlineFg := FgCode("xterm256", 11) if !strings.Contains(got, inlineFg+`Notice:`+Reset) { t.Errorf("inline tag outside quotes should work, got %q", got) } } func TestExpandDialogTags_MultipleQuotes(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("xterm256", dialogSpec, `"Hello," she said. "How are you?"`) count := strings.Count(got, FgCode("xterm256", 208)) if count < 2 { t.Errorf("expected at least 2 dialog-colored segments, got %d in %q", count, got) } } func TestExpandDialogTags_UnmatchedQuote(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("xterm256", dialogSpec, `"Something started but never finished`) expectFg := FgCode("xterm256", 208) if !strings.Contains(got, expectFg) { t.Errorf("unmatched quote should still be dialog-colored, got %q", got) } if !strings.Contains(got, `"Something started but never finished`) { t.Errorf("unmatched quote text should be present, got %q", got) } } func TestExpandDialogTags_Empty(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("xterm256", dialogSpec, "") if got != "" { t.Errorf("empty input should yield empty output, got %q", got) } } func TestExpandDialogTags_NoColorMode(t *testing.T) { dialogSpec := Parse("D0") got := ExpandDialogTags("none", dialogSpec, `"Hello."`) if strings.Contains(got, "\033") { t.Errorf("no-color mode should suppress ANSI codes, got %q", got) } if got != `"Hello."` { t.Errorf("no-color mode should preserve text, got %q", got) } } func TestExpandTagsNamed_ResolvesNamedColor(t *testing.T) { named := map[string]ColorSpec{"command": Parse("0A bold")} got := ExpandTagsNamed("xterm256", named, "Type {command}look sign{/} now") want := StyleCode("bold") + FgCode("xterm256", 10) if !strings.Contains(got, want+"look sign"+Reset) { t.Errorf("named tag did not resolve: %q", got) } if strings.Contains(got, "{command}") { t.Errorf("named tag leaked literally: %q", got) } } func TestExpandTagsNamed_UnknownNameFallsBackToParse(t *testing.T) { // "0B bold" is not a named key, so it must keep parsing as a spec. named := map[string]ColorSpec{"command": Parse("0A bold")} got := ExpandTagsNamed("xterm256", named, "x{0B bold}y{/}z") if !strings.Contains(got, StyleCode("bold")+FgCode("xterm256", 11)+"y"+Reset) { t.Errorf("unknown named tag should fall back to spec parsing, got %q", got) } // A string that is neither named nor a valid spec stays literal. got = ExpandTagsNamed("xterm256", named, "{not_a_color}hi{/}") if !strings.Contains(got, "{not_a_color}hi") { t.Errorf("unknown tag should remain literal, got %q", got) } } func TestExpandTagsNamed_OffResolvesPlain(t *testing.T) { named := map[string]ColorSpec{"command": NoColor()} got := ExpandTagsNamed("xterm256", named, "A {command}bet{/}.") if strings.Contains(got, "\033") { t.Errorf("off category should render plain, got %q", got) } if !strings.Contains(got, "A bet.") { t.Errorf("off category should keep the inner text, got %q", got) } } func TestExpandTagsNamed_NoColorMode(t *testing.T) { named := map[string]ColorSpec{"command": Parse("0A bold")} got := ExpandTagsNamed("none", named, "Type {command}look sign{/} now") if strings.Contains(got, "\033") { t.Errorf("no-color mode should strip ANSI, got %q", got) } if !strings.Contains(got, "Type look sign now") { t.Errorf("no-color mode should keep text, got %q", got) } } func TestExpandDialogTagsNamed_ResolvesInsideQuotes(t *testing.T) { named := map[string]ColorSpec{"command": Parse("0A bold")} dialogSpec := Parse("D0") got := ExpandDialogTagsNamed("xterm256", dialogSpec, named, `"Please be sure {command}look{/} at the sign."`) want := StyleCode("bold") + FgCode("xterm256", 10) if !strings.Contains(got, want+"look"+Reset) { t.Errorf("named tag inside quoted dialog did not resolve: %q", got) } }