- 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
147 lines
4.3 KiB
Go
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))
|
|
}
|
|
})
|
|
}
|