ajhahn.de
← eeco
Go 130 lines
package guide

import (
	"flag"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
)

var updateGolden = flag.Bool("update", false, "rewrite the rendered golden file")

var reANSI = regexp.MustCompile("\x1b\\[[0-9;]*m")

func stripANSI(s string) string { return reANSI.ReplaceAllString(s, "") }

// TestRender_Golden locks the full rendered manual (no colour) so any
// drift in the renderer is visible in review. Re-run with -update to
// refresh after an intentional change.
func TestRender_Golden(t *testing.T) {
	got := Render(false)
	golden := filepath.Join("testdata", "usage.rendered.golden")
	if *updateGolden {
		if err := os.WriteFile(golden, []byte(got), 0o644); err != nil {
			t.Fatalf("write golden: %v", err)
		}
	}
	want, err := os.ReadFile(golden)
	if err != nil {
		t.Fatalf("read golden: %v (run with -update to create it)", err)
	}
	if got != string(want) {
		t.Errorf("Render(false) drifted from testdata/usage.rendered.golden — re-run with -update if intended")
	}
}

// TestRender_ColourStripsToPlain is the colour invariant: Render only
// adds ANSI escapes on top of the plain layout, so stripping them from
// Render(true) must yield Render(false).
func TestRender_ColourStripsToPlain(t *testing.T) {
	coloured := Render(true)
	if !strings.Contains(coloured, "\x1b[") {
		t.Fatal("Render(true) emitted no ANSI escapes")
	}
	if stripANSI(coloured) != Render(false) {
		t.Error("stripANSI(Render(true)) != Render(false) — colour changed the layout")
	}
}

func TestRender_Heading(t *testing.T) {
	got := renderManual("## Builtin workflows", false)
	lines := strings.Split(got, "\n")
	if lines[0] != "Builtin workflows" {
		t.Errorf("heading text = %q, want stripped of ##", lines[0])
	}
	if lines[1] != strings.Repeat("─", len("Builtin workflows")) {
		t.Errorf("heading rule = %q, want ─ sized to the title", lines[1])
	}
}

func TestRender_TableBox(t *testing.T) {
	src := "| A | Bee |\n| - | --- |\n| 1 | two |"
	got := renderManual(src, false)
	want := strings.Join([]string{
		"┌───┬─────┐",
		"│ A │ Bee │",
		"├───┼─────┤",
		"│ 1 │ two │",
		"└───┴─────┘",
	}, "\n")
	if got != want {
		t.Errorf("table render mismatch:\n got:\n%s\nwant:\n%s", got, want)
	}
}

func TestRender_TableEscapedPipe(t *testing.T) {
	src := "| Key | Note |\n| --- | --- |\n| `a\\|b` | one cell |"
	got := renderManual(src, false)
	if !strings.Contains(got, "a|b") {
		t.Errorf("escaped pipe not preserved as a literal cell value:\n%s", got)
	}
	// Three rows of three columns would mean the escape leaked a split;
	// the body row must stay two columns (one ┼ junction in the rule).
	for line := range strings.SplitSeq(got, "\n") {
		if strings.HasPrefix(line, "├") && strings.Count(line, "┼") != 1 {
			t.Errorf("expected a 2-column body, got rule %q", line)
		}
	}
}

func TestRender_InlineCodeAndBold(t *testing.T) {
	plain := renderManual("run `eeco go` and read the **brief**.", false)
	if plain != "run eeco go and read the brief." {
		t.Errorf("plain inline = %q", plain)
	}
	coloured := renderManual("run `eeco go` and read the **brief**.", true)
	if !strings.Contains(coloured, ansiFaint+"eeco go"+ansiReset) {
		t.Errorf("code span not faint: %q", coloured)
	}
	if !strings.Contains(coloured, ansiBold+"brief"+ansiReset) {
		t.Errorf("bold span not bold: %q", coloured)
	}
}

func TestRender_FenceVerbatim(t *testing.T) {
	src := "```\n**not bold** `not code`\n```"
	got := renderManual(src, false)
	if !strings.Contains(got, "**not bold** `not code`") {
		t.Errorf("fenced body should stay literal, got %q", got)
	}
	if !strings.HasPrefix(got, "    ") {
		t.Errorf("fenced body should be indented, got %q", got)
	}
}

func TestRender_Bullet(t *testing.T) {
	got := renderManual("- a point", false)
	if got != "• a point" {
		t.Errorf("bullet render = %q, want • glyph", got)
	}
}

func TestRender_UnknownPassThrough(t *testing.T) {
	got := renderManual("just some prose with no markup", false)
	if got != "just some prose with no markup" {
		t.Errorf("plain prose changed: %q", got)
	}
}