allow for sections and components in any order on pages

This commit is contained in:
2025-09-09 22:30:00 -06:00
parent 88d757546a
commit b82e22c38d
11 changed files with 1179 additions and 848 deletions

View File

@ -12,14 +12,14 @@ func TestParsePageDefinitions(t *testing.T) {
wantErr bool
}{
{
name: "basic page with minimal fields",
input: `page Dashboard at "/dashboard" layout main`,
name: "basic page definition",
input: `page Home at "/" layout main`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Dashboard",
Path: "/dashboard",
Name: "Home",
Path: "/",
Layout: "main",
},
},
@ -27,17 +27,17 @@ func TestParsePageDefinitions(t *testing.T) {
},
},
{
name: "page with all optional fields",
input: `page UserProfile at "/profile" layout main title "User Profile" desc "Manage user profile settings" auth`,
name: "page with optional fields",
input: `page Settings at "/settings" layout main title "User Settings" desc "Manage your account settings" auth`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "UserProfile",
Path: "/profile",
Name: "Settings",
Path: "/settings",
Layout: "main",
Title: stringPtr("User Profile"),
Description: stringPtr("Manage user profile settings"),
Title: stringPtr("User Settings"),
Description: stringPtr("Manage your account settings"),
Auth: true,
},
},
@ -46,20 +46,22 @@ func TestParsePageDefinitions(t *testing.T) {
},
{
name: "page with meta tags",
input: `page HomePage at "/" layout main {
meta description "Welcome to our application"
meta keywords "app, dashboard, management"
input: `page Settings at "/settings" layout main {
meta description "Settings page description"
meta keywords "settings, user, account"
meta author "My App"
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "HomePage",
Path: "/",
Name: "Settings",
Path: "/settings",
Layout: "main",
Meta: []MetaTag{
{Name: "description", Content: "Welcome to our application"},
{Name: "keywords", Content: "app, dashboard, management"},
{Name: "description", Content: "Settings page description"},
{Name: "keywords", Content: "settings, user, account"},
{Name: "author", Content: "My App"},
},
},
},
@ -67,19 +69,17 @@ func TestParsePageDefinitions(t *testing.T) {
},
},
{
name: "page with nested sections",
name: "page with sections",
input: `page Settings at "/settings" layout main {
section tabs type tab {
section profile label "Profile" active {
component form for User {
field name type text
}
component form for User
}
section security label "Security" {
component form for User {
field password type password
}
component form for Security
}
section notifications label "Notifications" {
component toggle for NotificationSettings
}
}
}`,
@ -90,50 +90,50 @@ func TestParsePageDefinitions(t *testing.T) {
Name: "Settings",
Path: "/settings",
Layout: "main",
Sections: []Section{
Elements: []PageElement{
{
Name: "tabs",
Type: stringPtr("tab"),
Elements: []SectionElement{
{
Section: &Section{
Name: "profile",
Label: stringPtr("Profile"),
Active: true,
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
},
},
Section: &Section{
Name: "tabs",
Type: stringPtr("tab"),
Elements: []SectionElement{
{
Section: &Section{
Name: "profile",
Label: stringPtr("Profile"),
Active: true,
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("User"),
},
},
},
},
},
},
{
Section: &Section{
Name: "security",
Label: stringPtr("Security"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "password",
Type: "password",
},
},
{
Section: &Section{
Name: "security",
Label: stringPtr("Security"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("Security"),
},
},
},
},
},
{
Section: &Section{
Name: "notifications",
Label: stringPtr("Notifications"),
Elements: []SectionElement{
{
Component: &Component{
Type: "toggle",
Entity: stringPtr("NotificationSettings"),
},
},
},
@ -149,71 +149,124 @@ func TestParsePageDefinitions(t *testing.T) {
},
},
{
name: "page with modal and panel sections",
input: `page ProductList at "/products" layout main {
section main type container {
component table for Product
name: "page with components",
input: `page Dashboard at "/dashboard" layout main {
component stats for Analytics {
field total_users type display
field revenue type display format "currency"
}
section editModal type modal trigger "edit-product" {
component form for Product {
field name type text required
button save label "Save Changes" style "primary"
}
}
section filters type panel position "left" {
component form {
field category type select
field price_range type range
}
component chart for SalesData {
data from "analytics/sales"
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "ProductList",
Path: "/products",
Name: "Dashboard",
Path: "/dashboard",
Layout: "main",
Sections: []Section{
Elements: []PageElement{
{
Name: "main",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Component: &Component{
Type: "table",
Entity: stringPtr("Product"),
Component: &Component{
Type: "stats",
Entity: stringPtr("Analytics"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "total_users",
Type: "display",
},
},
{
Field: &ComponentField{
Name: "revenue",
Type: "display",
Attributes: []ComponentFieldAttribute{
{Format: stringPtr("currency")},
},
},
},
},
},
},
{
Name: "editModal",
Type: stringPtr("modal"),
Trigger: stringPtr("edit-product"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("Product"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
Attributes: []ComponentFieldAttribute{
{Required: true},
},
},
},
{
Button: &ComponentButton{
Name: "save",
Label: "Save Changes",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "primary"}},
Component: &Component{
Type: "chart",
Entity: stringPtr("SalesData"),
Elements: []ComponentElement{
{
Attribute: &ComponentAttr{
DataSource: stringPtr("analytics/sales"),
},
},
},
},
},
},
},
},
},
},
},
{
name: "page with mixed sections and components",
input: `page Home at "/" layout main {
component hero for Banner {
field title type display
field subtitle type display
}
section content type container {
component posts for Post {
fields [title, excerpt, date]
}
}
component newsletter for Subscription {
field email type email required
button subscribe label "Subscribe"
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Home",
Path: "/",
Layout: "main",
Elements: []PageElement{
{
Component: &Component{
Type: "hero",
Entity: stringPtr("Banner"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "title",
Type: "display",
},
},
{
Field: &ComponentField{
Name: "subtitle",
Type: "display",
},
},
},
},
},
{
Section: &Section{
Name: "content",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Component: &Component{
Type: "posts",
Entity: stringPtr("Post"),
Elements: []ComponentElement{
{
Attribute: &ComponentAttr{
Fields: []string{"title", "excerpt", "date"},
},
},
},
@ -223,28 +276,25 @@ func TestParsePageDefinitions(t *testing.T) {
},
},
{
Name: "filters",
Type: stringPtr("panel"),
Position: stringPtr("left"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "category",
Type: "select",
},
},
{
Field: &ComponentField{
Name: "price_range",
Type: "range",
},
Component: &Component{
Type: "newsletter",
Entity: stringPtr("Subscription"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "email",
Type: "email",
Attributes: []ComponentFieldAttribute{
{Required: true},
},
},
},
{
Button: &ComponentButton{
Name: "subscribe",
Label: "Subscribe",
},
},
},
},
},
@ -264,7 +314,66 @@ func TestParsePageDefinitions(t *testing.T) {
return
}
if !astEqual(got, tt.want) {
t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
t.Errorf("ParseInput() = %v, want %v", got, tt.want)
}
})
}
}
func TestParsePageLayouts(t *testing.T) {
layouts := []string{"main", "admin", "public", "auth", "minimal", "dashboard"}
for _, layout := range layouts {
t.Run("layout_"+layout, func(t *testing.T) {
input := `page Test at "/test" layout ` + layout
got, err := ParseInput(input)
if err != nil {
t.Errorf("ParseInput() failed for layout %s: %v", layout, err)
return
}
if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
t.Errorf("ParseInput() failed to parse page for layout %s", layout)
return
}
page := got.Definitions[0].Page
if page.Layout != layout {
t.Errorf("ParseInput() layout mismatch: got %s, want %s", page.Layout, layout)
}
})
}
}
func TestParsePagePaths(t *testing.T) {
tests := []struct {
name string
path string
}{
{"root", "/"},
{"simple", "/about"},
{"nested", "/admin/users"},
{"deep_nested", "/api/v1/users/profile"},
{"with_params", "/users/:id"},
{"with_multiple_params", "/users/:userId/posts/:postId"},
{"with_query", "/search?q=:query"},
{"with_extension", "/api/users.json"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := `page Test at "` + tt.path + `" layout main`
got, err := ParseInput(input)
if err != nil {
t.Errorf("ParseInput() failed for path %s: %v", tt.path, err)
return
}
page := got.Definitions[0].Page
if page.Path != tt.path {
t.Errorf("ParseInput() path mismatch: got %s, want %s", page.Path, tt.path)
}
})
}
@ -276,16 +385,26 @@ func TestParsePageErrors(t *testing.T) {
input string
}{
{
name: "missing layout",
input: `page Dashboard at "/dashboard"`,
name: "missing page name",
input: `page at "/" layout main`,
},
{
name: "missing path",
input: `page Dashboard layout main`,
input: `page Test layout main`,
},
{
name: "missing layout",
input: `page Test at "/"`,
},
{
name: "invalid path format",
input: `page Dashboard at dashboard layout main`,
input: `page Test at /invalid layout main`,
},
{
name: "unclosed page block",
input: `page Test at "/" layout main {
section test type container
`,
},
}