mirror of
https://github.com/tiennm99/ghglance.git
synced 2026-10-11 03:13:20 +00:00
The Marketplace rejects ghstats as an action name, so the repo, Go module, binary, and action share the name ghglance instead. The demo workflow keeps reading the existing GHSTATS_TOKEN repo secret.
178 lines
6.0 KiB
Go
178 lines
6.0 KiB
Go
package card
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tiennm99/ghglance/internal/github"
|
|
"github.com/tiennm99/ghglance/internal/theme"
|
|
)
|
|
|
|
// TestPadToWeekGridRotatesByWeekStart checks that the leading blank pad
|
|
// matches the configured start day — with weekStart=Monday, a Thursday-first
|
|
// series needs 3 leading zero-date slots so row 0 stays Monday.
|
|
func TestPadToWeekGridRotatesByWeekStart(t *testing.T) {
|
|
// Thursday 2025-01-02 — int(Weekday)=4.
|
|
thu := time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)
|
|
days := []github.DailyContribution{{Date: thu, Count: 1}}
|
|
|
|
cases := []struct {
|
|
name string
|
|
weekStart time.Weekday
|
|
wantOffset int
|
|
}{
|
|
{"Sunday start", time.Sunday, 4}, // Thu is row 4 of Sun..Sat
|
|
{"Monday start", time.Monday, 3}, // Thu is row 3 of Mon..Sun
|
|
{"Thursday start", time.Thursday, 0},
|
|
{"Friday start", time.Friday, 6},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
grid := padToWeekGrid(days, c.weekStart)
|
|
// The first real day should land at wantOffset; slots before it
|
|
// carry a zero Date.
|
|
if grid[c.wantOffset].Date != thu {
|
|
t.Fatalf("weekStart=%v: expected Thu at offset %d, got %v", c.weekStart, c.wantOffset, grid[c.wantOffset].Date)
|
|
}
|
|
for i := 0; i < c.wantOffset; i++ {
|
|
if !grid[i].Date.IsZero() {
|
|
t.Errorf("weekStart=%v: slot %d should be blank, got %v", c.weekStart, i, grid[i].Date)
|
|
}
|
|
}
|
|
if len(grid)%7 != 0 {
|
|
t.Errorf("weekStart=%v: grid len %d not a multiple of 7", c.weekStart, len(grid))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRenderWeekdayRespectsWeekStart asserts bar order follows weekStart.
|
|
// Saturday (6) has the peak — with Monday start, Saturday lands at position 5,
|
|
// which should carry the accent fill.
|
|
func TestRenderWeekdayRespectsWeekStart(t *testing.T) {
|
|
th, _ := theme.Lookup("dracula")
|
|
var data [7]int
|
|
data[time.Saturday] = 99 // peak
|
|
|
|
sunSVG := string(renderWeekday("t", data, time.Sunday, th))
|
|
monSVG := string(renderWeekday("t", data, time.Monday, th))
|
|
|
|
// Label positions: with Sunday start, "Sun" is the first label; with
|
|
// Monday start, "Mon" is the first label. Check the order of labels
|
|
// by scanning the <text …>LABEL</text> occurrences in the SVG.
|
|
sunOrder := extractWeekdayLabels(sunSVG)
|
|
wantSun := []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
|
|
if !equalStrings(sunOrder, wantSun) {
|
|
t.Errorf("Sunday-start label order = %v, want %v", sunOrder, wantSun)
|
|
}
|
|
monOrder := extractWeekdayLabels(monSVG)
|
|
wantMon := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
|
|
if !equalStrings(monOrder, wantMon) {
|
|
t.Errorf("Monday-start label order = %v, want %v", monOrder, wantMon)
|
|
}
|
|
}
|
|
|
|
// TestRenderHeatmapLabelsRespectWeekStart asserts the left-gutter weekday
|
|
// labels rotate with weekStart (rows 1,3,5 get labels).
|
|
func TestRenderHeatmapLabelsRespectWeekStart(t *testing.T) {
|
|
th, _ := theme.Lookup("dracula")
|
|
base := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
var days []github.DailyContribution
|
|
for i := 0; i < 30; i++ {
|
|
days = append(days, github.DailyContribution{Date: base.AddDate(0, 0, i), Count: i % 3})
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
weekStart time.Weekday
|
|
want []string // labels expected at rows 1, 3, 5
|
|
}{
|
|
{"Sunday start", time.Sunday, []string{"Mon", "Wed", "Fri"}},
|
|
{"Monday start", time.Monday, []string{"Tue", "Thu", "Sat"}},
|
|
{"Saturday start", time.Saturday, []string{"Sun", "Tue", "Thu"}},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
svg := string(renderHeatmap("t", days, c.weekStart, th))
|
|
for _, label := range c.want {
|
|
// Row labels use text-anchor="end"; pick that form so we
|
|
// don't collide with month labels along the top.
|
|
needle := `text-anchor="end">` + label
|
|
if !strings.Contains(svg, needle) {
|
|
t.Errorf("weekStart=%v: missing row label %q", c.weekStart, label)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRenderHeatmapMonthLabelsSkipPaddedCells guards against the leading
|
|
// zero-pad cell at week 0 leaking a bogus "Jan" label (zero time = 0001-01-01,
|
|
// Day=1 ≤ 7, Month=Jan). When weekStart=Monday and data starts mid-week, the
|
|
// pad cell would otherwise render an out-of-range month above the first column.
|
|
func TestRenderHeatmapMonthLabelsSkipPaddedCells(t *testing.T) {
|
|
th, _ := theme.Lookup("dracula")
|
|
// Sunday 2025-04-27 — with weekStart=Monday this needs 6 leading zero pads,
|
|
// putting the bogus "Jan" label at week 0 in the buggy version.
|
|
start := time.Date(2025, 4, 27, 0, 0, 0, 0, time.UTC)
|
|
var days []github.DailyContribution
|
|
for i := 0; i < 200; i++ {
|
|
days = append(days, github.DailyContribution{Date: start.AddDate(0, 0, i), Count: i % 5})
|
|
}
|
|
|
|
svg := string(renderHeatmap("t", days, time.Monday, th))
|
|
|
|
// The data range is 2025-04-27 .. 2025-11-12 — none of which is January.
|
|
// Any "Jan" label is the leaked zero-time month from the leading pad.
|
|
if strings.Contains(svg, ">Jan</text>") {
|
|
t.Error("month-label loop leaked a Jan label from the leading zero-pad cell")
|
|
}
|
|
// Sanity: months actually in range should still appear.
|
|
for _, m := range []string{"May", "Jun", "Jul"} {
|
|
if !strings.Contains(svg, ">"+m+"</text>") {
|
|
t.Errorf("expected month label %q in heatmap, missing", m)
|
|
}
|
|
}
|
|
}
|
|
|
|
// extractWeekdayLabels pulls weekday 3-letter names out of <text …>…</text>
|
|
// blocks in the order they appear. Good enough for bar-chart cards that emit
|
|
// exactly 7 weekday labels and no colliding strings.
|
|
func extractWeekdayLabels(svg string) []string {
|
|
names := []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
|
|
var out []string
|
|
rest := svg
|
|
for {
|
|
idx := -1
|
|
var pick string
|
|
for _, n := range names {
|
|
// Match the `>Label</text>` trailer so we catch rendered labels
|
|
// and not the `<title>` tooltip text (which uses "—").
|
|
needle := ">" + n + "</text>"
|
|
i := strings.Index(rest, needle)
|
|
if i >= 0 && (idx == -1 || i < idx) {
|
|
idx = i
|
|
pick = n
|
|
}
|
|
}
|
|
if idx == -1 {
|
|
return out
|
|
}
|
|
out = append(out, pick)
|
|
rest = rest[idx+len(pick)+len("</text>")+1:]
|
|
}
|
|
}
|
|
|
|
func equalStrings(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|