Files
Dion Kobi 097904f2b9 feat: implement pagination in Web UI (Fixes #86) (#325)
* feat: implement pagination in Web UI (Fixes #86)

- Added Count method to JobRepository and Offset to SelectParams
- Updated sqlite implementation to support offset and count calculations
- Added ListJobs function to Service, keeping All intact for API backward compatibility
- Updated getJobs to parse ?page query param and return JobRowsData
- Updated HTML templates to support HTMX outerHTML replacements with out-of-band updates

* fix: resolve pagination bugs and add tests for #86

* fix: remove HTMX load loop, stray files, add trigger regression test (#86)

- index.html: initial tbody uses hx-trigger='load' only (fires once)
- job_rows.html: returned tbody uses hx-trigger='every 10s' only (no load)
  This eliminates the outerHTML reload loop where the inserted element
  immediately fires 'load' again before the 10s timer.
- Remove pr_body.md and results.csv (local artifacts, not part of #86)
- Add TestGetJobsHTMXTriggerIsPollingOnly to assert the fix is permanent
- Service.All remains unchanged for existing callers

* fix: address pagination review feedback

Co-authored-by: Giorgos Komninos <admin@gkomninos.com>

---------

Co-authored-by: Giorgos Komninos <admin@gkomninos.com>
2026-09-13 11:27:34 +03:00

95 lines
2.2 KiB
Go

//nolint:testpackage // tests the private SQLite repository and schema directly
package sqlite
import (
"context"
"strings"
"testing"
"time"
"github.com/gosom/google-maps-scraper/web"
)
func newTestRepo(t *testing.T) *repo {
t.Helper()
db, err := initDatabase(":memory:")
if err != nil {
t.Fatalf("initDatabase: %v", err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("close database: %v", err)
}
})
return &repo{db: db}
}
func createTestJob(t *testing.T, repo *repo, id string, date time.Time) {
t.Helper()
job := web.Job{ID: id, Name: id, Date: date, Status: web.StatusPending}
if err := repo.Create(context.Background(), &job); err != nil {
t.Fatalf("Create(%q): %v", id, err)
}
}
func TestSelectSupportsOffsetWithoutLimit(t *testing.T) {
repo := newTestRepo(t)
createTestJob(t, repo, "a", time.Unix(1, 0))
createTestJob(t, repo, "b", time.Unix(2, 0))
createTestJob(t, repo, "c", time.Unix(3, 0))
jobs, err := repo.Select(context.Background(), web.SelectParams{Offset: 1})
if err != nil {
t.Fatalf("Select: %v", err)
}
if len(jobs) != 2 || jobs[0].ID != "b" || jobs[1].ID != "a" {
t.Fatalf("unexpected jobs: %+v", jobs)
}
}
func TestSelectOrdersSameSecondJobsByID(t *testing.T) {
repo := newTestRepo(t)
createdAt := time.Unix(1, 0)
createTestJob(t, repo, "b", createdAt)
createTestJob(t, repo, "c", createdAt)
createTestJob(t, repo, "a", createdAt)
jobs, err := repo.Select(context.Background(), web.SelectParams{})
if err != nil {
t.Fatalf("Select: %v", err)
}
want := []string{"c", "b", "a"}
for i := range want {
if jobs[i].ID != want[i] {
t.Fatalf("job %d: got %q, want %q", i, jobs[i].ID, want[i])
}
}
}
func TestSchemaIndexesJobPaginationOrder(t *testing.T) {
repo := newTestRepo(t)
var statement string
err := repo.db.QueryRowContext(
context.Background(),
`SELECT sql FROM sqlite_master WHERE type = 'index' AND name = 'idx_jobs_created_at_id'`,
).Scan(&statement)
if err != nil {
t.Fatalf("query pagination index: %v", err)
}
want := strings.Fields("CREATE INDEX idx_jobs_created_at_id ON jobs (created_at DESC, id DESC)")
if strings.Join(strings.Fields(statement), " ") != strings.Join(want, " ") {
t.Fatalf("unexpected index: %q", statement)
}
}