-
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathjavascript.go
More file actions
229 lines (175 loc) · 7.4 KB
/
Copy pathjavascript.go
File metadata and controls
229 lines (175 loc) · 7.4 KB
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
package biloba
import (
"encoding/json"
"fmt"
"strings"
"sync/atomic"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/onsi/gomega/gcustom"
"github.com/onsi/gomega/types"
)
/*
EvaluateTo is a matcher that asserts that the result of running the script passed to Gomega matches expected:
Eventually("app.users.map(user => user.name)").Should(tab.EvaluateTo(ConsistOf("George", "Sally", "Bob")))
EvaluateTo can be passed a Gomega matcher to assert against the returned value from the script. Or it can be passed an arbitrary value in which case Equal() is used.
Read https://onsi.github.io/biloba/#running-arbitrary-javascript to learn more about running JavaScript in Biloba
*/
func (b *Biloba) EvaluateTo(expected any) types.GomegaMatcher {
var data = map[string]any{}
var matcher = matcherOrEqual(expected)
data["Matcher"] = matcher
return gcustom.MakeMatcher(func(script string) (bool, error) {
r, err := b.RunErr(script)
if err != nil {
return false, fmt.Errorf("Failed to run script:\n%s\n\n%w", script, err)
}
data["Result"] = r
return matcher.Match(data["Result"])
}).WithTemplate("Return value for script:\n{{.Actual}}\nFailed with:\n{{if .Failure}}{{.Data.Matcher.FailureMessage .Data.Result}}{{else}}{{.Data.Matcher.NegatedFailureMessage .Data.Result}}{{end}}", data)
}
/*
RunErr() runs the passed in script and returns the result as well as an error
You should generally use [Biloba.Run] instead of RunErr and let Biloba handle errors for you
Read https://onsi.github.io/biloba/#running-arbitrary-javascript to learn more about running JavaScript in Biloba
*/
func (b *Biloba) RunErr(script string, args ...any) (any, error) {
return b.runErr(script, false, args...)
}
func (b *Biloba) runErr(script string, awaitPromise bool, args ...any) (any, error) {
b.blockIfNecessaryToEnsureSuccessfulDownloads()
var encodedResult []byte
options := func(p *runtime.EvaluateParams) *runtime.EvaluateParams {
p = p.WithUserGesture(true)
if awaitPromise {
p = p.WithAwaitPromise(true)
}
return p
}
err := chromedp.Run(b.Context, chromedp.EvaluateAsDevTools(script, &encodedResult, options))
if err != nil {
if strings.Contains(err.Error(), "_biloba is not defined") {
b.reloadBiloba()
return b.runErr(script, awaitPromise, args...)
}
return nil, err
}
if len(args) == 0 {
var result any
json.Unmarshal(encodedResult, &result)
return result, nil
}
err = json.Unmarshal(encodedResult, args[0])
return args[0], err
}
/*
RunErrAsync() runs the passed in script as the body of an async function and awaits the result, returning the result as well as an error
Use await freely and return the value you want out of the script:
b.RunErrAsync(`return await app.load()`)
You should generally use [Biloba.RunAsync] instead of RunErrAsync and let Biloba handle errors for you
Read https://onsi.github.io/biloba/#running-arbitrary-javascript to learn more about running JavaScript in Biloba
*/
func (b *Biloba) RunErrAsync(script string, args ...any) (any, error) {
return b.runErr("(async () => {"+script+"\n})()", true, args...)
}
/*
Run() runs the passed in script and returns the result (as type any):
tab.Run("1+3") // returns 4.0
You can also pass a single pointer argument if you would like Biloba to decode the result into a specific type (a la json.Unmarshal):
var result int
tab.Run("1+3", &result) // result is now 4
# If an error occurs Run() will fail the spec
Read https://onsi.github.io/biloba/#running-arbitrary-javascript to learn more about running JavaScript in Biloba
*/
func (b *Biloba) Run(script string, args ...any) any {
b.gt.Helper()
res, err := b.RunErr(script, args...)
if err != nil {
b.gt.Fatalf("Failed to run script:\n%s\n\n%s", script, err.Error())
}
return res
}
/*
RunAsync() runs the passed in script as the body of an async function, awaits the result, and returns it.
Unlike [Biloba.Run] (which evaluates a synchronous expression) RunAsync lets you use await and return the value you care about:
users := b.RunAsync(`
const response = await fetch("/api/users")
return await response.json()
`)
As with Run you can pass a single pointer argument to decode the result into a specific type:
var users []User
b.RunAsync(`return await app.load()`, &users)
# If the script throws or the awaited promise rejects RunAsync will fail the spec
Read https://onsi.github.io/biloba/#running-arbitrary-javascript to learn more about running JavaScript in Biloba
*/
func (b *Biloba) RunAsync(script string, args ...any) any {
b.gt.Helper()
res, err := b.RunErrAsync(script, args...)
if err != nil {
b.gt.Fatalf("Failed to run async script:\n%s\n\n%s", script, err.Error())
}
return res
}
type JSFunc string
/*
JSFunc() allows you to write Javascript functions that are invoked with Go arguments and then passed in to Run:
adder := b.JSFunc("(...nums) => nums.reduce((s, n) => s + n, 0)")
var result int
b.Run(adder.Invoke(1, 2, 3, 4, 5, 10), &result)
Read https://onsi.github.io/biloba/#running-arbitrary-javascript to learn more about running JavaScript in Biloba
*/
func (b *Biloba) JSFunc(f string) JSFunc {
return JSFunc("(" + f + ")")
}
/*
Invoke() interpolates the passed-in args into the JSFunc and generates a script that cna be passed to Run()
adder := b.JSFunc("(...nums) => nums.reduce((s, n) => s + n, 0)")
var result int
tab.Run(adder.Invoke(1, 2, 3, 4, 5, 10), &result)
Read https://onsi.github.io/biloba/#running-arbitrary-javascript to learn more about running JavaScript in Biloba
*/
func (j JSFunc) Invoke(args ...any) string {
if len(args) == 0 {
return string(j) + "()"
}
encodedArgsBytes, err := json.Marshal(args)
if err != nil {
panic(err)
}
encodedArgs := string(encodedArgsBytes)
for _, arg := range args {
if v, ok := arg.(JSVar); ok {
encodedArgs = v.interpolate(encodedArgs)
}
}
return string(j) + "(..." + string(encodedArgs) + ")"
}
/*
JSVar() allows you to indicate that the wrapped variable should be not be interpolated by Invoke(), instead it should be provided by the JavaScript environment and not Go.
This is perhaps best understood with an example:
adder := tab.JSFunc("(...nums) => nums.reduce((s, n) => s + n, 0)")
tab.Run(adder.Invoke(15, 10, tab.JSVar("app.numRecords"), tab.JSVar("app.numUsers + 10")))
This call to invoke will generate the following literal script:
((...nums) => nums.reduce((s, n) => s + n, 0))(...[15, 10, app.numRecords, app.numUsers + 10])
which, when eval()'d in JavaScript will pull in the app variable from the global window object and grab the numRecords and numUsers properties.
If were to not wrap these variables in JSVar:
tab.Run(adder.Invoke(15, 10, "app.numRecords", "app.numUsers + 10")) //wrong!
then the generated script would be:
//not what you want!
((...nums) => nums.reduce((s, n) => s + n, 0))(...[15, 10, "app.numRecords", "app.numUsers + 10"])
which would (of course) evaluate to "25app.numRecordsapp.numUsers + 10". (of course).
Read https://onsi.github.io/biloba/#running-arbitrary-javascript to learn more about running JavaScript in Biloba
*/
func (b *Biloba) JSVar(v string) JSVar {
return JSVar{
v: v,
identifier: fmt.Sprintf(`"__biloba_var_%d"`, atomic.AddInt64(&jsVarCounter, 1)),
}
}
var jsVarCounter int64
type JSVar struct {
v string
identifier string
}
func (j JSVar) MarshalJSON() ([]byte, error) { return []byte(j.identifier), nil }
func (j JSVar) interpolate(json string) string { return strings.Replace(json, j.identifier, j.v, 1) }