The bars were wrapped in <div class="chart">, and .chart was already the fixed-height box the time-series charts are drawn in: 120px, with a border and a background. A chart that grows a row at a time does not fit in a cage, so the SVG overflowed it and landed on the table underneath. Renamed to .ranked, which is a wrapper rather than the mark itself, and capped at the design width so a wide window scales the box instead of inflating the 12px labels. The test reads the class back out of the markup and checks the stylesheet does not cap it, because the collision was invisible in both files on their own — each rule was reasonable, and only the shared name was wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
5.3 KiB
Go
149 lines
5.3 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRankedBarsLabelEveryRowDirectly(t *testing.T) {
|
|
// The label is the point of this chart: the question is which person or
|
|
// which model, not what shape the distribution is. Rotated or truncated
|
|
// axis labels would say the chart matters more than reading it.
|
|
out := string(HBarChart([]Row{
|
|
{Label: "ilker@example.com", Value: 1200},
|
|
{Label: "deniz@example.com", Value: 300},
|
|
}, "--viz-1", "tokens"))
|
|
|
|
for _, want := range []string{"ilker@example.com", "deniz@example.com", "1.2k", "300"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("the chart does not show %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestASmallValueStillDrawsAMark(t *testing.T) {
|
|
// A bar that rounds to zero width reads as no data rather than as a small
|
|
// number, which is a different and wrong claim.
|
|
out := string(HBarChart([]Row{
|
|
{Label: "huge", Value: 1_000_000},
|
|
{Label: "tiny", Value: 1},
|
|
}, "--viz-1", ""))
|
|
// Two paths, one per row: the tiny one is still drawn.
|
|
if n := strings.Count(out, "<path"); n != 2 {
|
|
t.Errorf("drew %d marks for two rows", n)
|
|
}
|
|
}
|
|
|
|
func TestAnEmptyChartSaysSoRatherThanRenderingNothing(t *testing.T) {
|
|
// A missing chart reads as a broken page.
|
|
out := string(HBarChart(nil, "--viz-1", ""))
|
|
if !strings.Contains(out, "Nothing recorded") {
|
|
t.Errorf("an empty series rendered as %q", out)
|
|
}
|
|
}
|
|
|
|
func TestChartTextUsesTextColoursNotTheSeriesColour(t *testing.T) {
|
|
// Values and labels wear text tokens; the mark beside them carries the
|
|
// identity. Colouring the text too makes the palette do two jobs and reads
|
|
// badly for anyone the hue separation was chosen to protect.
|
|
out := string(HBarChart([]Row{{Label: "a", Value: 5}}, "--viz-2", "req"))
|
|
for _, frag := range []string{`fill="var(--fg)"`, `fill="var(--muted)"`} {
|
|
if !strings.Contains(out, frag) {
|
|
t.Errorf("expected text in a text colour, got:\n%s", out)
|
|
}
|
|
}
|
|
if strings.Contains(out, `<text`) && strings.Contains(out, `fill="var(--viz-2)"><text`) {
|
|
t.Error("text was painted with the series colour")
|
|
}
|
|
}
|
|
|
|
func TestEveryBarCarriesItsOwnHoverTitle(t *testing.T) {
|
|
// The chart is in a browser, so it should answer a pointer. A <title> does
|
|
// that with no JavaScript, which is the right trade for a page this project
|
|
// serves from a single binary.
|
|
out := string(HBarChart([]Row{
|
|
{Label: "a", Value: 1}, {Label: "b", Value: 2},
|
|
}, "--viz-1", "tokens"))
|
|
if n := strings.Count(out, "<title>"); n != 2 {
|
|
t.Errorf("%d hover titles for two bars", n)
|
|
}
|
|
}
|
|
|
|
func TestALongLabelIsTrimmedRatherThanOverflowing(t *testing.T) {
|
|
long := strings.Repeat("a", 80) + "@example.com"
|
|
out := string(HBarChart([]Row{{Label: long, Value: 1}}, "--viz-1", ""))
|
|
// Trimmed where it is drawn, in full where it is hovered: the plot keeps
|
|
// its geometry and the reader can still find out whose row it is.
|
|
drawn := out[strings.Index(out, "<text"):strings.Index(out, "</text>")]
|
|
if strings.Contains(drawn, long) {
|
|
t.Error("a very long label was drawn in full and would overrun the plot")
|
|
}
|
|
if !strings.Contains(out, long) {
|
|
t.Error("the full label is not available anywhere, not even on hover")
|
|
}
|
|
if !strings.Contains(out, "…") {
|
|
t.Error("the label was cut without saying it had been")
|
|
}
|
|
}
|
|
|
|
func TestASparklineOfNothingIsNotDrawn(t *testing.T) {
|
|
// One point is not a trend, and a single dot invites reading one.
|
|
if got := Sparkline([]float64{5}, ""); got != "" {
|
|
t.Errorf("a one-point sparkline rendered: %q", got)
|
|
}
|
|
if got := Sparkline(nil, ""); got != "" {
|
|
t.Errorf("an empty sparkline rendered: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAFlatSparklineIsDrawnAlongTheBottom(t *testing.T) {
|
|
// No traffic is a real answer. An empty box would read as a broken chart.
|
|
out := string(Sparkline([]float64{0, 0, 0}, ""))
|
|
if !strings.Contains(out, "<polyline") {
|
|
t.Errorf("a flat series did not draw: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestTheChartScalesToItsContainer(t *testing.T) {
|
|
// The dashboard is read on laptops and phones. A fixed pixel width is how a
|
|
// table ends up scrolling sideways.
|
|
out := string(HBarChart([]Row{{Label: "a", Value: 1}}, "", ""))
|
|
if !strings.Contains(out, "viewBox") {
|
|
t.Error("no viewBox, so the chart cannot scale")
|
|
}
|
|
if strings.Contains(out, `width="720"`) {
|
|
t.Error("a fixed pixel width was set on the chart")
|
|
}
|
|
}
|
|
|
|
func TestTheRankedWrapperIsNotGivenAFixedHeight(t *testing.T) {
|
|
// This chart grows a row at a time, so whatever class it wears must not be
|
|
// one the stylesheet caps. It wore "chart", which is the fixed-height box
|
|
// the time-series charts live in, and the bars spilled over the table
|
|
// underneath them.
|
|
out := string(HBarChart([]Row{{Label: "a", Value: 1}}, "", ""))
|
|
class := "" // the wrapper's class, read back out of the markup
|
|
if i := strings.Index(out, `<div class="`); i >= 0 {
|
|
rest := out[i+len(`<div class="`):]
|
|
class = rest[:strings.Index(rest, `"`)]
|
|
}
|
|
if class == "" {
|
|
t.Fatalf("no wrapper to check:\n%s", out)
|
|
}
|
|
|
|
css, err := staticFiles.ReadFile("static/base.css")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, line := range strings.Split(string(css), "\n") {
|
|
decl, ok := strings.CutPrefix(strings.TrimSpace(line), "."+class+" ")
|
|
if !ok || !strings.HasPrefix(decl, "{") {
|
|
continue
|
|
}
|
|
if strings.Contains(decl, "height:") && !strings.Contains(decl, "height: auto") {
|
|
t.Errorf("%q is capped by the stylesheet, so taller charts overflow it: %s",
|
|
class, strings.TrimSpace(line))
|
|
}
|
|
}
|
|
}
|