jamtrack/internal/query/ranking_test.go
Frédéric G. MARAND 6f3321371d bug(ux): fix various front end bugs.
- Fixed song add form (3 HTMX bugs: reset on typeahead, wrong query param, artist-only find-or-create)
- Added Played checkbox to add form; inline edit (✎/Save/Cancel) for setlist rows
- View fixes: date format, location name instead of ID in jam list, notes on location detail, song count column, location name links to detail
- Title typeahead + cross-filtering (artist↔title filter each other)
- Fixed datalist accumulation bug: HTMX 2.0 inherits hx-swap from parent; added explicit hx-swap="innerHTML" on both inputs
- Setlist sort by id (ULID = insertion order); created rejected by PocketBase
- Location delete guard: OnRecordDelete hook blocks if jams exist; FK enforcement is app-level not SQLite-level

Loose ends to pick up:
- Focus-after-add not user-confirmed yet (setTimeout approach)
- jams.participants and setlist.position still in DB schema but unused in UI (optional migration to clean up)
- Logout flow and dashboard ranking not browser-tested
2026-06-15 14:12:57 +02:00

147 lines
4.3 KiB
Go

package query_test
import (
"testing"
"github.com/pocketbase/pocketbase/core"
"code.osinet.fr/fgm/jamtrack/internal/query"
_ "code.osinet.fr/fgm/jamtrack/migrations"
_ "github.com/pocketbase/pocketbase/migrations"
)
// newTestApp creates a bootstrapped PocketBase app in a temp directory with
// all jamtrack migrations applied. The caller is responsible for calling
// app.ResetBootstrapState() via t.Cleanup.
func newTestApp(t *testing.T) core.App {
t.Helper()
app := core.NewBaseApp(core.BaseAppConfig{DataDir: t.TempDir()})
if err := app.Bootstrap(); err != nil {
t.Fatalf("Bootstrap: %v", err)
}
if err := app.RunAppMigrations(); err != nil {
t.Fatalf("RunAppMigrations: %v", err)
}
t.Cleanup(func() { app.ResetBootstrapState() })
return app
}
func mustSave(t *testing.T, app core.App, record *core.Record) {
t.Helper()
if err := app.Save(record); err != nil {
t.Fatalf("Save %s: %v", record.Collection().Name, err)
}
}
func newRecord(t *testing.T, app core.App, collection string) *core.Record {
t.Helper()
col, err := app.FindCollectionByNameOrId(collection)
if err != nil {
t.Fatalf("FindCollection %q: %v", collection, err)
}
return core.NewRecord(col)
}
func TestFetchRanking(t *testing.T) {
app := newTestApp(t)
// Seed: one location
loc := newRecord(t, app, "locations")
loc.Set("name", "The Rusty String")
mustSave(t, app, loc)
// Seed: three songs
songs := []struct{ artist, title string }{
{"Tom Petty", "Learning to Fly"},
{"Pink Floyd", "Learning to Fly"},
{"Eagles", "Hotel California"},
}
songRecords := make([]*core.Record, len(songs))
for i, s := range songs {
r := newRecord(t, app, "songs")
r.Set("artist", s.artist)
r.Set("title", s.title)
mustSave(t, app, r)
songRecords[i] = r
}
// Seed: two jams
newJam := func(date string) *core.Record {
j := newRecord(t, app, "jams")
j.Set("date", date)
j.Set("location", loc.Id)
mustSave(t, app, j)
return j
}
jam1 := newJam("2024-01-10")
jam2 := newJam("2024-02-14")
// Seed setlist entries.
// jam1: Tom Petty played, Pink Floyd proposed, Eagles played
// jam2: Tom Petty played, Eagles proposed
entries := []struct {
jam *core.Record
song *core.Record
played bool
}{
{jam1, songRecords[0], true}, // Tom Petty — played
{jam1, songRecords[1], false}, // Pink Floyd — proposed only
{jam1, songRecords[2], true}, // Eagles — played
{jam2, songRecords[0], true}, // Tom Petty — played again
{jam2, songRecords[2], false}, // Eagles — proposed only
}
for _, en := range entries {
sl := newRecord(t, app, "setlist")
sl.Set("jam", en.jam.Id)
sl.Set("song", en.song.Id)
sl.Set("played", en.played)
mustSave(t, app, sl)
}
t.Run("global ranking", func(t *testing.T) {
ranks, err := query.FetchRanking(app, "")
if err != nil {
t.Fatalf("FetchRanking: %v", err)
}
if len(ranks) != 3 {
t.Fatalf("want 3 rows, got %d", len(ranks))
}
// Tom Petty: played=2, proposed=2 → first
if ranks[0].Artist != "Tom Petty" || ranks[0].PlayedCount != 2 || ranks[0].ProposedCount != 2 {
t.Errorf("rank[0] = %+v, want Tom Petty played=2 proposed=2", ranks[0])
}
// Eagles: played=1, proposed=2 → second
if ranks[1].Artist != "Eagles" || ranks[1].PlayedCount != 1 || ranks[1].ProposedCount != 2 {
t.Errorf("rank[1] = %+v, want Eagles played=1 proposed=2", ranks[1])
}
// Pink Floyd: played=0, proposed=1 → last
if ranks[2].Artist != "Pink Floyd" || ranks[2].PlayedCount != 0 || ranks[2].ProposedCount != 1 {
t.Errorf("rank[2] = %+v, want Pink Floyd played=0 proposed=1", ranks[2])
}
})
t.Run("location filter", func(t *testing.T) {
// With location filter the results should be the same (all entries
// are at the same location), so just check the count and top entry.
ranks, err := query.FetchRanking(app, loc.Id)
if err != nil {
t.Fatalf("FetchRanking with location: %v", err)
}
if len(ranks) != 3 {
t.Fatalf("want 3 rows, got %d", len(ranks))
}
if ranks[0].Artist != "Tom Petty" {
t.Errorf("rank[0].Artist = %q, want Tom Petty", ranks[0].Artist)
}
})
t.Run("unknown location returns empty", func(t *testing.T) {
ranks, err := query.FetchRanking(app, "nonexistentid000")
if err != nil {
t.Fatalf("FetchRanking: %v", err)
}
if len(ranks) != 0 {
t.Errorf("want 0 rows for unknown location, got %d", len(ranks))
}
})
}