@@ -2,15 +2,19 @@ package dashboard
2
2
3
3
import (
4
4
"context"
5
+ "fmt"
5
6
"net/url"
6
7
"os"
8
+ "path/filepath"
7
9
"time"
8
10
9
11
"github.com/chromedp/cdproto/cdp"
10
12
"github.com/chromedp/cdproto/network"
11
13
"github.com/chromedp/chromedp"
12
14
"golang.org/x/xerrors"
13
15
16
+ "github.com/coder/coder/v2/cryptorand"
17
+
14
18
"cdr.dev/slog"
15
19
)
16
20
@@ -86,17 +90,17 @@ var defaultTargets = []Target{
86
90
},
87
91
}
88
92
89
- // ClickRandomElement returns an action that will click an element from defaultTargets.
93
+ // clickRandomElement returns an action that will click an element from defaultTargets.
90
94
// If no elements are found, an error is returned.
91
95
// If more than one element is found, one is chosen at random.
92
96
// The label of the clicked element is returned.
93
- func ClickRandomElement (ctx context.Context , randIntn func (int ) int ) (Label , Action , error ) {
97
+ func clickRandomElement (ctx context.Context , log slog. Logger , randIntn func (int ) int , deadline time. Time ) (Label , Action , error ) {
94
98
var xpath Selector
95
99
var found bool
96
100
var err error
97
101
matches := make ([]Target , 0 )
98
102
for _ , tgt := range defaultTargets {
99
- xpath , found , err = randMatch (ctx , tgt .ClickOn , randIntn )
103
+ xpath , found , err = randMatch (ctx , log , tgt .ClickOn , randIntn , deadline )
100
104
if err != nil {
101
105
return "" , nil , xerrors .Errorf ("find matches for %q: %w" , tgt .ClickOn , err )
102
106
}
@@ -111,14 +115,20 @@ func ClickRandomElement(ctx context.Context, randIntn func(int) int) (Label, Act
111
115
}
112
116
113
117
if len (matches ) == 0 {
118
+ log .Debug (ctx , "no matches found this time" )
114
119
return "" , nil , xerrors .Errorf ("no matches found" )
115
120
}
116
121
match := pick (matches , randIntn )
117
- // rely on map iteration order being random
118
122
act := func (actx context.Context ) error {
119
- if err := clickAndWait (actx , match .ClickOn , match .WaitFor ); err != nil {
123
+ log .Debug (ctx , "clicking" , slog .F ("label" , match .Label ), slog .F ("xpath" , match .ClickOn ))
124
+ if err := runWithDeadline (ctx , deadline , chromedp .Click (match .ClickOn , chromedp .NodeReady )); err != nil {
125
+ log .Error (ctx , "click failed" , slog .F ("label" , match .Label ), slog .F ("xpath" , match .ClickOn ), slog .Error (err ))
120
126
return xerrors .Errorf ("click %q: %w" , match .ClickOn , err )
121
127
}
128
+ if err := runWithDeadline (ctx , deadline , chromedp .WaitReady (match .WaitFor )); err != nil {
129
+ log .Error (ctx , "wait failed" , slog .F ("label" , match .Label ), slog .F ("xpath" , match .WaitFor ), slog .Error (err ))
130
+ return xerrors .Errorf ("wait for %q: %w" , match .WaitFor , err )
131
+ }
122
132
return nil
123
133
}
124
134
return match .Label , act , nil
@@ -128,26 +138,32 @@ func ClickRandomElement(ctx context.Context, randIntn func(int) int) (Label, Act
128
138
// The returned selector is the full XPath of the matched node.
129
139
// If no matches are found, an error is returned.
130
140
// If multiple matches are found, one is chosen at random.
131
- func randMatch (ctx context.Context , s Selector , randIntn func (int ) int ) (Selector , bool , error ) {
141
+ func randMatch (ctx context.Context , log slog. Logger , s Selector , randIntn func (int ) int , deadline time. Time ) (Selector , bool , error ) {
132
142
var nodes []* cdp.Node
133
- err := chromedp .Run (ctx , chromedp .Nodes (s , & nodes , chromedp .NodeVisible , chromedp .AtLeast (0 )))
134
- if err != nil {
143
+ log .Debug (ctx , "getting nodes for selector" , slog .F ("selector" , s ))
144
+ if err := runWithDeadline (ctx , deadline , chromedp .Nodes (s , & nodes , chromedp .NodeReady , chromedp .AtLeast (0 ))); err != nil {
145
+ log .Debug (ctx , "failed to get nodes for selector" , slog .F ("selector" , s ), slog .Error (err ))
135
146
return "" , false , xerrors .Errorf ("get nodes for selector %q: %w" , s , err )
136
147
}
137
148
if len (nodes ) == 0 {
149
+ log .Debug (ctx , "no nodes found for selector" , slog .F ("selector" , s ))
138
150
return "" , false , nil
139
151
}
140
152
n := pick (nodes , randIntn )
153
+ log .Debug (ctx , "found node" , slog .F ("node" , n .FullXPath ()))
141
154
return Selector (n .FullXPath ()), true , nil
142
155
}
143
156
144
- // clickAndWait clicks the given selector and waits for the page to finish loading.
145
- // The page is considered loaded when the network event "LoadingFinished" is received.
146
- func clickAndWait (ctx context.Context , clickOn , waitFor Selector ) error {
147
- return chromedp .Run (ctx , chromedp.Tasks {
148
- chromedp .Click (clickOn , chromedp .NodeVisible ),
149
- chromedp .WaitVisible (waitFor , chromedp .NodeVisible ),
150
- })
157
+ func waitForWorkspacesPageLoaded (ctx context.Context , deadline time.Time ) error {
158
+ return runWithDeadline (ctx , deadline , chromedp .WaitReady (`tbody.MuiTableBody-root` ))
159
+ }
160
+
161
+ func runWithDeadline (ctx context.Context , deadline time.Time , acts ... chromedp.Action ) error {
162
+ deadlineCtx , deadlineCancel := context .WithDeadline (ctx , deadline )
163
+ defer deadlineCancel ()
164
+ c := chromedp .FromContext (ctx )
165
+ tasks := chromedp .Tasks (acts )
166
+ return tasks .Do (cdp .WithExecutor (deadlineCtx , c .Target ))
151
167
}
152
168
153
169
// initChromeDPCtx initializes a chromedp context with the given session token cookie
@@ -178,6 +194,13 @@ func initChromeDPCtx(ctx context.Context, log slog.Logger, u *url.URL, sessionTo
178
194
}
179
195
}
180
196
197
+ // force a viewport size of 1024x768 so we don't go into mobile mode
198
+ if err := chromedp .Run (cdpCtx , chromedp .EmulateViewport (1024 , 768 )); err != nil {
199
+ cancelFunc ()
200
+ allocCtxCancel ()
201
+ return nil , nil , xerrors .Errorf ("set viewport size: %w" , err )
202
+ }
203
+
181
204
// set cookies
182
205
if err := setSessionTokenCookie (cdpCtx , sessionToken , u .Host ); err != nil {
183
206
cancelFunc ()
@@ -209,6 +232,34 @@ func visitMainPage(ctx context.Context, u *url.URL) error {
209
232
return chromedp .Run (ctx , chromedp .Navigate (u .String ()))
210
233
}
211
234
235
+ func Screenshot (ctx context.Context , name string ) (string , error ) {
236
+ var buf []byte
237
+ if err := chromedp .Run (ctx , chromedp .CaptureScreenshot (& buf )); err != nil {
238
+ return "" , xerrors .Errorf ("capture screenshot: %w" , err )
239
+ }
240
+ randExt , err := cryptorand .String (4 )
241
+ if err != nil {
242
+ // this should never happen
243
+ return "" , xerrors .Errorf ("generate random string: %w" , err )
244
+ }
245
+ fname := fmt .Sprintf ("scaletest-dashboard-%s-%s-%s.png" , name , time .Now ().Format ("20060102-150405" ), randExt )
246
+ pwd , err := os .Getwd ()
247
+ if err != nil {
248
+ return "" , xerrors .Errorf ("get working directory: %w" , err )
249
+ }
250
+ fpath := filepath .Join (pwd , fname )
251
+ f , err := os .OpenFile (fpath , os .O_CREATE | os .O_WRONLY , 0o644 )
252
+ if err != nil {
253
+ return "" , xerrors .Errorf ("open file: %w" , err )
254
+ }
255
+ defer f .Close ()
256
+ if _ , err := f .Write (buf ); err != nil {
257
+ return "" , xerrors .Errorf ("write file: %w" , err )
258
+ }
259
+
260
+ return fpath , nil
261
+ }
262
+
212
263
// pick chooses a random element from a slice.
213
264
// If the slice is empty, it returns the zero value of the type.
214
265
func pick [T any ](s []T , randIntn func (int ) int ) T {
0 commit comments