All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m48s
CI/CD / test-sqlite (pull_request) Successful in 2m57s
CI/CD / build-and-deploy (pull_request) Successful in 1m23s
Packages can declare a `surfaces` array with per-path access controls, titles, and layouts. A single package can serve a public form, an authenticated dashboard, and an admin page — each with independent access enforcement. Kernel: - Manifest validation for surfaces array (path, access, duplicates) - Auto-synthesis from legacy auth/layout for existing packages - Unified /s/:slug route tree (RegisterExtensionRoutes) dispatches between surface rendering and ext API calls - matchSurface() with Gin-style :param support and specificity ordering - evaluateAccess() for per-surface access checks - Nav filters out pending_review packages (pre-existing bug fix) Frontend: - __SURFACE_PATH__ and __SURFACE_PARAMS__ template injection - sw.navigate(path, params) for SPA-style intra-package routing - surface.navigate event + popstate handling - SDK version bumped to 0.9.0 Docs: - MULTI-SURFACE-GUIDE.md — full developer guide - PACKAGE-FORMAT.md — surfaces field reference - CHANGELOG.md, ROADMAP.md updated 22 new tests (11 manifest validation, 11 route matching/nav). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
176 lines
4.9 KiB
Go
176 lines
4.9 KiB
Go
package pages
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// ── matchSurface tests ──────────────────────────────────────
|
|
|
|
func TestMatchSurface_StaticRoot(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/", "title": "Dashboard"},
|
|
map[string]any{"path": "/submit", "title": "Submit"},
|
|
}
|
|
s, params := matchSurface(surfaces, "/")
|
|
if s == nil {
|
|
t.Fatal("expected match for /")
|
|
}
|
|
if s["title"] != "Dashboard" {
|
|
t.Errorf("expected Dashboard, got %v", s["title"])
|
|
}
|
|
if len(params) != 0 {
|
|
t.Errorf("expected no params, got %v", params)
|
|
}
|
|
}
|
|
|
|
func TestMatchSurface_StaticPath(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/", "title": "Dashboard"},
|
|
map[string]any{"path": "/submit", "title": "Submit"},
|
|
map[string]any{"path": "/monitor", "title": "Monitor"},
|
|
}
|
|
s, _ := matchSurface(surfaces, "/submit")
|
|
if s == nil {
|
|
t.Fatal("expected match for /submit")
|
|
}
|
|
if s["title"] != "Submit" {
|
|
t.Errorf("expected Submit, got %v", s["title"])
|
|
}
|
|
}
|
|
|
|
func TestMatchSurface_ParamPath(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/", "title": "Dashboard"},
|
|
map[string]any{"path": "/:id", "title": "Detail"},
|
|
}
|
|
s, params := matchSurface(surfaces, "/abc123")
|
|
if s == nil {
|
|
t.Fatal("expected match for /:id")
|
|
}
|
|
if s["title"] != "Detail" {
|
|
t.Errorf("expected Detail, got %v", s["title"])
|
|
}
|
|
if params["id"] != "abc123" {
|
|
t.Errorf("expected id=abc123, got %v", params["id"])
|
|
}
|
|
}
|
|
|
|
func TestMatchSurface_MultiSegmentParam(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/monitor", "title": "Monitor"},
|
|
map[string]any{"path": "/monitor/:id", "title": "Instance Detail"},
|
|
}
|
|
s, params := matchSurface(surfaces, "/monitor/inst-42")
|
|
if s == nil {
|
|
t.Fatal("expected match for /monitor/:id")
|
|
}
|
|
if s["title"] != "Instance Detail" {
|
|
t.Errorf("expected Instance Detail, got %v", s["title"])
|
|
}
|
|
if params["id"] != "inst-42" {
|
|
t.Errorf("expected id=inst-42, got %v", params["id"])
|
|
}
|
|
}
|
|
|
|
func TestMatchSurface_NoMatch(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/", "title": "Dashboard"},
|
|
map[string]any{"path": "/submit", "title": "Submit"},
|
|
}
|
|
s, _ := matchSurface(surfaces, "/nonexistent/deep")
|
|
if s != nil {
|
|
t.Errorf("expected no match, got %v", s)
|
|
}
|
|
}
|
|
|
|
func TestMatchSurface_StaticPreferredOverParam(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/:id", "title": "Detail"},
|
|
map[string]any{"path": "/new", "title": "New"},
|
|
}
|
|
s, params := matchSurface(surfaces, "/new")
|
|
if s == nil {
|
|
t.Fatal("expected match for /new")
|
|
}
|
|
if s["title"] != "New" {
|
|
t.Errorf("expected static match 'New', got %v", s["title"])
|
|
}
|
|
if len(params) != 0 {
|
|
t.Errorf("expected no params for static match, got %v", params)
|
|
}
|
|
}
|
|
|
|
func TestMatchSurface_EmptySurfaces(t *testing.T) {
|
|
s, _ := matchSurface([]any{}, "/")
|
|
if s != nil {
|
|
t.Errorf("expected no match for empty surfaces, got %v", s)
|
|
}
|
|
}
|
|
|
|
// ── splitPath tests ─────────────────────────────────────────
|
|
|
|
func TestSplitPath(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want int
|
|
}{
|
|
{"/", 0},
|
|
{"/submit", 1},
|
|
{"/monitor/:id", 2},
|
|
{"/:id/edit", 2},
|
|
}
|
|
for _, tt := range tests {
|
|
parts := splitPath(tt.input)
|
|
if len(parts) != tt.want {
|
|
t.Errorf("splitPath(%q) = %d parts, want %d", tt.input, len(parts), tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── findNavSurface tests ────────────────────────────────────
|
|
|
|
func TestFindNavSurface_ExplicitNav(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/", "title": "Dashboard"},
|
|
map[string]any{"path": "/monitor", "title": "Monitor", "nav": true},
|
|
}
|
|
s := findNavSurface(surfaces)
|
|
if s == nil {
|
|
t.Fatal("expected nav surface")
|
|
}
|
|
if s["title"] != "Monitor" {
|
|
t.Errorf("expected Monitor, got %v", s["title"])
|
|
}
|
|
}
|
|
|
|
func TestFindNavSurface_FallbackRoot(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/submit", "title": "Submit"},
|
|
map[string]any{"path": "/", "title": "Dashboard"},
|
|
}
|
|
s := findNavSurface(surfaces)
|
|
if s == nil {
|
|
t.Fatal("expected nav surface")
|
|
}
|
|
if s["title"] != "Dashboard" {
|
|
t.Errorf("expected Dashboard, got %v", s["title"])
|
|
}
|
|
}
|
|
|
|
func TestFindNavSurface_NoMatch(t *testing.T) {
|
|
surfaces := []any{
|
|
map[string]any{"path": "/submit", "title": "Submit"},
|
|
map[string]any{"path": "/admin", "title": "Admin"},
|
|
}
|
|
s := findNavSurface(surfaces)
|
|
if s != nil {
|
|
t.Errorf("expected nil, got %v", s)
|
|
}
|
|
}
|
|
|
|
// ── evaluateAccess tests ────────────────────────────────────
|
|
// Note: evaluateAccess depends on gin.Context which is hard to unit test
|
|
// without a full Gin setup. These are covered via handler integration tests.
|
|
// The matchSurface + splitPath + findNavSurface functions above are the
|
|
// pure-logic components that benefit most from unit testing.
|