-
-
Notifications
You must be signed in to change notification settings - Fork 144
/
testsuite_test.go
305 lines (265 loc) · 8.3 KB
/
testsuite_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
package vecty
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strings"
"testing"
)
var _ = func() bool {
isTest = true
return true
}()
// recoverStr runs f and returns the recovered panic as a string.
func recoverStr(f func()) (s string) {
defer func() {
s = fmt.Sprint(recover())
}()
f()
return
}
type componentFunc struct {
Core
id string
render func() ComponentOrHTML
skipRender func(prev Component) bool
}
func (c *componentFunc) Render() ComponentOrHTML { return c.render() }
func (c *componentFunc) SkipRender(prev Component) bool { return c.skipRender(prev) }
func TestMain(m *testing.M) {
// Try to remove all testdata/*.got.txt files now.
matches, _ := filepath.Glob("testdata/*.got.txt")
for _, match := range matches {
os.Remove(match)
}
os.Exit(m.Run())
}
func testSuite(t *testing.T) *testSuiteT {
ts := &testSuiteT{
t: t,
callbacks: make(map[string]interface{}),
strings: &valueMocker{},
bools: &valueMocker{},
floats: &valueMocker{},
ints: &valueMocker{},
truthies: &valueMocker{},
isUndefined: &valueMocker{},
}
globalValue = &objectRecorder{
ts: ts,
name: "global",
}
return ts
}
// mockedValue represents a mocked value.
type mockedValue struct {
invocation string
value interface{}
}
// valueMocker keeps tracked of mocked values for method invocations on
// jsObject's.
type valueMocker struct {
values []mockedValue
}
// mock adds an entry to mock the specified invocation to return the given
// value.
func (v *valueMocker) mock(invocation string, value interface{}) {
v.values = append(v.values, mockedValue{invocation, value})
}
// get gets the mocked value for the specified invocation.
func (v *valueMocker) get(invocation string) interface{} {
for i, value := range v.values {
if value.invocation == invocation {
// Found the right invocation.
v.values = append(v.values[:i], v.values[i+1:]...)
return value.value
}
}
panic(fmt.Sprintf("expected mocked value for invocation: %s", invocation))
}
type testSuiteT struct {
t *testing.T
callbacks map[string]interface{}
strings, bools, floats, ints, truthies, isUndefined *valueMocker
got string
isDone bool
}
func (ts *testSuiteT) done() {
ts.t.Helper()
ts.multiSortedDone()
}
// sortedDone is just like done(), except it sorts the specified line range first.
func (ts *testSuiteT) sortedDone(sortStartLine, sortEndLine int) {
ts.t.Helper()
ts.multiSortedDone([2]int{sortStartLine, sortEndLine})
}
// multiSortedDone is just like done(), except it sorts the specified line range first.
func (ts *testSuiteT) multiSortedDone(linesToSort ...[2]int) {
ts.t.Helper()
if ts.isDone {
panic("testSuite done methods called multiple times")
}
ts.isDone = true
// Read the want file or create it if it does not exist.
testName := strings.Replace(ts.t.Name(), "/", "__", -1)
wantFileName := path.Join("testdata", testName+".want.txt")
wantBytes, err := ioutil.ReadFile(wantFileName)
if err != nil {
if os.IsNotExist(err) {
// Touch the file
f, err := os.Create(wantFileName)
f.Close()
if err != nil {
ts.t.Error(err)
return
}
} else {
ts.t.Error(err)
return
}
}
want := strings.TrimSpace(string(wantBytes))
// Ensure output is properly sorted.
split := strings.Split(strings.TrimSpace(ts.got), "\n")
for _, pair := range linesToSort {
sortStartLine := pair[0] - 1 // to match editor line numbers
if sortStartLine < 0 {
sortStartLine = 0
}
sortEndLine := pair[1]
if sortEndLine > len(split) {
sortEndLine = len(split)
}
sorted := split[sortStartLine:sortEndLine]
ts.t.Logf("lines selected for sorting (%d-%d):\n%s\n\n", sortStartLine, sortEndLine, strings.Join(sorted, "\n"))
sort.Strings(sorted)
for i := sortStartLine; i < sortEndLine; i++ {
split[i] = sorted[i-sortStartLine]
}
}
got := strings.Join(split, "\n")
// Check if we got what we wanted.
if got == want {
// Successful test.
// Ensure there are no unused mocked values.
for _, v := range ts.strings.values {
ts.t.Errorf("unused mocked string value %q %v", v.invocation, v.value)
}
for _, v := range ts.bools.values {
ts.t.Errorf("unused mocked bool value %q %v", v.invocation, v.value)
}
for _, v := range ts.floats.values {
ts.t.Errorf("unused mocked float value %q %v", v.invocation, v.value)
}
for _, v := range ts.ints.values {
ts.t.Errorf("unused mocked int value %q %v", v.invocation, v.value)
}
return
}
// Write what we got to disk.
gotFileName := path.Join("testdata", testName+".got.txt")
err = ioutil.WriteFile(gotFileName, []byte(got), 0o777)
if err != nil {
ts.t.Error(err)
return
}
// Print a nice diff for easy comparison.
out, err := commandOutput("git", "-c", "color.ui=always", "diff", "--no-index", wantFileName, gotFileName)
if err != nil {
ts.t.Log("running git diff", err)
} else {
ts.t.Log("\n" + out)
}
ts.t.Errorf("to accept these changes:\n\n$ mv %s %s", gotFileName, wantFileName)
}
// record records the invocation to the test suite and returns the string
// unmodified.
func (ts *testSuiteT) record(invocation string) string {
ts.got += "\n" + invocation
return invocation
}
// addCallbacks adds the first function in args to ts.callbacks[invocation], if there is one.
func (ts *testSuiteT) addCallbacks(invocation string, args ...interface{}) {
for _, a := range args {
if fi, ok := a.(*jsFuncImpl); ok {
ts.callbacks[invocation] = fi.goFunc
return
}
}
}
func (ts *testSuiteT) invokeCallbackRequestAnimationFrame(v float64) {
cb := ts.callbacks[`global.Call("requestAnimationFrame", func)`].(func(this jsObject, args []jsObject) interface{})
cb(undefined(), []jsObject{valueOf(v)})
}
func (ts *testSuiteT) invokeCallbackDOMContentLoaded() {
cb := ts.callbacks[`global.Get("document").Call("addEventListener", "DOMContentLoaded", func)`].(func(this jsObject, args []jsObject) interface{})
cb(undefined(), nil)
}
// objectRecorder implements the jsObject interface by recording method
// invocations to the test suite.
type objectRecorder struct {
ts *testSuiteT
name string
}
// Set implements the jsObject interface.
func (r *objectRecorder) Set(key string, value interface{}) {
invocation := r.ts.record(fmt.Sprintf("%s.Set(%q, %+v)", r.name, key, stringify(value)))
r.ts.addCallbacks(invocation, value)
}
// Get implements the jsObject interface.
func (r *objectRecorder) Get(key string) jsObject {
invocation := r.ts.record(fmt.Sprintf("%s.Get(%q)", r.name, key))
return &objectRecorder{
ts: r.ts,
name: invocation,
}
}
// Delete implements the jsObject interface.
func (r *objectRecorder) Delete(key string) {
r.ts.record(fmt.Sprintf("%s.Delete(%q)", r.name, key))
}
// Call implements the jsObject interface.
func (r *objectRecorder) Call(name string, args ...interface{}) jsObject {
invocation := r.ts.record(fmt.Sprintf("%s.Call(%q, %s)", r.name, name, stringify(args...)))
r.ts.addCallbacks(invocation, args...)
return &objectRecorder{
ts: r.ts,
name: invocation,
}
}
// String implements the jsObject interface.
func (r *objectRecorder) String() string { return r.ts.strings.get(r.name).(string) }
// Truthy implements the jsObject interface.
func (r *objectRecorder) Truthy() bool { return r.ts.truthies.get(r.name).(bool) }
// IsUndefined implements the jsObject interface.
func (r *objectRecorder) IsUndefined() bool { return r.ts.isUndefined.get(r.name).(bool) }
// Equal implements the jsObject interface.
func (r *objectRecorder) Equal(other jsObject) bool {
if (r == nil) != (other == nil) {
return false
}
return r == other.(*objectRecorder)
}
// Bool implements the jsObject interface.
func (r *objectRecorder) Bool() bool { return r.ts.bools.get(r.name).(bool) }
// Int implements the jsObject interface.
func (r *objectRecorder) Int() int { return r.ts.ints.get(r.name).(int) }
// Float implements the jsObject interface.
func (r *objectRecorder) Float() float64 { return r.ts.floats.get(r.name).(float64) }
func stringify(args ...interface{}) string {
var s []string
for _, a := range args {
switch v := a.(type) {
case string:
s = append(s, fmt.Sprintf("%q", v))
case *objectRecorder:
s = append(s, fmt.Sprintf("jsObject(%s)", v.name))
default:
s = append(s, fmt.Sprintf("%v", v))
}
}
return strings.Join(s, ", ")
}