package preview import ( "reflect" "regexp" "strings" "testing" "github.com/charmbracelet/glamour/styles" ) func TestLegibleTextContrasts(t *testing.T) { // Dark/mid accents → light text; very light bg → dark text. if got := legibleText("#4385BE"); got != "#FFFCF0" { t.Errorf("legibleText(blue) = %q, want light", got) } if got := legibleText("#FF5FAF"); got != "#FFFCF0" { t.Errorf("legibleText(pink) = %q, want light", got) } if got := legibleText("#F0E6BE"); got != "#100F0F" { t.Errorf("legibleText(pale yellow) = %q, want dark", got) } } func TestApplyThemeClearsBackgroundsAndH1(t *testing.T) { cfg := styles.DarkStyleConfig applyTheme(&cfg, Colors{Background: "#100F0F", Text: "#CECDC3", Heading: "#4385BE", Code: "#879A39", Link: "#3AA99F"}) // No element keeps glamour's dark code/table panel: every BackgroundColor is the bg. var bad []string var walk func(v reflect.Value, path string) walk = func(v reflect.Value, path string) { switch v.Kind() { case reflect.Pointer: if !v.IsNil() { walk(v.Elem(), path) } case reflect.Struct: tp := v.Type() for i := 0; i < v.NumField(); i++ { f := v.Field(i) name := tp.Field(i).Name if name == "BackgroundColor" && f.Type() == reflect.TypeOf((*string)(nil)) && !f.IsNil() { if *f.Interface().(*string) != "#100F0F" && !strings.HasPrefix(path, ".H1") { bad = append(bad, path) } continue } walk(f, path+"."+name) } } } walk(reflect.ValueOf(cfg), "") if len(bad) > 0 { t.Errorf("backgrounds not cleared to theme bg at: %v", bad) } // H1 is the heading-color bar with legible text. if cfg.H1.BackgroundColor == nil || *cfg.H1.BackgroundColor != "#4385BE" { t.Errorf("H1 background not the heading color") } if cfg.H1.Color == nil || *cfg.H1.Color != "#FFFCF0" { t.Errorf("H1 text not legible color, got %v", cfg.H1.Color) } } // Headings below H1 should sit directly above their body — no blank line. func TestRenderNoBlankLineAfterSubheadings(t *testing.T) { m := New("") m.SetColors(Colors{Background: "#100F0F", Text: "#CECDC3", Heading: "#4385BE", Code: "#879A39", Link: "#3AA99F"}) m.SetSize(80, 40) if err := m.Render("# Title\n\nintro\n\n## Section\n\nbody\n\n### Sub\n\nmore\n"); err != nil { t.Fatal(err) } lines := stripANSI(m.vp.View()) idx := func(want string) int { for i, ln := range lines { if strings.TrimSpace(ln) == want { return i } } t.Fatalf("line %q not found in:\n%s", want, strings.Join(lines, "\n")) return -1 } // H2/H3 are immediately followed by their body — no intervening blank line. if got := strings.TrimSpace(lines[idx("## Section")+1]); got != "body" { t.Errorf("expected body directly after H2, got %q", got) } if got := strings.TrimSpace(lines[idx("### Sub")+1]); got != "more" { t.Errorf("expected body directly after H3, got %q", got) } // H1 keeps its trailing blank line. if got := strings.TrimSpace(lines[idx("Title")+1]); got != "" { t.Errorf("expected blank line after H1, got %q", got) } } var ansiRE = regexp.MustCompile("\x1b\\[[0-9;]*m") func stripANSI(s string) []string { return strings.Split(ansiRE.ReplaceAllString(s, ""), "\n") }