@@ -2,6 +2,7 @@ package dashboard
2
2
3
3
import (
4
4
"context"
5
+ "math/rand"
5
6
"net/url"
6
7
"os"
7
8
"time"
@@ -18,56 +19,106 @@ type Action func(ctx context.Context) error
18
19
// Selector locates an element on a page.
19
20
type Selector string
20
21
22
+ // Target is a thing that can be clicked.
23
+ type Target struct {
24
+ // Label is a human-readable label for the target.
25
+ Label Label
26
+ // ClickOn is the selector that locates the element to be clicked.
27
+ ClickOn Selector
28
+ // WaitFor is a selector that is expected to appear after the target is clicked.
29
+ WaitFor Selector
30
+ }
31
+
21
32
// Label identifies an action.
22
33
type Label string
23
34
24
- // defaultSelectors is a map of labels to selectors.
25
- var defaultSelectors = map [Label ]Selector {
26
- "workspaces_list" : `nav a[href="/workspaces"]:not(.active)` ,
27
- "templates_list" : `nav a[href="/templates"]:not(.active)` ,
28
- "users_list" : `nav a[href^="/users"]:not(.active)` ,
29
- "deployment_status" : `nav a[href="/deployment/general"]:not(.active)` ,
30
- "starter_templates" : `a[href="/starter-templates"]` ,
31
- "workspaces_table_row" : `tr[role="button"][data-testid^="workspace-"]` ,
32
- "workspace_builds_table_row" : `tr[role="button"][data-testid^="build-"]` ,
33
- "templates_table_row" : `tr[role="button"][data-testid^="template-"]` ,
34
- "template_docs" : `a[href^="/templates/"][href$="/docs"]:not([aria-current])` ,
35
- "template_files" : `a[href^="/templates/"][href$="/files"]:not([aria-current])` ,
36
- "template_versions" : `a[href^="/templates/"][href$="/versions"]:not([aria-current])` ,
37
- "template_embed" : `a[href^="/templates/"][href$="/embed"]:not([aria-current])` ,
38
- "template_insights" : `a[href^="/templates/"][href$="/insights"]:not([aria-current])` ,
35
+ var defaultTargets = []Target {
36
+ {
37
+ Label : "workspace_list" ,
38
+ ClickOn : `nav a[href="/workspaces"]:not(.active)` ,
39
+ WaitFor : `tr[role="button"][data-testid^="workspace-"]` ,
40
+ },
41
+ {
42
+ Label : "starter_templates" ,
43
+ ClickOn : `a[href="/starter-templates"]` ,
44
+ WaitFor : `a[href^="/starter-templates/"]` ,
45
+ },
46
+ {
47
+ Label : "workspace_details" ,
48
+ ClickOn : `tr[role="button"][data-testid^="workspace-"]` ,
49
+ WaitFor : `tr[role="button"][data-testid^="build-"]` ,
50
+ },
51
+ {
52
+ Label : "workspace_build_details" ,
53
+ ClickOn : `tr[role="button"][data-testid^="build-"]` ,
54
+ WaitFor : `*[aria-label="Build details"]` ,
55
+ },
56
+ {
57
+ Label : "template_list" ,
58
+ ClickOn : `nav a[href="/templates"]:not(.active)` ,
59
+ WaitFor : `tr[role="button"][data-testid^="template-"]` ,
60
+ },
61
+ {
62
+ Label : "template_docs" ,
63
+ ClickOn : `a[href^="/templates/"][href$="/docs"]:not([aria-current])` ,
64
+ WaitFor : `#readme` ,
65
+ },
66
+ {
67
+ Label : "template_files" ,
68
+ ClickOn : `a[href^="/templates/"][href$="/docs"]:not([aria-current])` ,
69
+ WaitFor : `.monaco-editor` ,
70
+ },
71
+ {
72
+ Label : "template_versions" ,
73
+ ClickOn : `a[href^="/templates/"][href$="/versions"]:not([aria-current])` ,
74
+ WaitFor : `tr[role="button"][data-testid^="version-"]` ,
75
+ },
76
+ {
77
+ Label : "template_version_details" ,
78
+ ClickOn : `tr[role="button"][data-testid^="version-"]` ,
79
+ WaitFor : `.monaco-editor` ,
80
+ },
81
+ {
82
+ Label : "user_list" ,
83
+ ClickOn : `nav a[href^="/users"]:not(.active)` ,
84
+ WaitFor : `tr[data-testid^="user-"]` ,
85
+ },
39
86
}
40
87
41
- // ClickRandomElement returns an action that will click an element from the given selectors at random .
88
+ // ClickRandomElement returns an action that will click an element from defaultTargets .
42
89
// If no elements are found, an error is returned.
43
90
// If more than one element is found, one is chosen at random.
44
91
// The label of the clicked element is returned.
45
92
func ClickRandomElement (ctx context.Context ) (Label , Action , error ) {
46
- var matched Selector
47
- var matchedLabel Label
93
+ var xpath Selector
48
94
var found bool
49
95
var err error
50
- for l , s := range defaultSelectors {
51
- matched , found , err = randMatch (ctx , s )
96
+ matches := make (map [Label ]Selector )
97
+ waitFor := make (map [Label ]Selector )
98
+ for _ , tgt := range defaultTargets {
99
+ xpath , found , err = randMatch (ctx , tgt .ClickOn )
52
100
if err != nil {
53
- return "" , nil , xerrors .Errorf ("find matches for %q: %w" , s , err )
101
+ return "" , nil , xerrors .Errorf ("find matches for %q: %w" , tgt . ClickOn , err )
54
102
}
55
103
if ! found {
56
104
continue
57
105
}
58
- matchedLabel = l
59
- break
60
- }
61
- if ! found {
62
- return "" , nil , xerrors .Errorf ("no matches found" )
106
+ matches [tgt .Label ] = xpath
107
+ waitFor [tgt .Label ] = tgt .WaitFor
63
108
}
64
109
65
- return "click_" + matchedLabel , func (ctx context.Context ) error {
66
- if err := clickAndWait (ctx , matched ); err != nil {
67
- return xerrors .Errorf ("click %q: %w" , matched , err )
110
+ // rely on map iteration order being random
111
+ for lbl , tgt := range matches {
112
+ act := func (actx context.Context ) error {
113
+ if err := clickAndWait (actx , tgt , waitFor [lbl ]); err != nil {
114
+ return xerrors .Errorf ("click %q: %w" , tgt , err )
115
+ }
116
+ return nil
68
117
}
69
- return nil
70
- }, nil
118
+ return lbl , act , nil
119
+ }
120
+
121
+ return "" , nil , xerrors .Errorf ("no matches found" )
71
122
}
72
123
73
124
// randMatch returns a random match for the given selector.
@@ -89,25 +140,18 @@ func randMatch(ctx context.Context, s Selector) (Selector, bool, error) {
89
140
90
141
// clickAndWait clicks the given selector and waits for the page to finish loading.
91
142
// The page is considered loaded when the network event "LoadingFinished" is received.
92
- func clickAndWait (ctx context.Context , s Selector ) error {
143
+ func clickAndWait (ctx context.Context , clickOn , waitFor Selector ) error {
93
144
return chromedp .Run (ctx , chromedp.Tasks {
94
- chromedp .Click (s , chromedp .NodeVisible ),
95
- chromedp .ActionFunc (func (ctx context.Context ) error {
96
- return waitForEvent (ctx , func (e interface {}) bool {
97
- if _ , ok := e .(* network.EventLoadingFinished ); ok {
98
- return true
99
- }
100
- return false
101
- })
102
- }),
145
+ chromedp .Click (clickOn , chromedp .NodeVisible ),
146
+ chromedp .WaitVisible (waitFor , chromedp .NodeVisible ),
103
147
})
104
148
}
105
149
106
150
// initChromeDPCtx initializes a chromedp context with the given session token cookie
107
151
//
108
152
//nolint:revive // yes, headless is a control flag
109
153
func initChromeDPCtx (ctx context.Context , u * url.URL , sessionToken string , headless bool ) (context.Context , context.CancelFunc , error ) {
110
- dir , err := os .MkdirTemp ("" , "scaletest-dashboard" )
154
+ dir , err := os .MkdirTemp ("" , "scaletest-dashboard-* " )
111
155
if err != nil {
112
156
return nil , nil , err
113
157
}
@@ -145,7 +189,7 @@ func initChromeDPCtx(ctx context.Context, u *url.URL, sessionToken string, headl
145
189
}
146
190
147
191
func setSessionTokenCookie (ctx context.Context , token , domain string ) error {
148
- exp := cdp .TimeSinceEpoch (time .Now ().Add (30 * 24 * time .Hour ))
192
+ exp := cdp .TimeSinceEpoch (time .Now ().Add (24 * time .Hour ))
149
193
err := chromedp .Run (ctx , network .SetCookie ("coder_session_token" , token ).
150
194
WithExpires (& exp ).
151
195
WithDomain (domain ).
@@ -156,26 +200,17 @@ func setSessionTokenCookie(ctx context.Context, token, domain string) error {
156
200
return nil
157
201
}
158
202
159
- // waitForEvent waits for a lifecycle event that matches the given function.
160
- // Adapted from https://github.com/chromedp/chromedp/issues/431
161
- func waitForEvent (ctx context.Context , matcher func (e interface {}) bool ) error {
162
- ch := make (chan struct {})
163
- cctx , cancel := context .WithCancel (ctx )
164
- defer cancel ()
165
- chromedp .ListenTarget (cctx , func (evt interface {}) {
166
- if matcher (evt ) {
167
- cancel ()
168
- close (ch )
169
- }
170
- })
171
- select {
172
- case <- ch :
173
- return nil
174
- case <- ctx .Done ():
175
- return ctx .Err ()
176
- }
177
- }
178
-
179
203
func visitMainPage (ctx context.Context , u * url.URL ) error {
180
204
return chromedp .Run (ctx , chromedp .Navigate (u .String ()))
181
205
}
206
+
207
+ // pick chooses a random element from a slice.
208
+ // If the slice is empty, it returns the zero value of the type.
209
+ func pick [T any ](s []T ) T {
210
+ if len (s ) == 0 {
211
+ var zero T
212
+ return zero
213
+ }
214
+ // nolint:gosec
215
+ return s [rand .Intn (len (s ))]
216
+ }
0 commit comments