package integration_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
. "github.com/qor5/admin/presets"
"github.com/qor5/web"
"github.com/qor5/web/multipartestutils"
"github.com/sunfmin/reflectutils"
h "github.com/theplant/htmlgo"
"github.com/theplant/testingutils"
"github.com/thoas/go-funk"
)
type Company struct {
Name string
FoundedAt time.Time
}
type Media string
type User struct {
ID int
Int1 int
Float1 float32
String1 string
Bool1 bool
Time1 time.Time
Company *Company
Media1 Media
}
func TestFields(t *testing.T) {
vd := &web.ValidationErrors{}
vd.FieldError("String1", "too small")
ft := NewFieldDefaults(WRITE).Exclude("ID")
ft.FieldType(time.Time{}).ComponentFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
return h.Div().Class("time-control").
Text(field.Value(obj).(time.Time).Format("2006-01-02")).
Attr(web.VFieldName(field.Name)...)
})
ft.FieldType(Media("")).ComponentFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
if field.ContextValue("a") == nil {
return h.Text("")
}
return h.Text(field.ContextValue("a").(string) + ", " + field.ContextValue("b").(string))
})
r := httptest.NewRequest("GET", "/hello", nil)
ctx := &web.EventContext{R: r, Flash: vd}
time1 := time.Unix(1567048169, 0)
time1LocalFormat := time1.Local().Format("2006-01-02 15:04:05")
user := &User{
ID: 1,
Int1: 2,
Float1: 23.1,
String1: "hello",
Bool1: true,
Time1: time1,
Company: &Company{
Name: "Company1",
FoundedAt: time.Unix(1567048169, 0),
},
}
mb := New().Model(&User{})
ftRead := NewFieldDefaults(LIST)
var cases = []struct {
name string
toComponentFun func() h.HTMLComponent
expect string
}{
{
name: "Only with additional nested object",
toComponentFun: func() h.HTMLComponent {
return ft.InspectFields(&User{}).
Labels("Int1", "整数1", "Company.Name", "公司名").
Only("Int1", "Float1", "String1", "Bool1", "Time1", "Company.Name", "Company.FoundedAt").
ToComponent(
mb.Info(),
user,
ctx)
},
expect: `
2019-08-29
2019-08-29
`,
},
{
name: "Except with file glob pattern",
toComponentFun: func() h.HTMLComponent {
return ft.InspectFields(&User{}).
Except("Bool*").
ToComponent(mb.Info(), user, ctx)
},
expect: `
2019-08-29
`,
},
{
name: "Read Except with file glob pattern",
toComponentFun: func() h.HTMLComponent {
return ftRead.InspectFields(&User{}).
Except("Float*").ToComponent(mb.Info(), user, ctx)
},
expect: `
1 |
2 |
hello |
true |
`,
},
{
name: "Read for a time field",
toComponentFun: func() h.HTMLComponent {
return ftRead.InspectFields(&User{}).
Only("Time1", "Int1").ToComponent(mb.Info(), user, ctx)
},
expect: fmt.Sprintf(`
%s |
2 |
`, time1LocalFormat),
},
{
name: "pass in context",
toComponentFun: func() h.HTMLComponent {
fb := ft.InspectFields(&User{}).
Only("Media1")
fb.Field("Media1").
WithContextValue("a", "context value1").
WithContextValue("b", "context value2")
return fb.ToComponent(mb.Info(), user, ctx)
},
expect: `context value1, context value2`,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
output := h.MustString(c.toComponentFun(), web.WrapEventContext(context.TODO(), ctx))
diff := testingutils.PrettyJsonDiff(c.expect, output)
if len(diff) > 0 {
t.Error(c.name, diff)
}
})
}
}
type Person struct {
Addresses []*Org
}
type Org struct {
Name string
Address Address
PeopleCount int
Departments []*Department
}
type Department struct {
Name string
Employees []*Employee
DBStatus string
}
type Employee struct {
Number int
Address *Address
}
type Address struct {
City string
Detail AddressDetail
}
type AddressDetail struct {
Address1 string
Address2 string
}
func addressHTML(v Address, formKeyPrefix string) string {
return fmt.Sprintf(``,
formKeyPrefix, v.City,
formKeyPrefix, v.Detail.Address1,
formKeyPrefix, v.Detail.Address2,
)
}
func TestFieldsBuilder(t *testing.T) {
defaults := NewFieldDefaults(WRITE)
addressFb := NewFieldsBuilder().Model(&Address{}).Defaults(defaults).Only("City", "Detail")
addressDetailFb := NewFieldsBuilder().Model(&AddressDetail{}).Defaults(defaults).Only("Address1", "Address2")
addressFb.Field("Detail").Nested(addressDetailFb)
employeeFbs := NewFieldsBuilder().Model(&Employee{}).Defaults(defaults)
employeeFbs.Field("Number").ComponentFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
return h.Input(field.FormKey).Type("text").Value(field.StringValue(obj))
})
employeeFbs.Field("Address").Nested(addressFb)
employeeFbs.Field("FakeNumber").ComponentFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
return h.Input(field.FormKey).Type("text").Value(fmt.Sprintf("900%v", reflectutils.MustGet(obj, "Number")))
}).SetterFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) (err error) {
v := ctx.R.FormValue(field.FormKey)
if v == "" {
return
}
return reflectutils.Set(obj, "Number", "900"+v)
})
deptFbs := NewFieldsBuilder().Model(&Department{}).Defaults(defaults)
deptFbs.Field("Name").ComponentFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
// [0].Departments[0].Name
// [0].Departments[1].Name
// [1].Departments[0].Name
return h.Input(field.FormKey).Type("text").Value(field.StringValue(obj))
}).SetterFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) (err error) {
reflectutils.Set(obj, field.Name, ctx.R.FormValue(field.FormKey)+"!!!")
// panic("dept name setter")
return
})
deptFbs.Field("Employees").Nested(employeeFbs).ComponentFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
return h.Div(
field.NestedFieldsBuilder.ToComponentForEach(field, obj.(*Department).Employees, ctx, nil),
h.Button("Add Employee"),
).Class("employees")
})
fbs := NewFieldsBuilder().Model(&Org{}).Defaults(defaults)
fbs.Field("Name").ComponentFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
// [0].Name
return h.Input(field.Name).Type("text").Value(field.StringValue(obj))
})
// .SetterFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) (err error) {
// return
// })
fbs.Field("Departments").Nested(deptFbs).ComponentFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
// [0].Departments
return h.Div(
field.NestedFieldsBuilder.ToComponentForEach(field, obj.(*Org).Departments, ctx, nil),
h.Button("Add Department"),
).Class("departments")
})
fbs.Field("Address").Nested(addressFb)
fbs.Field("PeopleCount").SetterFunc(func(obj interface{}, field *FieldContext, ctx *web.EventContext) (err error) {
reflectutils.Set(obj, field.Name, ctx.R.FormValue(field.FormKey))
return
})
var toComponentCases = []struct {
name string
obj *Org
setup func(ctx *web.EventContext)
expectedHTML string
}{
{
name: "Only deleted",
obj: &Org{
Name: "Name 1",
Address: Address{
City: "c1",
Detail: AddressDetail{
Address1: "addr1",
Address2: "addr2",
},
},
Departments: []*Department{
{
Name: "11111",
Employees: []*Employee{
{Number: 111},
{Number: 222},
{Number: 333},
},
},
{
Name: "22222",
Employees: []*Employee{
{Number: 333},
{Number: 444},
},
},
},
},
setup: func(ctx *web.EventContext) {
ContextModifiedIndexesBuilder(ctx).
AppendDeleted("Departments[0].Employees", 1).
AppendDeleted("Departments[0].Employees", 5)
},
expectedHTML: fmt.Sprintf(`
%s
`,
addressHTML(Address{}, "Departments[0].Employees[0]."),
addressHTML(Address{}, "Departments[0].Employees[2]."),
addressHTML(Address{}, "Departments[1].Employees[0]."),
addressHTML(Address{}, "Departments[1].Employees[1]."),
addressHTML(Address{
City: "c1",
Detail: AddressDetail{
Address1: "addr1",
Address2: "addr2",
},
}, "")),
},
{
name: "Deleted with Sorted",
obj: &Org{
Name: "Name 1",
Departments: []*Department{
{
Name: "11111",
Employees: []*Employee{
{Number: 111},
{Number: 222},
{Number: 333},
{Number: 444},
{Number: 555},
},
},
{
Name: "22222",
Employees: []*Employee{
{Number: 333},
{Number: 444},
},
},
},
},
setup: func(ctx *web.EventContext) {
ContextModifiedIndexesBuilder(ctx).
AppendDeleted("Departments[0].Employees", 1).
SetSorted("Departments[0].Employees", []string{"2", "0", "3", "6"})
},
expectedHTML: fmt.Sprintf(`
%s
`,
addressHTML(Address{}, "Departments[0].Employees[2]."),
addressHTML(Address{}, "Departments[0].Employees[0]."),
addressHTML(Address{}, "Departments[0].Employees[3]."),
addressHTML(Address{}, "Departments[0].Employees[4]."),
addressHTML(Address{}, "Departments[1].Employees[0]."),
addressHTML(Address{}, "Departments[1].Employees[1]."),
addressHTML(Address{}, ""),
),
},
}
for _, c := range toComponentCases {
t.Run(c.name, func(t *testing.T) {
ctx := &web.EventContext{
R: httptest.NewRequest("POST", "/", nil),
}
c.setup(ctx)
result := fbs.ToComponent(nil, c.obj, ctx)
actual1 := h.MustString(result, context.TODO())
diff := testingutils.PrettyJsonDiff(c.expectedHTML, actual1)
if diff != "" {
t.Error(diff)
}
})
}
var unmarshalCases = []struct {
name string
initial *Org
expected *Org
req *http.Request
removeDeletedAndSort bool
}{
{
name: "case with deleted",
initial: &Org{
Departments: []*Department{
{
Name: "Department A",
Employees: []*Employee{
{
Number: 0,
},
{
Number: 1,
},
},
},
{
Name: "Department B",
Employees: []*Employee{
{Number: 0},
{Number: 1},
{Number: 2},
},
},
{
Name: "Department C",
},
},
},
req: multipartestutils.NewMultipartBuilder().
AddField("Name", "Org 1").
AddField("PeopleCount", "420").
AddField("Departments[1].Name", "Department 1").
AddField("Departments[1].Employees[0].Number", "888").
AddField("Departments[1].Employees[2].Number", "999").
AddField("Departments[1].Employees[1].FakeNumber", "666").
AddField("Departments[1].DBStatus", "Verified").
AddField("Departments[2].Name", "Department C").
AddField("__Deleted.Departments[0].Employees", "1,2").
AddField("__Deleted.Departments[1].Employees", "0").
AddField("__Deleted.Departments[2].Employees", "0").
BuildEventFuncRequest(),
removeDeletedAndSort: false,
expected: &Org{
Name: "Org 1",
PeopleCount: 420,
Departments: []*Department{
{
Name: "!!!",
Employees: []*Employee{
{
Number: 0,
},
nil,
nil,
},
},
{
Name: "Department 1!!!",
Employees: []*Employee{
nil,
{
Number: 900666,
Address: &Address{},
},
{
Number: 999,
Address: &Address{},
},
},
},
{
Name: "Department C!!!",
Employees: []*Employee{
nil,
},
},
},
},
},
{
name: "removeDeletedAndSort true",
initial: &Org{
Departments: []*Department{
{
Name: "Department A",
Employees: []*Employee{
{
Number: 0,
},
{
Number: 1,
},
},
},
{
Name: "Department B",
Employees: []*Employee{
{Number: 0},
{Number: 1},
{Number: 2},
},
},
{
Name: "Department C",
},
},
},
req: multipartestutils.NewMultipartBuilder().
AddField("Name", "Org 1").
AddField("PeopleCount", "420").
AddField("Departments[1].Name", "Department 1").
AddField("Departments[1].Employees[0].Number", "888").
AddField("Departments[1].Employees[2].Number", "999").
AddField("Departments[1].Employees[1].FakeNumber", "666").
AddField("Departments[1].DBStatus", "Verified").
AddField("__Deleted.Departments[0].Employees", "1,5").
AddField("__Deleted.Departments[1].Employees", "0").
BuildEventFuncRequest(),
removeDeletedAndSort: true,
expected: &Org{
Name: "Org 1",
PeopleCount: 420,
Departments: []*Department{
{
Name: "!!!",
Employees: []*Employee{
{
Number: 0,
},
},
},
{
Name: "Department 1!!!",
Employees: []*Employee{
{
Number: 900666,
Address: &Address{},
},
{
Number: 999,
Address: &Address{},
},
},
},
{
Name: "Department C",
},
},
},
},
{
name: "removeDeletedAndSort true and sorted",
initial: &Org{
Departments: []*Department{
{
Name: "Department A",
Employees: []*Employee{
{
Number: 0,
},
{
Number: 1,
},
{
Number: 2,
},
},
},
{
Name: "Department B",
Employees: []*Employee{
{Number: 0},
{Number: 1},
{Number: 2},
},
},
{
Name: "Department C",
},
},
},
req: multipartestutils.NewMultipartBuilder().
AddField("Name", "Org 1").
AddField("PeopleCount", "420").
AddField("Departments[1].Name", "Department 1").
AddField("Departments[1].Employees[0].Number", "000").
AddField("Departments[1].Employees[2].Number", "222").
AddField("Departments[1].Employees[3].Number", "333").
AddField("Departments[1].Employees[4].Number", "444").
AddField("Departments[1].Employees[1].FakeNumber", "666"). // this will set Number[1] to 900666
AddField("Departments[1].DBStatus", "Verified").
AddField("__Deleted.Departments[0].Employees", "1,5").
AddField("__Sorted.Departments[1].Employees", "3,4,0,2").
AddField("__Deleted.Departments[1].Employees", "0").
BuildEventFuncRequest(),
removeDeletedAndSort: true,
expected: &Org{
Name: "Org 1",
PeopleCount: 420,
Departments: []*Department{
{
Name: "!!!",
Employees: []*Employee{
{
Number: 0,
},
{
Number: 2,
},
},
},
{
Name: "Department 1!!!",
Employees: []*Employee{
{
Number: 333,
Address: &Address{},
},
{
Number: 444,
Address: &Address{},
},
{
Number: 222,
Address: &Address{},
},
{
Number: 900666,
Address: &Address{},
},
},
},
{
Name: "Department C",
},
},
},
},
{
name: "removeDeletedAndSort false and sorted",
initial: &Org{
Departments: []*Department{
{
Name: "Department A",
Employees: []*Employee{
{
Number: 0,
},
{
Number: 1,
},
{
Number: 2,
},
},
},
{
Name: "Department B",
Employees: []*Employee{
{Number: 0},
{Number: 1},
{Number: 2},
},
},
{
Name: "Department C",
},
},
},
req: multipartestutils.NewMultipartBuilder().
AddField("Name", "Org 1").
AddField("PeopleCount", "420").
AddField("Departments[1].Name", "Department 1").
AddField("Departments[1].Employees[0].Number", "000").
AddField("Departments[1].Employees[2].Number", "222").
AddField("Departments[1].Employees[3].Number", "333").
AddField("Departments[1].Employees[4].Number", "444").
AddField("Departments[1].Employees[1].FakeNumber", "666"). // this will set Number[1] to 900666
AddField("Departments[1].DBStatus", "Verified").
AddField("__Deleted.Departments[0].Employees", "1,3").
AddField("__Sorted.Departments[1].Employees", "3,4,0,2").
AddField("__Deleted.Departments[1].Employees", "0").
BuildEventFuncRequest(),
removeDeletedAndSort: false,
expected: &Org{
Name: "Org 1",
PeopleCount: 420,
Departments: []*Department{
{
Name: "!!!",
Employees: []*Employee{
{
Number: 0,
},
nil,
{
Number: 2,
},
nil,
},
},
{
Name: "Department 1!!!",
Employees: []*Employee{
nil,
{
Number: 900666,
Address: &Address{},
},
{
Number: 222,
Address: &Address{},
},
{
Number: 333,
Address: &Address{},
},
{
Number: 444,
Address: &Address{},
},
},
},
{
Name: "Department C",
},
},
},
},
{
name: "object item",
initial: &Org{
Name: "Org 1",
Address: Address{
City: "org city",
Detail: AddressDetail{
Address1: "org addr1",
Address2: "org addr2",
},
},
Departments: []*Department{
{
Name: "Department A",
Employees: []*Employee{
{
Number: 1,
Address: &Address{
City: "1 city",
Detail: AddressDetail{
Address1: "1 addr1",
Address2: "1 addr2",
},
},
},
{
Number: 2,
},
},
},
},
},
req: multipartestutils.NewMultipartBuilder().
AddField("Name", "Org 1").
AddField("Address.City", "org city e").
AddField("Address.Detail.Address1", "org addr1 e").
AddField("Address.Detail.Address2", "org addr2 e").
AddField("Departments[0].Name", "Department A").
AddField("Departments[0].Employees[0].Number", "1").
AddField("Departments[0].Employees[0].Address.City", "1 city e").
AddField("Departments[0].Employees[0].Address.Detail.Address1", "1 addr1 e").
AddField("Departments[0].Employees[0].Address.Detail.Address2", "1 addr2 e").
AddField("Departments[0].Employees[1].Number", "2").
AddField("Departments[0].Employees[1].Address.City", "2 city").
AddField("Departments[0].Employees[1].Address.Detail.Address1", "2 addr1").
AddField("Departments[0].Employees[1].Address.Detail.Address2", "2 addr2").
BuildEventFuncRequest(),
removeDeletedAndSort: false,
expected: &Org{
Name: "Org 1",
Address: Address{
City: "org city e",
Detail: AddressDetail{
Address1: "org addr1 e",
Address2: "org addr2 e",
},
},
Departments: []*Department{
{
Name: "Department A!!!",
Employees: []*Employee{
{
Number: 1,
Address: &Address{
City: "1 city e",
Detail: AddressDetail{
Address1: "1 addr1 e",
Address2: "1 addr2 e",
},
},
},
{
Number: 2,
Address: &Address{
City: "2 city",
Detail: AddressDetail{
Address1: "2 addr1",
Address2: "2 addr2",
},
},
},
},
},
},
},
},
}
for _, c := range unmarshalCases {
t.Run(c.name, func(t *testing.T) {
ctx2 := &web.EventContext{R: c.req}
_ = ctx2.R.ParseMultipartForm(128 << 20)
actual2 := c.initial
vErr := fbs.Unmarshal(actual2, nil, c.removeDeletedAndSort, ctx2)
if vErr.HaveErrors() {
t.Error(vErr.Error())
}
diff := testingutils.PrettyJsonDiff(c.expected, actual2)
if diff != "" {
t.Error(diff)
}
})
}
}
func TestGoFunk(t *testing.T) {
depts := []*Department{
{Name: "1"},
{Name: "2"},
}
funk.ForEach(depts, func(obj *Department) {
obj.Name = "3"
})
funk.ForEach(depts, func(obj interface{}) {
obj.(*Department).Name = "3"
})
}