package web import ( "context" "net/http" "net/url" "regexp" "strings" "testing" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" "sourcecraft.dev/bigbes/sr-ht-dolt/core" ) // dummyView is a test-only View that fingerprints repos containing a table // named "issues". It renders through an EXISTING page template ("404.html") so // the test needs no newly embedded file. That template references only the // shared chrome (basePage), so it renders cleanly with the fixed view envelope // (which does not carry a .Tables field the way tree.html would demand). It // records whether Build ran. type dummyView struct { built bool } func (d *dummyView) Name() string { return "issues" } func (d *dummyView) Label() string { return "Issues" } func (d *dummyView) Template() string { return "404.html" } func (d *dummyView) Applies(tables []browse.TableInfo) bool { for _, t := range tables { if t.Name == "issues" { return true } } return false } func (d *dummyView) Build(_ context.Context, sess BrowseSession, _ *core.Repo, ref string, _ url.Values) (any, error) { d.built = true // Pull data through the BrowseSession surface only, mirroring how a real // view works, and return the tables so tree.html renders something. tables, err := sess.Tables(context.Background(), ref) if err != nil { return nil, err } return tables, nil } func issueTables() []browse.TableInfo { return []browse.TableInfo{ {Name: "issues", Columns: []browse.ColumnInfo{{Name: "id", PrimaryKey: true}}, RowCount: 2}, {Name: "labels", RowCount: 5}, } } func TestApplicableViewsFilters(t *testing.T) { v := &dummyView{} views := []View{v} got := applicableViews(views, issueTables()) if len(got) != 1 || got[0] != v { t.Fatalf("applicableViews should select the matching view; got %v", got) } // No "issues" table → no applicable views. none := applicableViews(views, []browse.TableInfo{{Name: "widgets"}}) if len(none) != 0 { t.Fatalf("applicableViews should filter out non-matching views; got %v", none) } } func TestApplicableViewsPreservesOrder(t *testing.T) { a := &dummyView{} // A second always-applies view to check registration order is preserved. b := alwaysView{} got := applicableViews([]View{b, a}, issueTables()) if len(got) != 2 || got[0] != View(b) || got[1] != View(a) { t.Fatalf("applicableViews should preserve order; got %v", got) } } // alwaysView is a trivial always-applicable view used to check ordering. type alwaysView struct{} func (alwaysView) Name() string { return "always" } func (alwaysView) Label() string { return "Always" } func (alwaysView) Template() string { return "404.html" } func (alwaysView) Applies([]browse.TableInfo) bool { return true } func (alwaysView) Build(context.Context, BrowseSession, *core.Repo, string, url.Values) (any, error) { return nil, nil } func TestHandleViewSelectedAndBuilt(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) h.browse.sess = &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}}, tables: issueTables(), } dv := &dummyView{} // Inject the view directly on the app, without polluting the global registry. setViews(t, h, dv) rec := h.do("GET", "/~alice/db/view/issues", nil, nil) // A 200 (rather than the 404 an unknown/non-applicable view returns) proves // the view was selected and its Template rendered; dv.built proves Build ran. if rec.Code != http.StatusOK { t.Fatalf("view page: got %d, want 200; body=%s", rec.Code, rec.Body.String()) } if !dv.built { t.Fatalf("view.Build was not called") } // The view's Label flows into the page via the envelope's basePage, // so its presence confirms this view (not a fallback) produced the response. if !strings.Contains(rec.Body.String(), "Issues") { t.Fatalf("view page missing view label in chrome; body=%s", rec.Body.String()) } } func TestHandleViewUnknownSlugIs404(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) h.browse.sess = &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}}, tables: issueTables(), } setViews(t, h, &dummyView{}) rec := h.do("GET", "/~alice/db/view/nope", nil, nil) if rec.Code != http.StatusNotFound { t.Fatalf("unknown view slug: got %d, want 404", rec.Code) } } func TestHandleViewNotApplicableIs404(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) // Tables lack "issues", so dummyView.Applies is false even though the slug matches. h.browse.sess = &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}}, tables: []browse.TableInfo{{Name: "widgets"}}, } setViews(t, h, &dummyView{}) rec := h.do("GET", "/~alice/db/view/issues", nil, nil) if rec.Code != http.StatusNotFound { t.Fatalf("non-applicable view: got %d, want 404", rec.Code) } } func TestOverviewShowsViewTabs(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) h.browse.sess = &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}}, tables: issueTables(), } setViews(t, h, &dummyView{}) rec := h.do("GET", "/~alice/db", nil, nil) if rec.Code != http.StatusOK { t.Fatalf("overview: got %d, want 200", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "/~alice/db/view/issues") { t.Fatalf("overview missing view tab link; body=%s", body) } if !strings.Contains(body, "nav-tabs") { t.Fatalf("overview missing tab bar; body=%s", body) } // The bar carries three tabs in order: Overview, Tables, then the view. iOverview := strings.Index(body, ">Overview<") iTables := strings.Index(body, ">Tables<") iView := strings.Index(body, "/~alice/db/view/issues") if iOverview < 0 || iTables < 0 { t.Fatalf("overview missing Overview/Tables tabs; body=%s", body) } if !(iOverview < iTables && iTables < iView) { t.Fatalf("tab order should be Overview < Tables < view; got %d, %d, %d", iOverview, iTables, iView) } // The active tab on the overview is Overview, not Tables. if !regexp.MustCompile(`nav-link active"[^>]*>Overview<`).MatchString(body) { t.Fatalf("overview tab should be active on the overview page; body=%s", body) } } // setViews overrides the app's view snapshot without touching the global // registry, so view routing/overview tabs can be exercised with a controlled // set. The harness retains the *app the router is mounted on. func setViews(t *testing.T, h *harness, vs ...View) { t.Helper() h.app.views = vs }