vitrine

diagram_test.go

package main

import (
	"bytes"
	"strings"
	"sync"
	"testing"

	"github.com/yuin/goldmark/parser"
)

func TestDiagramDotProducesSVG(t *testing.T) {
	got := renderMD(t, "```dot\ndigraph { a -> b }\n```\n")
	if !strings.Contains(got, `<div class="diagram">`) {
		t.Errorf("missing diagram wrapper; got:\n%s", got)
	}
	if !strings.Contains(got, "<svg") {
		t.Errorf("expected inline <svg>; got:\n%s", got)
	}
	// The block must be consumed as a diagram, not left as a code block.
	if strings.Contains(got, "<pre><code") {
		t.Errorf("dot block leaked through as a code block; got:\n%s", got)
	}
}

func TestDiagramGraphvizAlias(t *testing.T) {
	// The graphviz tag is an alias for dot; both feed the same engine.
	got := renderMD(t, "```graphviz\ndigraph { a -> b }\n```\n")
	if !strings.Contains(got, `<div class="diagram">`) || !strings.Contains(got, "<svg") {
		t.Errorf("graphviz alias did not render a diagram; got:\n%s", got)
	}
}

func TestDiagramBrokenDotFallsBackVisibly(t *testing.T) {
	// A diagram that does not compile must not panic or 500; it echoes its
	// source in a failure-tagged box so the author still sees their input.
	got := renderMD(t, "```dot\ndigraph { a -> \n```\n")
	if strings.Contains(got, "<svg") {
		t.Errorf("broken dot unexpectedly produced an svg; got:\n%s", got)
	}
	if !strings.Contains(got, "diagram-error") {
		t.Errorf("broken dot lacked the failure marker; got:\n%s", got)
	}
	if !strings.Contains(got, "digraph { a -&gt;") {
		t.Errorf("broken dot did not echo its source; got:\n%s", got)
	}
}

func TestDiagramPlainCodeBlockUntouched(t *testing.T) {
	// A non-diagram language must render exactly as before: a plain code block.
	got := renderMD(t, "```go\nfunc main() {}\n```\n")
	if strings.Contains(got, "diagram") || strings.Contains(got, "<svg") {
		t.Errorf("a go block was treated as a diagram; got:\n%s", got)
	}
	if !strings.Contains(got, "<pre><code") {
		t.Errorf("a go block should render as a code block; got:\n%s", got)
	}
}

func TestDiagramLabelIsEscaped(t *testing.T) {
	// vitrine serves trees it may not have authored, so a hostile label must not
	// break out of the SVG into live HTML.  Graphviz escapes text content, so the
	// inline SVG carries no injected <script> and no premature </svg>.
	got := renderMD(t, "```dot\ndigraph { a [label=\"</text></svg><script>x</script>\"] }\n```\n")
	if strings.Contains(got, "<script>") {
		t.Errorf("hostile label injected a live script; got:\n%s", got)
	}
	if strings.Count(got, "</svg>") != 1 {
		t.Errorf("hostile label produced a stray </svg>; got:\n%s", got)
	}
}

func TestDiagramConcurrentRenders(t *testing.T) {
	// go-graphviz drives one process-global wasm module with no internal locking,
	// so renders must be serialized.  This exercises that: many diagrams rendered
	// at once must all succeed.  Without the serializing lock the shared module
	// corrupts and the process aborts with "concurrent map writes".
	const src = "```dot\ndigraph { a -> b -> c }\n```\n"
	var wg sync.WaitGroup
	fails := make(chan string, 16)
	for i := 0; i < 16; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			var buf bytes.Buffer
			ctx := parser.NewContext(parser.WithIDs(newSlugIDs()))
			if err := markdown.Convert([]byte(src), &buf, parser.WithContext(ctx)); err != nil {
				fails <- err.Error()
				return
			}
			if !strings.Contains(buf.String(), "<svg") {
				fails <- "no svg in output"
			}
		}()
	}
	wg.Wait()
	close(fails)
	for f := range fails {
		t.Errorf("concurrent render failed: %s", f)
	}
}

diagram_test.go · 3.4 KiB · 06f4ceb908