package web_test import ( "context" "strings" "testing" "time" "github.com/m/projax/web" ) // TestSystemViewLookup verifies the code-resident lookup returns the // expected slugs in display order, and that LookupSystemView round-trips // each entry. func TestSystemViewLookup(t *testing.T) { all := web.AllSystemViews() wantSlugs := []string{"tree", "dashboard", "calendar", "timeline", "graph"} if len(all) != len(wantSlugs) { t.Fatalf("AllSystemViews len = %d, want %d", len(all), len(wantSlugs)) } for i, sv := range all { if sv.Slug != wantSlugs[i] { t.Errorf("position %d: slug = %q, want %q", i, sv.Slug, wantSlugs[i]) } if sv.URL != "/views/"+sv.Slug { t.Errorf("position %d: URL = %q, want /views/%s", i, sv.URL, sv.Slug) } round := web.LookupSystemView(sv.Slug) if round == nil || round.Slug != sv.Slug { t.Errorf("LookupSystemView(%q) round-trip failed", sv.Slug) } } if web.LookupSystemView("not-a-system-slug") != nil { t.Error("LookupSystemView should return nil for unknown slugs") } } // TestLegacyRedirects verifies the slice C URL migration: each legacy // route 301-redirects to its /views/{slug} counterpart with chip params // preserved. func TestLegacyRedirects(t *testing.T) { srv, pool := mustServer(t) defer pool.Close() h := srv.Routes() cases := []struct { path, want string }{ {"/", "/views/tree"}, {"/dashboard", "/views/dashboard"}, {"/calendar", "/views/calendar"}, {"/timeline", "/views/timeline"}, {"/graph", "/views/graph"}, // chip params survive the redirect: {"/dashboard?tag=work", "/views/dashboard?tag=work"}, {"/timeline?from=2026-05-01", "/views/timeline?from=2026-05-01"}, } for _, tc := range cases { code, body := get(t, h, tc.path) if code != 301 { t.Errorf("GET %s status=%d body=%q, want 301", tc.path, code, body) } if !strings.Contains(body, `href="`+tc.want+`"`) { t.Errorf("GET %s body=%q, want redirect to %q", tc.path, body, tc.want) } } } // TestSidebarListsUserViews — slice E: every chrome-bearing page renders // the saved-view list under the main nav. Each entry links to // /views/{slug} with the name as the label. Active state fires when the // current URL matches. func TestSidebarListsUserViews(t *testing.T) { srv, pool := mustServer(t) defer pool.Close() h := srv.Routes() ctx := context.Background() stamp := strings.ReplaceAll(time.Now().UTC().Format("150405.000"), ".", "") slug := "p5j-e-sidebar-" + stamp defer pool.Exec(context.Background(), `DELETE FROM projax.views WHERE slug = $1`, slug) if _, err := pool.Exec(ctx, ` INSERT INTO projax.views (slug, name, filter_json) VALUES ($1, 'P5jE Sidebar', '{"view_type":"list"}'::jsonb)`, slug); err != nil { t.Fatalf("seed: %v", err) } _, body := get(t, h, "/views/tree") if !strings.Contains(body, `href="/views/`+slug+`"`) { t.Error("sidebar should list saved view as /views/") } if !strings.Contains(body, "P5jE Sidebar") { t.Error("sidebar should show saved view's display name") } if !strings.Contains(body, `href="/views/new"`) { t.Error("sidebar Views section should include a + New view link") } // Active state when the URL matches. _, onView := get(t, h, "/views/"+slug) if !strings.Contains(onView, `class="nav-item nav-item-user-view active"`) { t.Error("user-view nav-item should carry .active when its URL is current") } } // TestSidebarShowCountBadge — slice G: a saved view with show_count=true // renders a row-count badge in the sidebar reflecting the filter's match // count against ListAll(). func TestSidebarShowCountBadge(t *testing.T) { srv, pool := mustServer(t) defer pool.Close() h := srv.Routes() ctx := context.Background() stamp := strings.ReplaceAll(time.Now().UTC().Format("150405.000"), ".", "") slug := "p5j-g-badge-" + stamp defer pool.Exec(context.Background(), `DELETE FROM projax.views WHERE slug = $1`, slug) // Seed a view scoped to dev → its count = count of items under dev that // match status=active (default). if _, err := pool.Exec(ctx, ` INSERT INTO projax.views (slug, name, filter_json, show_count) VALUES ($1, 'P5jG Badge', '{"view_type":"list","project_path":"dev"}'::jsonb, true)`, slug); err != nil { t.Fatalf("seed view: %v", err) } _, body := get(t, h, "/views/tree") if !strings.Contains(body, `class="nav-badge"`) { t.Error("show_count view should render a nav-badge in the sidebar") } } // TestSidebarIconRenders — slice G: a view with an icon key emits the // SVG from the registry; missing key falls back to folder default. func TestSidebarIconRenders(t *testing.T) { srv, pool := mustServer(t) defer pool.Close() h := srv.Routes() ctx := context.Background() stamp := strings.ReplaceAll(time.Now().UTC().Format("150405.000"), ".", "") slug := "p5j-g-icon-" + stamp defer pool.Exec(context.Background(), `DELETE FROM projax.views WHERE slug = $1`, slug) if _, err := pool.Exec(ctx, ` INSERT INTO projax.views (slug, name, filter_json, icon) VALUES ($1, 'P5jG Icon', '{"view_type":"list"}'::jsonb, 'star')`, slug); err != nil { t.Fatalf("seed: %v", err) } _, body := get(t, h, "/views/tree") // The star icon's SVG path includes its distinctive 5-point polygon. if !strings.Contains(body, `polygon points="12 2 15.09 8.26`) { t.Error("sidebar should render the star icon SVG for icon=star") } } // TestLegacyViewUUIDRedirect — when a legacy URL carries the 5i overlay // `?view=` param, the redirect resolves the uuid to the current // slug (per m's Q3 pick), so old bookmarks land on the right user view. func TestLegacyViewUUIDRedirect(t *testing.T) { srv, pool := mustServer(t) defer pool.Close() h := srv.Routes() ctx := context.Background() stamp := strings.ReplaceAll(time.Now().UTC().Format("150405.000"), ".", "") slug := "p5j-c-legacy-" + stamp defer pool.Exec(context.Background(), `DELETE FROM projax.views WHERE slug = $1`, slug) var id string if err := pool.QueryRow(ctx, ` INSERT INTO projax.views (slug, name, filter_json) VALUES ($1, 'Legacy', '{"view_type":"list"}'::jsonb) RETURNING id`, slug).Scan(&id); err != nil { t.Fatalf("seed view: %v", err) } // Old-style URL: /?view= code, body := get(t, h, "/?view="+id) if code != 301 { t.Fatalf("GET /?view= status=%d body=%q want 301", code, body) } if !strings.Contains(body, "/views/"+slug) { t.Errorf("redirect should resolve uuid → slug; got body=%q", body) } }