576 lines
16 KiB
Go
576 lines
16 KiB
Go
package lang
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestParseAdvancedUIFeatures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want AST
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "complex conditional rendering with multiple operators",
|
|
input: `page Test at "/test" layout main {
|
|
component form for User {
|
|
field status type select options ["active", "inactive", "pending"]
|
|
|
|
when status equals "active" {
|
|
field last_login type datetime
|
|
field permissions type multiselect
|
|
button deactivate label "Deactivate User" style "warning"
|
|
}
|
|
|
|
when status not_equals "active" {
|
|
field reason type textarea placeholder "Reason for status"
|
|
button activate label "Activate User" style "success"
|
|
}
|
|
|
|
when status contains "pending" {
|
|
field approval_date type date
|
|
button approve label "Approve" style "primary"
|
|
button reject label "Reject" style "danger"
|
|
}
|
|
}
|
|
}`,
|
|
want: AST{
|
|
Definitions: []Definition{
|
|
{
|
|
Page: &Page{
|
|
Name: "Test",
|
|
Path: "/test",
|
|
Layout: "main",
|
|
Components: []Component{
|
|
{
|
|
Type: "form",
|
|
Entity: stringPtr("User"),
|
|
Elements: []ComponentElement{
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "status",
|
|
Type: "select",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Options: []string{"active", "inactive", "pending"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
When: &WhenCondition{
|
|
Field: "status",
|
|
Operator: "equals",
|
|
Value: "active",
|
|
Fields: []ComponentField{
|
|
{Name: "last_login", Type: "datetime"},
|
|
{Name: "permissions", Type: "multiselect"},
|
|
},
|
|
Buttons: []ComponentButton{
|
|
{
|
|
Name: "deactivate",
|
|
Label: "Deactivate User",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "warning"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
When: &WhenCondition{
|
|
Field: "status",
|
|
Operator: "not_equals",
|
|
Value: "active",
|
|
Fields: []ComponentField{
|
|
{
|
|
Name: "reason",
|
|
Type: "textarea",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Placeholder: stringPtr("Reason for status")},
|
|
},
|
|
},
|
|
},
|
|
Buttons: []ComponentButton{
|
|
{
|
|
Name: "activate",
|
|
Label: "Activate User",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "success"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
When: &WhenCondition{
|
|
Field: "status",
|
|
Operator: "contains",
|
|
Value: "pending",
|
|
Fields: []ComponentField{
|
|
{Name: "approval_date", Type: "date"},
|
|
},
|
|
Buttons: []ComponentButton{
|
|
{
|
|
Name: "approve",
|
|
Label: "Approve",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "primary"}},
|
|
},
|
|
},
|
|
{
|
|
Name: "reject",
|
|
Label: "Reject",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "danger"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "field attributes with all possible options",
|
|
input: `page Test at "/test" layout main {
|
|
component form for Product {
|
|
field name type text {
|
|
label "Product Name"
|
|
placeholder "Enter product name"
|
|
required
|
|
default "New Product"
|
|
validate min_length "3"
|
|
size "large"
|
|
display "block"
|
|
}
|
|
|
|
field price type number {
|
|
label "Price ($)"
|
|
format "currency"
|
|
validate min "0"
|
|
validate max "10000"
|
|
}
|
|
|
|
field category type autocomplete {
|
|
label "Category"
|
|
placeholder "Start typing..."
|
|
relates to Category
|
|
searchable
|
|
source "categories/search"
|
|
}
|
|
|
|
field tags type multiselect {
|
|
label "Tags"
|
|
options ["electronics", "clothing", "books", "home"]
|
|
source "tags/popular"
|
|
}
|
|
|
|
field description type richtext {
|
|
label "Description"
|
|
rows 10
|
|
placeholder "Describe your product..."
|
|
}
|
|
|
|
field thumbnail type image {
|
|
label "Product Image"
|
|
accept "image/jpeg,image/png"
|
|
thumbnail
|
|
}
|
|
|
|
field featured type checkbox {
|
|
label "Featured Product"
|
|
default "false"
|
|
value "true"
|
|
}
|
|
|
|
field availability type select {
|
|
label "Availability"
|
|
options ["in_stock", "out_of_stock", "pre_order"]
|
|
default "in_stock"
|
|
sortable
|
|
}
|
|
}
|
|
}`,
|
|
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{
|
|
{Label: stringPtr("Product Name")},
|
|
{Placeholder: stringPtr("Enter product name")},
|
|
{Required: true},
|
|
{Default: stringPtr("New Product")},
|
|
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
|
|
{Size: stringPtr("large")},
|
|
{Display: stringPtr("block")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "price",
|
|
Type: "number",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Price ($)")},
|
|
{Format: stringPtr("currency")},
|
|
{Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
|
|
{Validation: &ComponentValidation{Type: "max", Value: stringPtr("10000")}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "category",
|
|
Type: "autocomplete",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Category")},
|
|
{Placeholder: stringPtr("Start typing...")},
|
|
{Relates: &FieldRelation{Type: "Category"}},
|
|
{Searchable: true},
|
|
{Source: stringPtr("categories/search")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "tags",
|
|
Type: "multiselect",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Tags")},
|
|
{Options: []string{"electronics", "clothing", "books", "home"}},
|
|
{Source: stringPtr("tags/popular")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "description",
|
|
Type: "richtext",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Description")},
|
|
{Rows: intPtr(10)},
|
|
{Placeholder: stringPtr("Describe your product...")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "thumbnail",
|
|
Type: "image",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Product Image")},
|
|
{Accept: stringPtr("image/jpeg,image/png")},
|
|
{Thumbnail: true},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "featured",
|
|
Type: "checkbox",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Featured Product")},
|
|
{Default: stringPtr("false")},
|
|
{Value: stringPtr("true")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "availability",
|
|
Type: "select",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Label: stringPtr("Availability")},
|
|
{Options: []string{"in_stock", "out_of_stock", "pre_order"}},
|
|
{Default: stringPtr("in_stock")},
|
|
{Sortable: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "complex button configurations",
|
|
input: `page Test at "/test" layout main {
|
|
component form for Order {
|
|
field status type select options ["draft", "submitted", "approved"]
|
|
|
|
button save label "Save Draft" style "secondary" icon "save" position "left"
|
|
button submit label "Submit Order" style "primary" icon "send" loading "Submitting..." confirm "Submit this order?"
|
|
button approve label "Approve" style "success" loading "Approving..." disabled when status confirm "Approve this order?" target approval_modal via "api/orders/approve"
|
|
button reject label "Reject" style "danger" icon "x" confirm "Are you sure you want to reject this order?"
|
|
button print label "Print" style "outline" icon "printer" position "right"
|
|
}
|
|
}`,
|
|
want: AST{
|
|
Definitions: []Definition{
|
|
{
|
|
Page: &Page{
|
|
Name: "Test",
|
|
Path: "/test",
|
|
Layout: "main",
|
|
Components: []Component{
|
|
{
|
|
Type: "form",
|
|
Entity: stringPtr("Order"),
|
|
Elements: []ComponentElement{
|
|
{
|
|
Field: &ComponentField{
|
|
Name: "status",
|
|
Type: "select",
|
|
Attributes: []ComponentFieldAttribute{
|
|
{Options: []string{"draft", "submitted", "approved"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Button: &ComponentButton{
|
|
Name: "save",
|
|
Label: "Save Draft",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
|
{Icon: &ComponentButtonIcon{Value: "save"}},
|
|
{Position: &ComponentButtonPosition{Value: "left"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Button: &ComponentButton{
|
|
Name: "submit",
|
|
Label: "Submit Order",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "primary"}},
|
|
{Icon: &ComponentButtonIcon{Value: "send"}},
|
|
{Loading: &ComponentButtonLoading{Value: "Submitting..."}},
|
|
{Confirm: &ComponentButtonConfirm{Value: "Submit this order?"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Button: &ComponentButton{
|
|
Name: "approve",
|
|
Label: "Approve",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "success"}},
|
|
{Loading: &ComponentButtonLoading{Value: "Approving..."}},
|
|
{Disabled: &ComponentButtonDisabled{Value: "status"}},
|
|
{Confirm: &ComponentButtonConfirm{Value: "Approve this order?"}},
|
|
{Target: &ComponentButtonTarget{Value: "approval_modal"}},
|
|
{Via: &ComponentButtonVia{Value: "api/orders/approve"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Button: &ComponentButton{
|
|
Name: "reject",
|
|
Label: "Reject",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "danger"}},
|
|
{Icon: &ComponentButtonIcon{Value: "x"}},
|
|
{Confirm: &ComponentButtonConfirm{Value: "Are you sure you want to reject this order?"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Button: &ComponentButton{
|
|
Name: "print",
|
|
Label: "Print",
|
|
Attributes: []ComponentButtonAttr{
|
|
{Style: &ComponentButtonStyle{Value: "outline"}},
|
|
{Icon: &ComponentButtonIcon{Value: "printer"}},
|
|
{Position: &ComponentButtonPosition{Value: "right"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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 TestParseFieldValidationTypes(t *testing.T) {
|
|
validationTypes := []struct {
|
|
validation string
|
|
hasValue bool
|
|
}{
|
|
{"email", false},
|
|
{"required", false},
|
|
{"min_length", true},
|
|
{"max_length", true},
|
|
{"min", true},
|
|
{"max", true},
|
|
{"pattern", true},
|
|
{"numeric", false},
|
|
{"alpha", false},
|
|
{"alphanumeric", false},
|
|
{"url", false},
|
|
{"date", false},
|
|
{"datetime", false},
|
|
{"time", false},
|
|
{"phone", false},
|
|
{"postal_code", false},
|
|
{"credit_card", false},
|
|
}
|
|
|
|
for _, vt := range validationTypes {
|
|
t.Run("validation_"+vt.validation, func(t *testing.T) {
|
|
var input string
|
|
if vt.hasValue {
|
|
input = `page Test at "/test" layout main {
|
|
component form {
|
|
field test_field type text validate ` + vt.validation + ` "test_value"
|
|
}
|
|
}`
|
|
} else {
|
|
input = `page Test at "/test" layout main {
|
|
component form {
|
|
field test_field type text validate ` + vt.validation + `
|
|
}
|
|
}`
|
|
}
|
|
|
|
got, err := ParseInput(input)
|
|
if err != nil {
|
|
t.Errorf("ParseInput() failed for validation %s: %v", vt.validation, err)
|
|
return
|
|
}
|
|
|
|
if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
|
|
t.Errorf("ParseInput() failed to parse page for validation %s", vt.validation)
|
|
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 validation %s", vt.validation)
|
|
return
|
|
}
|
|
|
|
element := page.Components[0].Elements[0]
|
|
if element.Field == nil || len(element.Field.Attributes) != 1 {
|
|
t.Errorf("ParseInput() failed to parse field attributes for validation %s", vt.validation)
|
|
return
|
|
}
|
|
|
|
attr := element.Field.Attributes[0]
|
|
if attr.Validation == nil || attr.Validation.Type != vt.validation {
|
|
t.Errorf("ParseInput() validation type mismatch: got %v, want %s", attr.Validation, vt.validation)
|
|
}
|
|
|
|
if vt.hasValue && (attr.Validation.Value == nil || *attr.Validation.Value != "test_value") {
|
|
t.Errorf("ParseInput() validation value mismatch for %s", vt.validation)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseConditionalOperators(t *testing.T) {
|
|
operators := []string{"equals", "not_equals", "contains"}
|
|
|
|
for _, op := range operators {
|
|
t.Run("operator_"+op, func(t *testing.T) {
|
|
input := `page Test at "/test" layout main {
|
|
component form {
|
|
field test_field type text
|
|
when test_field ` + op + ` "test_value" {
|
|
field conditional_field type text
|
|
}
|
|
}
|
|
}`
|
|
|
|
got, err := ParseInput(input)
|
|
if err != nil {
|
|
t.Errorf("ParseInput() failed for operator %s: %v", op, err)
|
|
return
|
|
}
|
|
|
|
// Verify the when condition was parsed correctly
|
|
page := got.Definitions[0].Page
|
|
component := page.Components[0]
|
|
whenElement := component.Elements[1].When
|
|
|
|
if whenElement == nil || whenElement.Operator != op {
|
|
t.Errorf("ParseInput() operator mismatch: got %v, want %s", whenElement, op)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseAdvancedUIErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
}{
|
|
{
|
|
name: "invalid conditional operator",
|
|
input: `page Test at "/test" layout main {
|
|
component form {
|
|
when field invalid_operator "value" {
|
|
field test type text
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "missing field attribute block closure",
|
|
input: `page Test at "/test" layout main {
|
|
component form {
|
|
field test type text {
|
|
label "Test"
|
|
required
|
|
}
|
|
}`,
|
|
},
|
|
}
|
|
|
|
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")
|
|
}
|
|
})
|
|
}
|
|
}
|