549 lines
14 KiB
Go
549 lines
14 KiB
Go
package lang
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestParseComponentDefinitions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want AST
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "basic component with entity",
|
|
input: `page Test at "/test" layout main {
|
|
component table for User
|
|
}`,
|
|
want: AST{
|
|
Definitions: []Definition{
|
|
{
|
|
Page: &Page{
|
|
Name: "Test",
|
|
Path: "/test",
|
|
Layout: "main",
|
|
Components: []Component{
|
|
{
|
|
Type: "table",
|
|
Entity: stringPtr("User"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "form component with fields",
|
|
input: `page Test at "/test" layout main {
|
|
component form for User {
|
|
field name type text label "Full Name" placeholder "Enter your name" required
|
|
field email type email label "Email Address" required
|
|
field bio type textarea rows 5 placeholder "Tell us about yourself"
|
|
field avatar type file accept "image/*"
|
|
field role type select options ["admin", "user", "guest"] default "user"
|
|
}
|
|
}`,
|
|
want: AST{
|
|
Definitions: []Definition{
|
|
{
|
|
Page: &Page{
|
|
Name: "Test",
|
|
Path: "/test",
|
|
Layout: "main",
|
|
Components: []Component{
|
|
{
|
|
Type: "form",
|
|
Entity: stringPtr("User"),
|
|
Elements: []ComponentElement{
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "name",
|
|
Type: "text",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Full Name")},
|
|
{Placeholder: stringPtr("Enter your name")},
|
|
{Required: true},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "email",
|
|
Type: "email",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Email Address")},
|
|
{Required: true},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "bio",
|
|
Type: "textarea",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Rows: intPtr(5)},
|
|
{Placeholder: stringPtr("Tell us about yourself")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "avatar",
|
|
Type: "file",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Accept: stringPtr("image/*")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "role",
|
|
Type: "select",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Options: []string{"admin", "user", "guest"}},
|
|
{Default: stringPtr("user")},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "component with field attributes and validation",
|
|
input: `page Test at "/test" layout main {
|
|
component form for Product {
|
|
field name type text required validate min_length "3"
|
|
field price type number format "currency" validate min "0"
|
|
field category type autocomplete relates to Category
|
|
field tags type multiselect source "tags/popular"
|
|
field description type richtext
|
|
field featured type checkbox default "false"
|
|
field thumbnail type image thumbnail
|
|
}
|
|
}`,
|
|
want: AST{
|
|
Definitions: []Definition{
|
|
{
|
|
Page: &Page{
|
|
Name: "Test",
|
|
Path: "/test",
|
|
Layout: "main",
|
|
Components: []Component{
|
|
{
|
|
Type: "form",
|
|
Entity: stringPtr("Product"),
|
|
Elements: []ComponentElement{
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "name",
|
|
Type: "text",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Required: true},
|
|
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "price",
|
|
Type: "number",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Format: stringPtr("currency")},
|
|
{Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "category",
|
|
Type: "autocomplete",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Relates: &FieldRelation{Type: "Category"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "tags",
|
|
Type: "multiselect",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Source: stringPtr("tags/popular")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "description",
|
|
Type: "richtext",
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "featured",
|
|
Type: "checkbox",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Default: stringPtr("false")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "thumbnail",
|
|
Type: "image",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Thumbnail: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "component with buttons",
|
|
input: `page Test at "/test" layout main {
|
|
component form for User {
|
|
field name type text
|
|
button save label "Save User" style "primary" icon "save"
|
|
button cancel label "Cancel" style "secondary"
|
|
button delete label "Delete" style "danger" confirm "Are you sure?" disabled when is_protected
|
|
}
|
|
}`,
|
|
want: AST{
|
|
Definitions: []Definition{
|
|
{
|
|
Page: &Page{
|
|
Name: "Test",
|
|
Path: "/test",
|
|
Layout: "main",
|
|
Components: []Component{
|
|
{
|
|
Type: "form",
|
|
Entity: stringPtr("User"),
|
|
Elements: []ComponentElement{
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "name",
|
|
Type: "text",
|
|
},
|
|
},
|
|
{
|
|
Button: &ComponentButton{
|
|
Name: "save",
|
|
Label: "Save User",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "primary"}},
|
|
{Icon: &ComponentButtonIcon{Value: "save"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Button: &ComponentButton{
|
|
Name: "cancel",
|
|
Label: "Cancel",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Button: &ComponentButton{
|
|
Name: "delete",
|
|
Label: "Delete",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "danger"}},
|
|
{Confirm: &ComponentButtonConfirm{Value: "Are you sure?"}},
|
|
{Disabled: &ComponentButtonDisabled{Value: "is_protected"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "component with conditional fields",
|
|
input: `page Test at "/test" layout main {
|
|
component form for User {
|
|
field account_type type select options ["personal", "business"]
|
|
when account_type equals "business" {
|
|
field company_name type text required
|
|
field tax_id type text
|
|
button verify_business label "Verify Business"
|
|
}
|
|
when account_type equals "personal" {
|
|
field date_of_birth type date
|
|
}
|
|
}
|
|
}`,
|
|
want: AST{
|
|
Definitions: []Definition{
|
|
{
|
|
Page: &Page{
|
|
Name: "Test",
|
|
Path: "/test",
|
|
Layout: "main",
|
|
Components: []Component{
|
|
{
|
|
Type: "form",
|
|
Entity: stringPtr("User"),
|
|
Elements: []ComponentElement{
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "account_type",
|
|
Type: "select",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Options: []string{"personal", "business"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
When: &WhenCondition{
|
|
Field: "account_type",
|
|
Operator: "equals",
|
|
Value: "business",
|
|
Fields: []ComponentField{
|
|
{
|
|
Name: "company_name",
|
|
Type: "text",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Required: true},
|
|
},
|
|
},
|
|
{
|
|
Name: "tax_id",
|
|
Type: "text",
|
|
},
|
|
},
|
|
Buttons: []ComponentButton{
|
|
{
|
|
Name: "verify_business",
|
|
Label: "Verify Business",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
When: &WhenCondition{
|
|
Field: "account_type",
|
|
Operator: "equals",
|
|
Value: "personal",
|
|
Fields: []ComponentField{
|
|
{
|
|
Name: "date_of_birth",
|
|
Type: "date",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "component with nested sections",
|
|
input: `page Test at "/test" layout main {
|
|
component dashboard {
|
|
section stats type container class "stats-grid" {
|
|
component metric {
|
|
field total_users type display value "1,234"
|
|
field revenue type display format "currency" value "45,678"
|
|
}
|
|
}
|
|
section charts type container {
|
|
component chart for Analytics {
|
|
data from "analytics/monthly"
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
want: AST{
|
|
Definitions: []Definition{
|
|
{
|
|
Page: &Page{
|
|
Name: "Test",
|
|
Path: "/test",
|
|
Layout: "main",
|
|
Components: []Component{
|
|
{
|
|
Type: "dashboard",
|
|
Elements: []ComponentElement{
|
|
{
|
|
Section: &Section{
|
|
Name: "stats",
|
|
Type: stringPtr("container"),
|
|
Class: stringPtr("stats-grid"),
|
|
Elements: []SectionElement{
|
|
{
|
|
Component: &Component{
|
|
Type: "metric",
|
|
Elements: []ComponentElement{
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "total_users",
|
|
Type: "display",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Value: stringPtr("1,234")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "revenue",
|
|
Type: "display",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Format: stringPtr("currency")},
|
|
{Value: stringPtr("45,678")},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Section: &Section{
|
|
Name: "charts",
|
|
Type: stringPtr("container"),
|
|
Elements: []SectionElement{
|
|
{
|
|
Component: &Component{
|
|
Type: "chart",
|
|
Entity: stringPtr("Analytics"),
|
|
Elements: []ComponentElement{
|
|
{
|
|
Attribute: &ComponentAttr{
|
|
DataSource: stringPtr("analytics/monthly"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ParseInput(tt.input)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !astEqual(got, tt.want) {
|
|
t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseComponentFieldTypes(t *testing.T) {
|
|
fieldTypes := []string{
|
|
"text", "email", "password", "number", "date", "datetime", "time",
|
|
"textarea", "richtext", "select", "multiselect", "checkbox", "radio",
|
|
"file", "image", "autocomplete", "range", "color", "url", "tel",
|
|
"hidden", "display", "json", "code",
|
|
}
|
|
|
|
for _, fieldType := range fieldTypes {
|
|
t.Run("field_type_"+fieldType, func(t *testing.T) {
|
|
input := `page Test at "/test" layout main {
|
|
component form {
|
|
field test_field type ` + fieldType + `
|
|
}
|
|
}`
|
|
|
|
got, err := ParseInput(input)
|
|
if err != nil {
|
|
t.Errorf("ParseInput() failed for field type %s: %v", fieldType, err)
|
|
return
|
|
}
|
|
|
|
if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
|
|
t.Errorf("ParseInput() failed to parse page for field type %s", fieldType)
|
|
return
|
|
}
|
|
|
|
page := got.Definitions[0].Page
|
|
if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
|
|
t.Errorf("ParseInput() failed to parse component for field type %s", fieldType)
|
|
return
|
|
}
|
|
|
|
element := page.Components[0].Elements[0]
|
|
if element.Field == nil || element.Field.Type != fieldType {
|
|
t.Errorf("ParseInput() field type mismatch: got %v, want %s", element.Field, fieldType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseComponentErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
}{
|
|
{
|
|
name: "missing component type",
|
|
input: `page Test at "/test" layout main {
|
|
component
|
|
}`,
|
|
},
|
|
{
|
|
name: "invalid field syntax",
|
|
input: `page Test at "/test" layout main {
|
|
component form {
|
|
field name
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "invalid button syntax",
|
|
input: `page Test at "/test" layout main {
|
|
component form {
|
|
button
|
|
}
|
|
}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := ParseInput(tt.input)
|
|
if err == nil {
|
|
t.Errorf("ParseInput() expected error for invalid syntax, got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|