github.com/grahambrereton-form3/tilt@v0.10.18/internal/hud/webview/view_test.go (about) 1 package webview 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "io/ioutil" 7 "path/filepath" 8 "reflect" 9 "strings" 10 "testing" 11 "unicode" 12 "unicode/utf8" 13 14 "github.com/stretchr/testify/require" 15 16 "github.com/stretchr/testify/assert" 17 ) 18 19 // The view data model is not allowed to have any private properties, 20 // because these properties need to be serialized to JSON for the web UI. 21 func TestMarshalView(t *testing.T) { 22 assertCanMarshal(t, reflect.TypeOf(View{}), reflect.TypeOf(View{})) 23 } 24 25 func TestJSONRoundTrip(t *testing.T) { 26 // this file can be generated via 27 // curl localhost:10350/api/snapshot/aaaa | jq '.View' > view.json 28 testdataPath := filepath.Join("testdata", "view.json") 29 contents, err := ioutil.ReadFile(testdataPath) 30 assert.NoError(t, err) 31 32 // why is this 1.5 round trips instead of 1? 33 // go produces output where && is changed to \u0026. I couldn't find a convenient mechanism to generate 34 // view.json that matched this. 35 36 // deserialize into a map[string]interface so that it'll have everything and observed will, also 37 decoder := json.NewDecoder(bytes.NewBuffer(contents)) 38 decoder.DisallowUnknownFields() 39 expected := make(map[string]interface{}) 40 err = decoder.Decode(&expected) 41 require.NoError(t, err) 42 43 // round-trip through an instance of `View` 44 decoder = json.NewDecoder(bytes.NewBuffer(contents)) 45 decoder.DisallowUnknownFields() 46 var view View 47 err = decoder.Decode(&view) 48 require.NoError(t, err) 49 b := bytes.NewBuffer(nil) 50 encoder := json.NewEncoder(b) 51 err = encoder.Encode(view) 52 require.NoError(t, err) 53 54 // now put it back into a `map[string]interface` so that we can compare with `expected` 55 decoder = json.NewDecoder(b) 56 observed := make(map[string]interface{}) 57 err = decoder.Decode(&observed) 58 require.NoError(t, err) 59 60 require.Equal(t, expected, observed) 61 } 62 63 // v: the type to check. 64 // owner: the owner of this field, for display purposes. 65 func assertCanMarshal(t *testing.T, v reflect.Type, owner reflect.Type) { 66 // If this type does its own marshaling 67 var marshal *json.Marshaler 68 if v.Implements(reflect.TypeOf(marshal).Elem()) { 69 return 70 } 71 72 kind := v.Kind() 73 switch kind { 74 case reflect.Array, reflect.Slice, reflect.Ptr: 75 assertCanMarshal(t, v.Elem(), owner) 76 case reflect.Map: 77 assertCanMarshal(t, v.Elem(), owner) 78 assertCanMarshal(t, v.Key(), owner) 79 case reflect.Interface: 80 t.Errorf("View needs to be serializable. This type in the view don't make sense: %s in %s", v, owner) 81 82 case reflect.Chan, reflect.Func: 83 t.Errorf("View needs to be serializable. This type in the view don't make sense: %s in %s", v, owner) 84 case reflect.Struct: 85 for i := 0; i < v.NumField(); i++ { 86 field := v.Field(i) 87 if !isExported(field.Name) { 88 t.Errorf("All fields in the WebView need to be serializable to web. Unexported fields are forbidden: %s in %s", 89 field.Name, v) 90 } 91 tag := field.Tag.Get("json") 92 jsonName := strings.SplitN(tag, ",", 2)[0] 93 if !isValidJSONField(jsonName) { 94 t.Errorf("All fields in the WebView need to be serializable to valid lower-case JSON fields. Field name: %s. Json tag: %s", 95 field.Name, jsonName) 96 } 97 98 assertCanMarshal(t, field.Type, v) 99 } 100 } 101 } 102 103 func isExported(id string) bool { 104 r, _ := utf8.DecodeRuneInString(id) 105 return unicode.IsUpper(r) 106 } 107 108 func isValidJSONField(field string) bool { 109 if strings.Contains(field, "_") { 110 return false 111 } 112 113 r, _ := utf8.DecodeRuneInString(field) 114 return unicode.IsLower(r) 115 }