├── LICENSE.txt ├── README.md ├── api_test.go ├── doc.go ├── example_test.go ├── firefox.go ├── remote.go ├── remote_test.go ├── selenium.go ├── selenium_test.go └── test_helpers.go /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Miki Tebeka . 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | --------------------------------------------------------------------------------- 21 | 22 | Portions of code are adapted from from github.com/google/go-github. 23 | Copyright (c) 2013 The go-github AUTHORS. All rights reserved. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ============================================== 2 | go-selenium - Selenium WebDriver client for Go 3 | ============================================== 4 | 5 | go-selenium is a [Selenium](http://seleniumhq.org) WebDriver client for [Go](http://golang.org). 6 | 7 | Note: the public API is experimental and subject to change until further notice. 8 | 9 | 10 | Usage 11 | ===== 12 | 13 | Documentation: [go-selenium on Sourcegraph](https://sourcegraph.com/github.com/sourcegraph/go-selenium). 14 | 15 | Example: see example_test.go: 16 | 17 | ```go 18 | package selenium_test 19 | 20 | import ( 21 | "fmt" 22 | "sourcegraph.com/sourcegraph/go-selenium" 23 | ) 24 | 25 | func ExampleFindElement() { 26 | var webDriver selenium.WebDriver 27 | var err error 28 | caps := selenium.Capabilities(map[string]interface{}{"browserName": "firefox"}) 29 | if webDriver, err = selenium.NewRemote(caps, "http://localhost:4444/wd/hub"); err != nil { 30 | fmt.Printf("Failed to open session: %s\n", err) 31 | return 32 | } 33 | defer webDriver.Quit() 34 | 35 | err = webDriver.Get("https://sourcegraph.com/sourcegraph/go-selenium") 36 | if err != nil { 37 | fmt.Printf("Failed to load page: %s\n", err) 38 | return 39 | } 40 | 41 | if title, err := webDriver.Title(); err == nil { 42 | fmt.Printf("Page title: %s\n", title) 43 | } else { 44 | fmt.Printf("Failed to get page title: %s", err) 45 | return 46 | } 47 | 48 | var elem selenium.WebElement 49 | elem, err = webDriver.FindElement(selenium.ByCSSSelector, ".repo .name") 50 | if err != nil { 51 | fmt.Printf("Failed to find element: %s\n", err) 52 | return 53 | } 54 | 55 | if text, err := elem.Text(); err == nil { 56 | fmt.Printf("Repository: %s\n", text) 57 | } else { 58 | fmt.Printf("Failed to get text of element: %s\n", err) 59 | return 60 | } 61 | 62 | // output: 63 | // Page title: go-selenium - Sourcegraph 64 | // Repository: go-selenium 65 | } 66 | ``` 67 | 68 | The `WebDriverT` and `WebElementT` interfaces make test code cleaner. Each method in 69 | `WebDriver` and `WebElement` has a corresponding method in the `*T` interfaces that omits the error 70 | from the return values and instead calls `t.Fatalf` upon encountering an error. For example: 71 | 72 | ```go 73 | package mytest 74 | 75 | import ( 76 | "sourcegraph.com/sourcegraph/go-selenium" 77 | "testing" 78 | ) 79 | 80 | var caps selenium.Capabilities 81 | var executorURL = "http://localhost:4444/wd/hub" 82 | 83 | // An example test using the WebDriverT and WebElementT interfaces. If you use the non-*T 84 | // interfaces, you must perform error checking that is tangential to what you are testing, 85 | // and you have to destructure results from method calls. 86 | func TestWithT(t *testing.T) { 87 | wd, _ := selenium.NewRemote(caps, executor) 88 | 89 | // Call .T(t) to obtain a WebDriverT from a WebDriver (or to obtain a WebElementT from 90 | // a WebElement). 91 | wdt := wd.T(t) 92 | 93 | // Calls `t.Fatalf("Get: %s", err)` upon failure. 94 | wdt.Get("http://example.com") 95 | 96 | // Calls `t.Fatalf("FindElement(by=%q, value=%q): %s", by, value, err)` upon failure. 97 | elem := wdt.FindElement(selenium.ByCSSSelector, ".foo") 98 | 99 | // Calls `t.Fatalf("Text: %s", err)` if the `.Text()` call fails. 100 | if elem.Text() != "bar" { 101 | t.Fatalf("want elem text %q, got %q", "bar", elem.Text()) 102 | } 103 | } 104 | ``` 105 | 106 | See remote_test.go for more usage examples. 107 | 108 | 109 | 110 | Running tests 111 | ============= 112 | 113 | Start Selenium WebDriver and run `go test`. To see all available options, run `go test -test.h`. 114 | 115 | 116 | TODO 117 | ==== 118 | 119 | * Support Firefox profiles 120 | 121 | 122 | Contributors 123 | ============ 124 | 125 | * Quinn Slack 126 | * Miki Tebeka (go-selenium is based on Miki's 127 | [github.com/tebeka/selenium](https://github.com/tebeka/selenium) library) 128 | 129 | 130 | License 131 | ======= 132 | 133 | go-selenium is distributed under the Eclipse Public License. 134 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package selenium 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestExecuteScript_Args(t *testing.T) { 12 | setup() 13 | defer teardown() 14 | 15 | input := map[string]interface{}{"script": "return 'foo'", "args": []interface{}{}} 16 | mux.HandleFunc("/session/123/execute", func(w http.ResponseWriter, r *http.Request) { 17 | var v map[string]interface{} 18 | json.NewDecoder(r.Body).Decode(&v) 19 | 20 | testMethod(t, r, "POST") 21 | testHeader(t, r, "content-type", "application/json") 22 | testHeader(t, r, "accept", "application/json") 23 | 24 | if !reflect.DeepEqual(v, input) { 25 | t.Errorf("Request body = %+v, want %+v", v, input) 26 | } 27 | 28 | fmt.Fprint(w, `{"status": 0, "value": "foo"}`) 29 | }) 30 | 31 | result, err := client.ExecuteScript("return 'foo'", []interface{}{}) 32 | if err != nil { 33 | t.Errorf("ExecuteScript returned error: %v", err) 34 | } 35 | 36 | want := "foo" 37 | if !reflect.DeepEqual(result, want) { 38 | t.Errorf("ExecuteScript returned %+v, want %+v", result, want) 39 | } 40 | } 41 | 42 | func TestExecuteScript_NoArgs(t *testing.T) { 43 | setup() 44 | defer teardown() 45 | 46 | mux.HandleFunc("/session/123/execute", func(w http.ResponseWriter, r *http.Request) { 47 | var v map[string]interface{} 48 | json.NewDecoder(r.Body).Decode(&v) 49 | 50 | args := []interface{}{} 51 | if !reflect.DeepEqual(v["args"], args) { 52 | t.Errorf("Args = %+v, want %+v", v["args"], args) 53 | } 54 | 55 | fmt.Fprint(w, `{"status": 0, "value": "foo"}`) 56 | }) 57 | 58 | client.ExecuteScript("return 'foo'", nil) 59 | } 60 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // A Selenium WebDriver client for browser testing of Web applications. 2 | package selenium 3 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package selenium_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "sourcegraph.com/sourcegraph/go-selenium" 7 | ) 8 | 9 | func ExampleFindElement() { 10 | var webDriver selenium.WebDriver 11 | var err error 12 | caps := selenium.Capabilities(map[string]interface{}{"browserName": "firefox"}) 13 | if webDriver, err = selenium.NewRemote(caps, "http://localhost:4444/wd/hub"); err != nil { 14 | fmt.Printf("Failed to open session: %s\n", err) 15 | return 16 | } 17 | defer webDriver.Quit() 18 | 19 | err = webDriver.Get("https://github.com/sourcegraph/go-selenium") 20 | if err != nil { 21 | fmt.Printf("Failed to load page: %s\n", err) 22 | return 23 | } 24 | 25 | if title, err := webDriver.Title(); err == nil { 26 | fmt.Printf("Page title: %s\n", title) 27 | } else { 28 | fmt.Printf("Failed to get page title: %s", err) 29 | return 30 | } 31 | 32 | var elem selenium.WebElement 33 | elem, err = webDriver.FindElement(selenium.ByCSSSelector, ".author") 34 | if err != nil { 35 | fmt.Printf("Failed to find element: %s\n", err) 36 | return 37 | } 38 | 39 | if text, err := elem.Text(); err == nil { 40 | fmt.Printf("Author: %s\n", text) 41 | } else { 42 | fmt.Printf("Failed to get text of element: %s\n", err) 43 | return 44 | } 45 | 46 | // output: 47 | // Page title: GitHub - sourcegraph/go-selenium: Selenium WebDriver client for Go 48 | // Author: sourcegraph 49 | } 50 | -------------------------------------------------------------------------------- /firefox.go: -------------------------------------------------------------------------------- 1 | package selenium 2 | 3 | var defaultProfile = map[string]string{ 4 | "app.update.auto": "false", 5 | "app.update.enabled": "false", 6 | "browser.startup.page": "0", 7 | "browser.download.manager.showWhenStarting": "false", 8 | "browser.EULA.override": "true", 9 | "browser.EULA.3.accepted": "true", 10 | "browser.link.open_external": "2", 11 | "browser.link.open_newwindow": "2", 12 | "browser.offline": "false", 13 | "browser.safebrowsing.enabled": "false", 14 | "browser.search.update": "false", 15 | "browser.sessionstore.resume_from_crash": "false", 16 | "browser.shell.checkDefaultBrowser": "false", 17 | "browser.tabs.warnOnClose": "false", 18 | "browser.tabs.warnOnOpen": "false", 19 | "startup.homepage_welcome_url": "\"about:blank\"", 20 | "devtools.errorconsole.enabled": "true", 21 | "dom.disable_open_during_load": "false", 22 | "dom.max_script_run_time": "30", 23 | "extensions.logging.enabled": "true", 24 | "extensions.update.enabled": "false", 25 | "extensions.update.notifyUser": "false", 26 | "network.manage-offline-status": "false", 27 | "network.http.max-connections-per-server": "10", 28 | "network.http.phishy-userpass-length": "255", 29 | "prompts.tab_modal.enabled": "false", 30 | "security.fileuri.origin_policy": "3", 31 | "security.fileuri.strict_origin_policy": "false", 32 | "security.warn_entering_secure": "false", 33 | "security.warn_entering_secure.show_once": "false", 34 | "security.warn_entering_weak": "false", 35 | "security.warn_entering_weak.show_once": "false", 36 | "security.warn_leaving_secure": "false", 37 | "security.warn_leaving_secure.show_once": "false", 38 | "security.warn_submit_insecure": "false", 39 | "security.warn_viewing_mixed": "false", 40 | "security.warn_viewing_mixed.show_once": "false", 41 | "signon.rememberSignons": "false", 42 | "toolkit.networkmanager.disable": "true", 43 | "javascript.options.showInConsole": "true", 44 | "browser.dom.window.dump.enabled": "true", 45 | "webdriver_accept_untrusted_certs": "true", 46 | "webdriver_enable_native_events": "true", 47 | } 48 | 49 | type FirefoxProfile struct { 50 | Root string 51 | } 52 | -------------------------------------------------------------------------------- /remote.go: -------------------------------------------------------------------------------- 1 | /* Remote Selenium client implementation. 2 | 3 | See http://code.google.com/p/selenium/wiki/JsonWireProtocol for wire protocol. 4 | */ 5 | 6 | package selenium 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | "encoding/base64" 12 | "encoding/json" 13 | "errors" 14 | "fmt" 15 | "io" 16 | "io/ioutil" 17 | "log" 18 | "net/http" 19 | "net/http/httputil" 20 | "os" 21 | "strings" 22 | "sync" 23 | ) 24 | 25 | var Log = log.New(os.Stderr, "[selenium] ", log.Ltime|log.Lmicroseconds) 26 | var Trace bool 27 | 28 | /* Errors returned by Selenium server. */ 29 | var errorCodes = map[int]string{ 30 | 7: "no such element", 31 | 8: "no such frame", 32 | 9: "unknown command", 33 | 10: "stale element reference", 34 | 11: "element not visible", 35 | 12: "invalid element state", 36 | 13: "unknown error", 37 | 15: "element is not selectable", 38 | 17: "javascript error", 39 | 19: "xpath lookup error", 40 | 21: "timeout", 41 | 23: "no such window", 42 | 24: "invalid cookie domain", 43 | 25: "unable to set cookie", 44 | 26: "unexpected alert open", 45 | 27: "no alert open", 46 | 28: "script timeout", 47 | 29: "invalid element coordinates", 48 | 32: "invalid selector", 49 | } 50 | 51 | const ( 52 | SUCCESS = 0 53 | defaultExecutor = "http://127.0.0.1:4444/wd/hub" 54 | jsonMIMEType = "application/json" 55 | ) 56 | 57 | type remoteWebDriver struct { 58 | id, executor string 59 | capabilities Capabilities 60 | // FIXME 61 | // profile BrowserProfile 62 | ctx context.Context 63 | 64 | haveQuitMu sync.Mutex 65 | haveQuit bool 66 | } 67 | 68 | func (wd *remoteWebDriver) SetContext(ctx context.Context) { 69 | wd.ctx = ctx 70 | } 71 | 72 | func (wd *remoteWebDriver) url(template string, args ...interface{}) string { 73 | path := fmt.Sprintf(template, args...) 74 | return wd.executor + path 75 | } 76 | 77 | func (wd *remoteWebDriver) send(method, url string, data []byte) (r *reply, err error) { 78 | var buf []byte 79 | if buf, err = wd.execute(method, url, data); err == nil { 80 | if len(buf) > 0 { 81 | err = json.Unmarshal(buf, &r) 82 | } 83 | } 84 | return 85 | } 86 | 87 | // ErrCanceled is returned when the context is cancelled. 88 | var ErrCanceled = errors.New("cancelled") 89 | 90 | func (wd *remoteWebDriver) execute(method, url string, data []byte) (buf []byte, err error) { 91 | select { 92 | case <-wd.ctx.Done(): 93 | err = ErrCanceled 94 | wd.ctx = context.Background() 95 | _ = wd.Quit() 96 | return 97 | default: 98 | } 99 | defer func() { 100 | select { 101 | case <-wd.ctx.Done(): 102 | err = ErrCanceled 103 | wd.ctx = context.Background() 104 | _ = wd.Quit() 105 | return 106 | default: 107 | } 108 | }() 109 | 110 | if Log != nil { 111 | Log.Printf("-> %s %s [%d bytes]", method, url, len(data)) 112 | } 113 | req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) 114 | if err != nil { 115 | return nil, err 116 | } 117 | req.Header.Add("Accept", jsonMIMEType) 118 | if method == "POST" { 119 | req.Header.Add("Content-Type", jsonMIMEType) 120 | } 121 | 122 | if Trace { 123 | if dump, err := httputil.DumpRequest(req, true); err == nil && Log != nil { 124 | Log.Printf("-> TRACE\n%s", dump) 125 | } 126 | } 127 | 128 | req = req.WithContext(wd.ctx) 129 | 130 | res, err := httpClient.Do(req) 131 | if err != nil { 132 | return nil, err 133 | } 134 | defer res.Body.Close() 135 | 136 | if Trace { 137 | if dump, err := httputil.DumpResponse(res, true); err == nil && Log != nil { 138 | Log.Printf("<- TRACE\n%s", dump) 139 | } 140 | } 141 | 142 | buf, err = ioutil.ReadAll(res.Body) 143 | if err != nil { 144 | return nil, err 145 | } 146 | if Log != nil { 147 | Log.Printf("<- %s (%s) [%d bytes]", res.Status, res.Header["Content-Type"], len(buf)) 148 | } 149 | 150 | if res.StatusCode >= 400 { 151 | reply := new(reply) 152 | err := json.Unmarshal(buf, reply) 153 | if err != nil { 154 | return nil, errors.New(fmt.Sprintf("Bad server reply status: %s", res.Status)) 155 | } 156 | message, ok := errorCodes[reply.Status] 157 | if !ok { 158 | message = fmt.Sprintf("unknown error - %d", reply.Status) 159 | } 160 | 161 | return nil, errors.New(message) 162 | } 163 | 164 | /* Some bug(?) in Selenium gets us nil values in output, json.Unmarshal is 165 | * not happy about that. 166 | */ 167 | if strings.HasPrefix(res.Header.Get("Content-Type"), jsonMIMEType) { 168 | reply := new(reply) 169 | err := json.Unmarshal(buf, reply) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | if reply.Status != SUCCESS { 175 | message, ok := errorCodes[reply.Status] 176 | if !ok { 177 | message = fmt.Sprintf("unknown error - %d", reply.Status) 178 | } 179 | 180 | return nil, errors.New(message) 181 | } 182 | return buf, err 183 | } 184 | 185 | // Nothing was returned, this is OK for some commands 186 | return buf, nil 187 | } 188 | 189 | var httpClient = http.Client{ 190 | // WebDriver requires that all requests have an 'Accept: application/json' header. We must add 191 | // it here because by default net/http will not include that header when following redirects. 192 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 193 | if len(via) >= 10 { 194 | return errors.New("stopped after 10 redirects") 195 | } 196 | req.Header.Add("Accept", jsonMIMEType) 197 | if Trace { 198 | if dump, err := httputil.DumpRequest(req, true); err == nil && Log != nil { 199 | Log.Printf("-> TRACE (redirected request)\n%s", dump) 200 | } 201 | } 202 | return nil 203 | }, 204 | } 205 | 206 | // Server reply to WebDriver command. 207 | type reply struct { 208 | SessionId string 209 | Status int 210 | Value json.RawMessage 211 | } 212 | 213 | func (r *reply) readValue(v interface{}) error { 214 | return json.Unmarshal(r.Value, v) 215 | } 216 | 217 | // An active session. 218 | type Session struct { 219 | Id string 220 | Capabilities Capabilities 221 | } 222 | 223 | /* Create new remote client, this will also start a new session. 224 | capabilities - the desired capabilities, see http://goo.gl/SNlAk 225 | executor - the URL to the Selenim server 226 | */ 227 | func NewRemote(capabilities Capabilities, executor string) (WebDriver, error) { 228 | if executor == "" { 229 | executor = defaultExecutor 230 | } 231 | 232 | wd := &remoteWebDriver{ 233 | executor: executor, 234 | capabilities: capabilities, 235 | ctx: context.Background(), 236 | } 237 | // FIXME: Handle profile 238 | 239 | _, err := wd.NewSession() 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | return wd, nil 245 | } 246 | 247 | func (wd *remoteWebDriver) stringCommand(urlTemplate string) (v string, err error) { 248 | var r *reply 249 | if r, err = wd.send("GET", wd.url(urlTemplate, wd.id), nil); err == nil { 250 | err = r.readValue(&v) 251 | } 252 | return 253 | } 254 | 255 | func (wd *remoteWebDriver) voidCommand(urlTemplate string, params interface{}) (err error) { 256 | var data []byte 257 | if params != nil { 258 | data, err = json.Marshal(params) 259 | } 260 | if err == nil { 261 | _, err = wd.send("POST", wd.url(urlTemplate, wd.id), data) 262 | } 263 | return 264 | 265 | } 266 | 267 | func (wd remoteWebDriver) stringsCommand(urlTemplate string) (v []string, err error) { 268 | var r *reply 269 | if r, err = wd.send("GET", wd.url(urlTemplate, wd.id), nil); err == nil { 270 | err = r.readValue(&v) 271 | } 272 | return 273 | } 274 | 275 | func (wd *remoteWebDriver) boolCommand(urlTemplate string) (v bool, err error) { 276 | var r *reply 277 | if r, err = wd.send("GET", wd.url(urlTemplate, wd.id), nil); err == nil { 278 | err = r.readValue(&v) 279 | } 280 | return 281 | } 282 | 283 | // WebDriver interface implementation 284 | 285 | func (wd *remoteWebDriver) Status() (v *Status, err error) { 286 | var r *reply 287 | if r, err = wd.send("GET", wd.url("/status"), nil); err == nil { 288 | err = r.readValue(&v) 289 | } 290 | return 291 | } 292 | 293 | func (wd *remoteWebDriver) Sessions() (sessions []Session, err error) { 294 | var r *reply 295 | if r, err = wd.send("GET", wd.url("/sessions"), nil); err == nil { 296 | err = r.readValue(&sessions) 297 | } 298 | return 299 | } 300 | 301 | func (wd *remoteWebDriver) NewSession() (string, error) { 302 | message := map[string]interface{}{ 303 | "desiredCapabilities": wd.capabilities, 304 | } 305 | 306 | var data []byte 307 | data, err := json.Marshal(message) 308 | if err != nil { 309 | return "", err 310 | } 311 | 312 | r, err := wd.send("POST", wd.url("/session"), data) 313 | if err != nil { 314 | return "", err 315 | } 316 | wd.id = r.SessionId 317 | 318 | return r.SessionId, nil 319 | } 320 | 321 | func (wd *remoteWebDriver) Capabilities() (v Capabilities, err error) { 322 | var r *reply 323 | if r, err = wd.send("GET", wd.url("/session/%s", wd.id), nil); err == nil { 324 | r.readValue(&v) 325 | } 326 | return 327 | } 328 | 329 | func (wd *remoteWebDriver) SetTimeout(timeoutType string, ms uint) error { 330 | params := map[string]interface{}{"type": timeoutType, "ms": ms} 331 | return wd.voidCommand("/session/%s/timeouts", params) 332 | } 333 | 334 | func (wd *remoteWebDriver) SetAsyncScriptTimeout(ms uint) error { 335 | params := map[string]uint{"ms": ms} 336 | return wd.voidCommand("/session/%s/timeouts/async_script", params) 337 | } 338 | 339 | func (wd *remoteWebDriver) SetImplicitWaitTimeout(ms uint) error { 340 | params := map[string]uint{"ms": ms} 341 | return wd.voidCommand("/session/%s/timeouts/implicit_wait", params) 342 | } 343 | 344 | func (wd *remoteWebDriver) AvailableEngines() ([]string, error) { 345 | return wd.stringsCommand("/session/%s/ime/available_engines") 346 | } 347 | 348 | func (wd *remoteWebDriver) ActiveEngine() (string, error) { 349 | return wd.stringCommand("/session/%s/ime/active_engine") 350 | } 351 | 352 | func (wd *remoteWebDriver) IsEngineActivated() (bool, error) { 353 | return wd.boolCommand("/session/%s/ime/activated") 354 | } 355 | 356 | func (wd *remoteWebDriver) DeactivateEngine() error { 357 | return wd.voidCommand("session/%s/ime/deactivate", nil) 358 | } 359 | 360 | func (wd *remoteWebDriver) ActivateEngine(engine string) (err error) { 361 | return wd.voidCommand("/session/%s/ime/activate", map[string]string{"engine": engine}) 362 | } 363 | 364 | func (wd *remoteWebDriver) Quit() (err error) { 365 | wd.haveQuitMu.Lock() 366 | defer wd.haveQuitMu.Unlock() 367 | if wd.haveQuit { 368 | // Double-Quit is an error-free no-op. 369 | return nil 370 | } 371 | wd.haveQuit = true 372 | // Quit is the one method which cannot be canceled. 373 | // It's also the last thing that happens in a webdriver, so we can 374 | // kill the context here. 375 | wd.ctx = context.Background() 376 | 377 | if _, err = wd.execute("DELETE", wd.url("/session/%s", wd.id), nil); err == nil { 378 | wd.id = "" 379 | } 380 | return 381 | } 382 | 383 | func (wd *remoteWebDriver) CurrentWindowHandle() (string, error) { 384 | return wd.stringCommand("/session/%s/window_handle") 385 | } 386 | 387 | func (wd *remoteWebDriver) WindowHandles() ([]string, error) { 388 | return wd.stringsCommand("/session/%s/window_handles") 389 | } 390 | 391 | func (wd *remoteWebDriver) CurrentURL() (string, error) { 392 | return wd.stringCommand("/session/%s/url") 393 | } 394 | 395 | func (wd *remoteWebDriver) Get(url string) error { 396 | return wd.voidCommand("/session/%s/url", map[string]string{"url": url}) 397 | } 398 | 399 | func (wd *remoteWebDriver) Forward() error { 400 | return wd.voidCommand("/session/%s/forward", nil) 401 | } 402 | 403 | func (wd *remoteWebDriver) Back() error { 404 | return wd.voidCommand("/session/%s/back", nil) 405 | } 406 | 407 | func (wd *remoteWebDriver) Refresh() error { 408 | return wd.voidCommand("/session/%s/refresh", nil) 409 | } 410 | 411 | func (wd *remoteWebDriver) Title() (string, error) { 412 | return wd.stringCommand("/session/%s/title") 413 | } 414 | 415 | func (wd *remoteWebDriver) PageSource() (string, error) { 416 | return wd.stringCommand("/session/%s/source") 417 | } 418 | 419 | type element struct { 420 | Element string `json:"ELEMENT"` 421 | } 422 | 423 | func (wd *remoteWebDriver) find(by, value, suffix, url string) (r *reply, err error) { 424 | params := map[string]string{"using": by, "value": value} 425 | var data []byte 426 | if data, err = json.Marshal(params); err == nil { 427 | if url == "" { 428 | url = "/session/%s/element" 429 | } 430 | urlTemplate := url + suffix 431 | url = wd.url(urlTemplate, wd.id) 432 | r, err = wd.send("POST", url, data) 433 | } 434 | return 435 | } 436 | 437 | func decodeElement(wd *remoteWebDriver, r *reply) WebElement { 438 | var elem element 439 | if err := r.readValue(&elem); err != nil { 440 | panic(err.Error() + ": " + string(r.Value)) 441 | } 442 | return &remoteWE{parent: wd, id: elem.Element} 443 | } 444 | 445 | func (wd *remoteWebDriver) FindElement(by, value string) (WebElement, error) { 446 | if res, err := wd.find(by, value, "", ""); err == nil { 447 | return decodeElement(wd, res), nil 448 | } else { 449 | return nil, err 450 | } 451 | } 452 | 453 | func decodeElements(wd *remoteWebDriver, r *reply) (welems []WebElement) { 454 | var elems []element 455 | if err := r.readValue(&elems); err != nil { 456 | panic(err.Error() + ": " + string(r.Value)) 457 | } 458 | for _, elem := range elems { 459 | welems = append(welems, &remoteWE{wd, elem.Element}) 460 | } 461 | return 462 | } 463 | 464 | func (wd *remoteWebDriver) FindElements(by, value string) ([]WebElement, error) { 465 | if res, err := wd.find(by, value, "s", ""); err == nil { 466 | return decodeElements(wd, res), nil 467 | } else { 468 | return nil, err 469 | } 470 | } 471 | 472 | func (wd *remoteWebDriver) Q(sel string) (WebElement, error) { 473 | return wd.FindElement(ByCSSSelector, sel) 474 | } 475 | 476 | func (wd *remoteWebDriver) QAll(sel string) ([]WebElement, error) { 477 | return wd.FindElements(ByCSSSelector, sel) 478 | } 479 | 480 | func (wd *remoteWebDriver) Close() error { 481 | _, err := wd.execute("DELETE", wd.url("/session/%s/window", wd.id), nil) 482 | return err 483 | } 484 | 485 | func (wd *remoteWebDriver) SwitchWindow(name string) error { 486 | if name == "" { 487 | name = "current" 488 | } 489 | params := map[string]string{"name": name} 490 | return wd.voidCommand("/session/%s/window", params) 491 | } 492 | 493 | func (wd *remoteWebDriver) CloseWindow(name string) error { 494 | _, err := wd.execute("DELETE", wd.url("/session/%s/window", wd.id), nil) 495 | return err 496 | } 497 | 498 | func (wd *remoteWebDriver) WindowSize(name string) (sz *Size, err error) { 499 | if name == "" { 500 | name = "current" 501 | } 502 | url := wd.url("/session/%s/window/%s/size", wd.id, name) 503 | var r *reply 504 | if r, err = wd.send("GET", url, nil); err == nil { 505 | err = r.readValue(&sz) 506 | } 507 | return 508 | } 509 | 510 | func (wd *remoteWebDriver) WindowPosition(name string) (pt *Point, err error) { 511 | if name == "" { 512 | name = "current" 513 | } 514 | url := wd.url("/session/%s/window/%s/position", wd.id, name) 515 | var r *reply 516 | if r, err = wd.send("GET", url, nil); err == nil { 517 | err = r.readValue(&pt) 518 | } 519 | return 520 | } 521 | 522 | func (wd *remoteWebDriver) ResizeWindow(name string, to Size) error { 523 | if name == "" { 524 | name = "current" 525 | } 526 | url := wd.url("/session/%s/window/%s/size", wd.id, name) 527 | data, err := json.Marshal(to) 528 | if err != nil { 529 | return err 530 | } 531 | _, err = wd.send("POST", url, data) 532 | return err 533 | } 534 | 535 | func (wd *remoteWebDriver) SwitchFrame(frame string) error { 536 | params := map[string]string{"id": frame} 537 | return wd.voidCommand("/session/%s/frame", params) 538 | } 539 | 540 | func (wd *remoteWebDriver) SwitchFrameParent() error { 541 | return wd.voidCommand("/session/%s/frame/parent", nil) 542 | } 543 | 544 | func (wd *remoteWebDriver) ActiveElement() (WebElement, error) { 545 | url := wd.url("/session/%s/element/active", wd.id) 546 | if r, err := wd.send("GET", url, nil); err == nil { 547 | return decodeElement(wd, r), nil 548 | } else { 549 | return nil, err 550 | } 551 | } 552 | 553 | func (wd *remoteWebDriver) GetCookies() (c []Cookie, err error) { 554 | var r *reply 555 | if r, err = wd.send("GET", wd.url("/session/%s/cookie", wd.id), nil); err == nil { 556 | err = r.readValue(&c) 557 | if err == nil { 558 | parseCookieExpiry(&c, r.Value) 559 | } 560 | } 561 | return 562 | } 563 | 564 | func parseCookieExpiry(cookies *[]Cookie, raw json.RawMessage) { 565 | var expiries []struct { 566 | Expiry json.Number 567 | } 568 | 569 | err := json.Unmarshal(raw, &expiries) 570 | if err != nil { 571 | return 572 | } 573 | 574 | for i, _ := range *cookies { 575 | expiry, err := expiries[i].Expiry.Float64() 576 | if err != nil { 577 | continue 578 | } 579 | 580 | (*cookies)[i].Expiry = uint(expiry) 581 | } 582 | } 583 | 584 | func (wd *remoteWebDriver) AddCookie(cookie *Cookie) error { 585 | params := map[string]*Cookie{"cookie": cookie} 586 | return wd.voidCommand("/session/%s/cookie", params) 587 | } 588 | 589 | func (wd *remoteWebDriver) DeleteAllCookies() error { 590 | _, err := wd.execute("DELETE", wd.url("/session/%s/cookie", wd.id), nil) 591 | return err 592 | } 593 | 594 | func (wd *remoteWebDriver) DeleteCookie(name string) error { 595 | _, err := wd.execute("DELETE", wd.url("/session/%s/cookie/%s", wd.id, name), nil) 596 | return err 597 | } 598 | 599 | func (wd *remoteWebDriver) Click(button int) error { 600 | params := map[string]int{"button": button} 601 | return wd.voidCommand("/session/%s/click", params) 602 | } 603 | 604 | func (wd *remoteWebDriver) DoubleClick() error { 605 | return wd.voidCommand("/session/%s/doubleclick", nil) 606 | } 607 | 608 | func (wd *remoteWebDriver) ButtonDown() error { 609 | return wd.voidCommand("/session/%s/buttondown", nil) 610 | } 611 | 612 | func (wd *remoteWebDriver) ButtonUp() error { 613 | return wd.voidCommand("/session/%s/buttonup", nil) 614 | } 615 | 616 | func (wd *remoteWebDriver) SendModifier(modifier string, isDown bool) error { 617 | params := map[string]interface{}{ 618 | "value": modifier, 619 | "isdown": isDown, 620 | } 621 | 622 | data, err := json.Marshal(params) 623 | if err != nil { 624 | return err 625 | } 626 | 627 | return wd.voidCommand("/session/%s/modifier", data) 628 | } 629 | 630 | func (wd *remoteWebDriver) DismissAlert() error { 631 | return wd.voidCommand("/session/%s/dismiss_alert", nil) 632 | } 633 | 634 | func (wd *remoteWebDriver) AcceptAlert() error { 635 | return wd.voidCommand("/session/%s/accept_alert", nil) 636 | } 637 | 638 | func (wd *remoteWebDriver) AlertText() (string, error) { 639 | return wd.stringCommand("/session/%s/alert_text") 640 | } 641 | 642 | func (wd *remoteWebDriver) SetAlertText(text string) error { 643 | params := map[string]string{"text": text} 644 | return wd.voidCommand("/session/%s/alert_text", params) 645 | } 646 | 647 | func (wd *remoteWebDriver) execScript(script string, args []interface{}, suffix string) (res interface{}, err error) { 648 | if args == nil { 649 | args = []interface{}{} 650 | } 651 | for i, arg := range args { 652 | if v, ok := arg.(*remoteWE); ok { 653 | args[i] = &element{Element: v.id} 654 | } 655 | } 656 | params := map[string]interface{}{ 657 | "script": script, 658 | "args": args, 659 | } 660 | var data []byte 661 | if data, err = json.Marshal(params); err != nil { 662 | return nil, err 663 | } 664 | url := wd.url("/session/%s/execute"+suffix, wd.id) 665 | var r *reply 666 | if r, err = wd.send("POST", url, data); err == nil { 667 | err = r.readValue(&res) 668 | } 669 | return 670 | } 671 | 672 | func (wd *remoteWebDriver) ExecuteScript(script string, args []interface{}) (interface{}, error) { 673 | return wd.execScript(script, args, "") 674 | } 675 | 676 | func (wd *remoteWebDriver) ExecuteScriptAsync(script string, args []interface{}) (interface{}, error) { 677 | return wd.execScript(script, args, "_async") 678 | } 679 | 680 | func (wd *remoteWebDriver) Screenshot() (io.Reader, error) { 681 | data, err := wd.stringCommand("/session/%s/screenshot") 682 | if err != nil { 683 | return nil, err 684 | } 685 | 686 | // Selenium returns base64 encoded image 687 | buf := []byte(data) 688 | decoder := base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(buf)) 689 | return decoder, nil 690 | } 691 | 692 | func (wd *remoteWebDriver) T(t TestingT) WebDriverT { 693 | return &webDriverT{wd, t} 694 | } 695 | 696 | // WebElement interface implementation 697 | 698 | type remoteWE struct { 699 | parent *remoteWebDriver 700 | id string 701 | } 702 | 703 | func (elem *remoteWE) Click() error { 704 | urlTemplate := fmt.Sprintf("/session/%%s/element/%s/click", elem.id) 705 | return elem.parent.voidCommand(urlTemplate, nil) 706 | } 707 | 708 | func (elem *remoteWE) SendKeys(keys string) error { 709 | chars := make([]string, len(keys)) 710 | for i, c := range keys { 711 | chars[i] = string(c) 712 | } 713 | params := map[string][]string{"value": chars} 714 | urltmpl := fmt.Sprintf("/session/%%s/element/%s/value", elem.id) 715 | return elem.parent.voidCommand(urltmpl, params) 716 | } 717 | 718 | func (elem *remoteWE) TagName() (string, error) { 719 | urlTemplate := fmt.Sprintf("/session/%%s/element/%s/name", elem.id) 720 | return elem.parent.stringCommand(urlTemplate) 721 | } 722 | 723 | func (elem *remoteWE) Text() (string, error) { 724 | urlTemplate := fmt.Sprintf("/session/%%s/element/%s/text", elem.id) 725 | return elem.parent.stringCommand(urlTemplate) 726 | } 727 | 728 | func (elem *remoteWE) Submit() error { 729 | urlTemplate := fmt.Sprintf("/session/%%s/element/%s/submit", elem.id) 730 | return elem.parent.voidCommand(urlTemplate, nil) 731 | } 732 | 733 | func (elem *remoteWE) Clear() error { 734 | urlTemplate := fmt.Sprintf("/session/%%s/element/%s/clear", elem.id) 735 | return elem.parent.voidCommand(urlTemplate, nil) 736 | } 737 | 738 | func (elem *remoteWE) MoveTo(xOffset, yOffset int) error { 739 | params := map[string]interface{}{ 740 | "element": elem.id, 741 | "xoffset": xOffset, 742 | "yoffset": yOffset, 743 | } 744 | return elem.parent.voidCommand("/session/%s/moveto", params) 745 | } 746 | 747 | func (elem *remoteWE) FindElement(by, value string) (WebElement, error) { 748 | res, err := elem.parent.find(by, value, "", fmt.Sprintf("/session/%%s/element/%s/element", elem.id)) 749 | if err != nil { 750 | return nil, err 751 | } 752 | return decodeElement(elem.parent, res), nil 753 | } 754 | 755 | func (elem *remoteWE) Q(sel string) (WebElement, error) { 756 | return elem.FindElement(ByCSSSelector, sel) 757 | } 758 | 759 | func (elem *remoteWE) QAll(sel string) ([]WebElement, error) { 760 | return elem.FindElements(ByCSSSelector, sel) 761 | } 762 | 763 | func (elem *remoteWE) FindElements(by, value string) ([]WebElement, error) { 764 | res, err := elem.parent.find(by, value, "s", fmt.Sprintf("/session/%%s/element/%s/element", elem.id)) 765 | if err != nil { 766 | return nil, err 767 | } 768 | return decodeElements(elem.parent, res), nil 769 | } 770 | 771 | func (elem *remoteWE) boolQuery(urlTemplate string) (bool, error) { 772 | url := fmt.Sprintf(urlTemplate, elem.id) 773 | return elem.parent.boolCommand(url) 774 | } 775 | 776 | // Porperties 777 | func (elem *remoteWE) IsSelected() (bool, error) { 778 | return elem.boolQuery("/session/%%s/element/%s/selected") 779 | } 780 | 781 | func (elem *remoteWE) IsEnabled() (bool, error) { 782 | return elem.boolQuery("/session/%%s/element/%s/enabled") 783 | } 784 | 785 | func (elem *remoteWE) IsDisplayed() (bool, error) { 786 | return elem.boolQuery("/session/%%s/element/%s/displayed") 787 | } 788 | 789 | func (elem *remoteWE) GetAttribute(name string) (string, error) { 790 | template := "/session/%%s/element/%s/attribute/%s" 791 | urlTemplate := fmt.Sprintf(template, elem.id, name) 792 | 793 | return elem.parent.stringCommand(urlTemplate) 794 | } 795 | 796 | func (elem *remoteWE) location(suffix string) (pt *Point, err error) { 797 | wd := elem.parent 798 | path := "/session/%s/element/%s/location" + suffix 799 | url := wd.url(path, wd.id, elem.id) 800 | var r *reply 801 | if r, err = wd.send("GET", url, nil); err == nil { 802 | err = r.readValue(&pt) 803 | } 804 | return 805 | } 806 | 807 | func (elem *remoteWE) Location() (*Point, error) { 808 | return elem.location("") 809 | } 810 | 811 | func (elem *remoteWE) LocationInView() (*Point, error) { 812 | return elem.location("_in_view") 813 | } 814 | 815 | func (elem *remoteWE) Size() (sz *Size, err error) { 816 | wd := elem.parent 817 | url := wd.url("/session/%s/element/%s/size", wd.id, elem.id) 818 | var r *reply 819 | if r, err = wd.send("GET", url, nil); err == nil { 820 | err = r.readValue(&sz) 821 | } 822 | return 823 | } 824 | 825 | func (elem *remoteWE) CSSProperty(name string) (string, error) { 826 | urlTemplate := fmt.Sprintf("/session/%%s/element/%s/css/%s", elem.id, name) 827 | return elem.parent.stringCommand(urlTemplate) 828 | } 829 | 830 | func (elem *remoteWE) T(t TestingT) WebElementT { 831 | return &webElementT{elem, t} 832 | } 833 | -------------------------------------------------------------------------------- /remote_test.go: -------------------------------------------------------------------------------- 1 | package selenium 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | var grid = flag.Bool("test.grid", false, "skip tests that fail on Selenium Grid") 14 | var executor = flag.String("test.executor", defaultExecutor, "executor URL") 15 | var browserName = flag.String("test.browserName", "firefox", "browser to run tests on") 16 | 17 | func init() { 18 | flag.BoolVar(&Trace, "trace", false, "trace HTTP requests and responses") 19 | flag.Parse() 20 | 21 | caps["browserName"] = *browserName 22 | } 23 | 24 | var caps Capabilities = make(Capabilities) 25 | 26 | var runOnSauce *bool = flag.Bool("saucelabs", false, "run on sauce") 27 | 28 | func newRemote(testName string, t *testing.T) (wd WebDriver) { 29 | var err error 30 | if wd, err = NewRemote(caps, *executor); err != nil { 31 | t.Fatalf("can't start session for test %s: %s", testName, err) 32 | } 33 | return wd 34 | } 35 | 36 | func TestStatus(t *testing.T) { 37 | if *grid { 38 | t.Skip() 39 | } 40 | t.Parallel() 41 | wd := newRemote("TestStatus", t) 42 | defer wd.Quit() 43 | 44 | status, err := wd.Status() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | if status.OS.Name == "" { 50 | t.Fatal("No OS") 51 | } 52 | } 53 | 54 | func TestSessions(t *testing.T) { 55 | if *grid { 56 | t.Skip() 57 | } 58 | t.Parallel() 59 | wd := newRemote("TestSessions", t) 60 | defer wd.Quit() 61 | 62 | _, err := wd.Sessions() 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | } 67 | 68 | func TestNewSession(t *testing.T) { 69 | t.Parallel() 70 | if *runOnSauce { 71 | return 72 | } 73 | wd := &remoteWebDriver{capabilities: caps, executor: *executor} 74 | sid, err := wd.NewSession() 75 | defer wd.Quit() 76 | 77 | if err != nil { 78 | t.Fatalf("error in new session - %s", err) 79 | } 80 | 81 | if sid == "" { 82 | t.Fatal("Empty session id") 83 | } 84 | 85 | if wd.id != sid { 86 | t.Fatal("Session id mismatch") 87 | } 88 | } 89 | 90 | func TestCapabilities(t *testing.T) { 91 | t.Parallel() 92 | wd := newRemote("TestCapabilities", t) 93 | defer wd.Quit() 94 | 95 | c, err := wd.Capabilities() 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if c["browserName"] != caps["browserName"] { 101 | t.Fatalf("bad browser name - %s", c["browserName"]) 102 | } 103 | } 104 | 105 | func TestSetTimeout(t *testing.T) { 106 | t.Parallel() 107 | wd := newRemote("TestSetTimeout", t).T(t) 108 | defer wd.Quit() 109 | 110 | wd.SetTimeout("script", 200) 111 | wd.SetTimeout("implicit", 200) 112 | wd.SetTimeout("page load", 200) 113 | } 114 | 115 | func TestSetAsyncScriptTimeout(t *testing.T) { 116 | t.Parallel() 117 | wd := newRemote("TestSetAsyncScriptTimeout", t).T(t) 118 | defer wd.Quit() 119 | 120 | wd.SetAsyncScriptTimeout(200) 121 | } 122 | 123 | func TestSetImplicitWaitTimeout(t *testing.T) { 124 | t.Parallel() 125 | wd := newRemote("TestSetImplicitWaitTimeout", t).T(t) 126 | defer wd.Quit() 127 | 128 | wd.SetImplicitWaitTimeout(200) 129 | } 130 | 131 | func TestCurrentWindowHandle(t *testing.T) { 132 | t.Parallel() 133 | wd := newRemote("TestCurrentWindowHandle", t).T(t) 134 | defer wd.Quit() 135 | 136 | handle := wd.CurrentWindowHandle() 137 | 138 | if handle == "" { 139 | t.Fatal("Empty handle") 140 | } 141 | } 142 | 143 | func TestWindowHandles(t *testing.T) { 144 | t.Parallel() 145 | wd := newRemote("TestWindowHandles", t).T(t) 146 | defer wd.Quit() 147 | 148 | handles := wd.CurrentWindowHandle() 149 | 150 | if handles == "" { 151 | t.Fatal("No handles") 152 | } 153 | } 154 | 155 | func TestWindowSize(t *testing.T) { 156 | t.Parallel() 157 | wd := newRemote("TestWindowSize", t).T(t) 158 | defer wd.Quit() 159 | 160 | size := wd.WindowSize(wd.CurrentWindowHandle()) 161 | if size == nil || size.Height == 0 || size.Width == 0 { 162 | t.Fatal("Window size failed with size: %+v", size) 163 | } 164 | } 165 | 166 | func TestWindowPosition(t *testing.T) { 167 | t.Parallel() 168 | wd := newRemote("TestWindowPosition", t).T(t) 169 | defer wd.Quit() 170 | 171 | pos := wd.WindowPosition(wd.CurrentWindowHandle()) 172 | if pos == nil { 173 | t.Fatal("Window position failed") 174 | } 175 | } 176 | 177 | func TestResizeWindow(t *testing.T) { 178 | t.Parallel() 179 | wd := newRemote("TestResizeWindow", t).T(t) 180 | defer wd.Quit() 181 | 182 | wd.ResizeWindow(wd.CurrentWindowHandle(), Size{400, 400}) 183 | 184 | sz := wd.WindowSize(wd.CurrentWindowHandle()) 185 | if int(sz.Width) != 400 { 186 | t.Fatalf("got width %f, want 400", sz.Width) 187 | } 188 | if int(sz.Height) != 400 { 189 | t.Fatalf("got height %f, want 400", sz.Height) 190 | } 191 | } 192 | 193 | func TestGet(t *testing.T) { 194 | t.Parallel() 195 | wd := newRemote("TestGet", t).T(t) 196 | defer wd.Quit() 197 | 198 | wd.Get(serverURL) 199 | 200 | newURL := wd.CurrentURL() 201 | 202 | if newURL != serverURL { 203 | t.Fatalf("%s != %s", newURL, serverURL) 204 | } 205 | } 206 | 207 | func TestNavigation(t *testing.T) { 208 | t.Parallel() 209 | wd := newRemote("TestNavigation", t).T(t) 210 | defer wd.Quit() 211 | 212 | url1 := serverURL 213 | wd.Get(url1) 214 | 215 | url2 := serverURL + "other" 216 | wd.Get(url2) 217 | 218 | wd.Back() 219 | url := wd.CurrentURL() 220 | if url != url1 { 221 | t.Fatalf("back got me to %s (expected %s)", url, url1) 222 | } 223 | wd.Forward() 224 | url = wd.CurrentURL() 225 | if url != url2 { 226 | t.Fatalf("forward got me to %s (expected %s)", url, url2) 227 | } 228 | 229 | wd.Refresh() 230 | url = wd.CurrentURL() 231 | if url != url2 { 232 | t.Fatalf("refresh got me to %s (expected %s)", url, url2) 233 | } 234 | } 235 | 236 | func TestTitle(t *testing.T) { 237 | t.Parallel() 238 | wd := newRemote("TestTitle", t).T(t) 239 | defer wd.Quit() 240 | 241 | wd.Get(serverURL) 242 | title := wd.Title() 243 | expectedTitle := "Go Selenium Test Suite" 244 | if title != expectedTitle { 245 | t.Fatal("Bad title %s, should be %s", title, expectedTitle) 246 | } 247 | } 248 | 249 | func TestPageSource(t *testing.T) { 250 | t.Parallel() 251 | wd := newRemote("TestPageSource", t).T(t) 252 | defer wd.Quit() 253 | 254 | wd.Get(serverURL) 255 | source := wd.PageSource() 256 | if !strings.Contains(source, "The home page.") { 257 | t.Fatalf("Bad source\n%s", source) 258 | } 259 | } 260 | 261 | type elementFinder interface { 262 | FindElement(by, value string) WebElementT 263 | FindElements(by, value string) []WebElementT 264 | } 265 | 266 | func TestFindElement(t *testing.T) { 267 | t.Parallel() 268 | wd := newRemote("TestFindElement", t).T(t) 269 | defer wd.Quit() 270 | wd.Get(serverURL) 271 | testFindElement(t, wd, ByCSSSelector, "ol.list li", "foo") 272 | } 273 | 274 | func TestFindChildElement(t *testing.T) { 275 | t.Parallel() 276 | wd := newRemote("TestFindChildElement", t).T(t) 277 | defer wd.Quit() 278 | wd.Get(serverURL) 279 | testFindElement(t, wd.FindElement(ByTagName, "body"), ByCSSSelector, "ol.list li", "foo") 280 | } 281 | 282 | func testFindElement(t *testing.T, ef elementFinder, by, value string, txt string) { 283 | elem := ef.FindElement(by, value) 284 | if want, got := txt, elem.Text(); want != got { 285 | t.Errorf("Elem for %q %q: want text %q, got %q", by, value, want, got) 286 | } 287 | } 288 | 289 | func TestFindElements(t *testing.T) { 290 | t.Parallel() 291 | wd := newRemote("TestFindElements", t).T(t) 292 | defer wd.Quit() 293 | wd.Get(serverURL) 294 | testFindElements(t, wd, ByCSSSelector, "ol.list li", []string{"foo", "bar"}) 295 | } 296 | 297 | func TestFindChildElements(t *testing.T) { 298 | t.Parallel() 299 | wd := newRemote("TestFindChildElements", t).T(t) 300 | defer wd.Quit() 301 | wd.Get(serverURL) 302 | testFindElements(t, wd.FindElement(ByCSSSelector, "ol.list"), ByCSSSelector, "li", []string{"foo", "bar"}) 303 | } 304 | 305 | func testFindElements(t *testing.T, ef elementFinder, by, value string, elemsTxt []string) { 306 | elems := ef.FindElements(by, value) 307 | if len(elems) != len(elemsTxt) { 308 | t.Fatal("Wrong number of elements %d (should be %d)", len(elems), len(elemsTxt)) 309 | } 310 | t.Logf("Found %d elements for %q %q", len(elems), by, value) 311 | for i, txt := range elemsTxt { 312 | elem := elems[i] 313 | if want, got := txt, elem.Text(); want != got { 314 | t.Errorf("Elem %d for %q %q: want text %q, got %q", i, by, value, want, got) 315 | } 316 | } 317 | } 318 | 319 | func TestSendKeys(t *testing.T) { 320 | t.Parallel() 321 | wd := newRemote("TestSendKeys", t).T(t) 322 | defer wd.Quit() 323 | 324 | wd.Get(serverURL) 325 | input := wd.FindElement(ByName, "q") 326 | input.SendKeys("golang\n") 327 | 328 | source := wd.PageSource() 329 | if !strings.Contains(source, "The Go Programming Language") { 330 | t.Fatal("Can't find Go") 331 | } 332 | if !strings.Contains(source, "golang") { 333 | t.Fatal("Can't find search query in source") 334 | } 335 | } 336 | 337 | func TestClick(t *testing.T) { 338 | t.Parallel() 339 | wd := newRemote("TestClick", t).T(t) 340 | defer wd.Quit() 341 | 342 | wd.Get(serverURL) 343 | input := wd.FindElement(ByName, "q") 344 | input.SendKeys("golang") 345 | 346 | button := wd.FindElement(ById, "submit") 347 | button.Click() 348 | 349 | if !strings.Contains(wd.PageSource(), "The Go Programming Language") { 350 | t.Fatal("Can't find Go") 351 | } 352 | } 353 | 354 | func TestClick_Hidden(t *testing.T) { 355 | t.Parallel() 356 | wd := newRemote("TestClick_Hidden", t) 357 | defer wd.Quit() 358 | 359 | if err := wd.Get(serverURL); err != nil { 360 | t.Fatal(err) 361 | } 362 | e, err := wd.FindElement(ByName, "hidden_name") 363 | if err != nil { 364 | t.Fatal(err) 365 | } 366 | err = e.Click() 367 | if err == nil { 368 | t.Fatal("expected clicking on hidden element to error") 369 | } 370 | want := "element not visible" 371 | if err.Error() != want { 372 | t.Fatalf("got error %v, want %v", err.Error(), want) 373 | } 374 | } 375 | 376 | func TestGetCookies(t *testing.T) { 377 | t.Parallel() 378 | wd := newRemote("TestGetCookies", t).T(t) 379 | defer wd.Quit() 380 | 381 | wd.Get(serverURL) 382 | cookies := wd.GetCookies() 383 | 384 | if len(cookies) == 0 { 385 | t.Fatal("No cookies") 386 | } 387 | 388 | if cookies[0].Name == "" { 389 | t.Fatal("Empty cookie") 390 | } 391 | 392 | if cookies[0].Expiry != uint(cookieExpiry.Unix()) { 393 | t.Fatalf("Bad expiry time: expected %v, got %v", cookieExpiry, cookies[0].Expiry) 394 | } 395 | } 396 | 397 | func TestAddCookie(t *testing.T) { 398 | t.Parallel() 399 | wd := newRemote("TestAddCookie", t).T(t) 400 | defer wd.Quit() 401 | 402 | wd.Get(serverURL) 403 | cookie := &Cookie{Name: "the nameless cookie", Value: "I have nothing"} 404 | wd.AddCookie(cookie) 405 | 406 | cookies := wd.GetCookies() 407 | for _, c := range cookies { 408 | if (c.Name == cookie.Name) && (c.Value == cookie.Value) { 409 | return 410 | } 411 | } 412 | 413 | t.Fatal("Can't find new cookie") 414 | } 415 | 416 | func TestDeleteAllCookies(t *testing.T) { 417 | t.Parallel() 418 | wd := newRemote("TestDeleteCookie", t).T(t) 419 | defer wd.Quit() 420 | 421 | wd.Get(serverURL) 422 | cookies := wd.GetCookies() 423 | if len(cookies) == 0 { 424 | t.Fatal("No cookies") 425 | } 426 | 427 | wd.DeleteAllCookies() 428 | 429 | newCookies := wd.GetCookies() 430 | if len(newCookies) != 0 { 431 | t.Fatal("Cookies not deleted") 432 | } 433 | } 434 | 435 | func TestDeleteCookie(t *testing.T) { 436 | t.Parallel() 437 | wd := newRemote("TestDeleteCookie", t).T(t) 438 | defer wd.Quit() 439 | 440 | wd.Get(serverURL) 441 | cookies := wd.GetCookies() 442 | if len(cookies) == 0 { 443 | t.Fatal("No cookies") 444 | } 445 | wd.DeleteCookie(cookies[0].Name) 446 | newCookies := wd.GetCookies() 447 | if len(newCookies) != len(cookies)-1 { 448 | t.Fatal("Cookie not deleted") 449 | } 450 | 451 | for _, c := range newCookies { 452 | if c.Name == cookies[0].Name { 453 | t.Fatal("Deleted cookie found") 454 | } 455 | } 456 | } 457 | 458 | func TestLocation(t *testing.T) { 459 | wd := newRemote("TestLocation", t).T(t) 460 | defer wd.Quit() 461 | 462 | wd.Get(serverURL) 463 | button := wd.FindElement(ById, "submit") 464 | 465 | loc := button.Location() 466 | 467 | if (loc.X == 0) || (loc.Y == 0) { 468 | t.Fatalf("Bad location: %v\n", loc) 469 | } 470 | } 471 | 472 | func TestLocationInView(t *testing.T) { 473 | t.Parallel() 474 | wd := newRemote("TestLocationInView", t).T(t) 475 | defer wd.Quit() 476 | 477 | wd.Get(serverURL) 478 | button := wd.FindElement(ById, "submit") 479 | 480 | loc := button.LocationInView() 481 | 482 | if (loc.X == 0) || (loc.Y == 0) { 483 | t.Fatalf("Bad location: %v\n", loc) 484 | } 485 | } 486 | 487 | func TestSize(t *testing.T) { 488 | t.Parallel() 489 | wd := newRemote("TestSize", t).T(t) 490 | defer wd.Quit() 491 | 492 | wd.Get(serverURL) 493 | button := wd.FindElement(ById, "submit") 494 | 495 | size := button.Size() 496 | 497 | if (size.Width == 0) || (size.Height == 0) { 498 | t.Fatalf("Bad size: %v\n", size) 499 | } 500 | } 501 | 502 | func TestExecuteScript(t *testing.T) { 503 | t.Parallel() 504 | wd := newRemote("TestExecuteScript", t).T(t) 505 | defer wd.Quit() 506 | 507 | script := "return arguments[0] + arguments[1]" 508 | args := []interface{}{1, 2} 509 | reply := wd.ExecuteScript(script, args) 510 | 511 | result, ok := reply.(float64) 512 | if !ok { 513 | t.Fatal("Not an int reply") 514 | } 515 | 516 | if result != 3 { 517 | t.Fatal("Bad result %d (expected 3)", result) 518 | } 519 | } 520 | 521 | func TestScreenshot(t *testing.T) { 522 | t.Parallel() 523 | wd := newRemote("TestScreenshot", t).T(t) 524 | defer wd.Quit() 525 | 526 | wd.Get(serverURL) 527 | dataReader := wd.Screenshot() 528 | 529 | data, err := ioutil.ReadAll(dataReader) 530 | if err != nil { 531 | t.Fatal("failed to read screenshot data") 532 | } 533 | 534 | if len(data) == 0 { 535 | t.Fatal("Empty reply") 536 | } 537 | } 538 | 539 | func TestIsSelected(t *testing.T) { 540 | t.Parallel() 541 | wd := newRemote("TestIsSelected", t).T(t) 542 | defer wd.Quit() 543 | 544 | wd.Get(serverURL) 545 | elem := wd.FindElement(ById, "chuk") 546 | 547 | selected := elem.IsSelected() 548 | if selected { 549 | t.Fatal("Already selected") 550 | } 551 | 552 | elem.Click() 553 | selected = elem.IsSelected() 554 | if !selected { 555 | t.Fatal("Not selected") 556 | } 557 | } 558 | 559 | // Test server 560 | 561 | var homePage = ` 562 | 563 | 564 | Go Selenium Test Suite 565 | 566 | 567 | The home page.
568 |
569 |
570 | A checkbox. 571 | 572 |
573 |
    574 |
  1. foo
  2. 575 |
  3. bar
  4. 576 |
577 |
    578 |
  1. baz
  2. 579 |
  3. qux
  4. 580 |
581 | 582 | 583 | ` 584 | 585 | var otherPage = ` 586 | 587 | 588 | Go Selenium Test Suite - Other Page 589 | 590 | 591 | The other page. 592 | 593 | 594 | ` 595 | 596 | var searchPage = ` 597 | 598 | 599 | Go Selenium Test Suite - Search Page 600 | 601 | 602 | You searched for "%s". I'll pretend I've found: 603 |

604 | "The Go Programming Language" 605 |

606 | 607 | 608 | ` 609 | 610 | var pages = map[string]string{ 611 | "/": homePage, 612 | "/other": otherPage, 613 | "/search": searchPage, 614 | } 615 | 616 | var cookieExpiry = time.Now().Add(1 * time.Hour).UTC() 617 | 618 | func handler(w http.ResponseWriter, r *http.Request) { 619 | path := r.URL.Path 620 | page, ok := pages[path] 621 | if !ok { 622 | http.NotFound(w, r) 623 | return 624 | } 625 | 626 | if path == "/search" { 627 | r.ParseForm() 628 | page = fmt.Sprintf(page, r.Form["q"][0]) 629 | } 630 | // Some cookies for the tests 631 | for i := 0; i < 3; i++ { 632 | name := fmt.Sprintf("cookie-%d", i) 633 | value := fmt.Sprintf("value-%d", i) 634 | http.SetCookie(w, &http.Cookie{Name: name, Value: value, Expires: cookieExpiry}) 635 | } 636 | 637 | fmt.Fprintf(w, page) 638 | } 639 | 640 | var serverPort = ":4793" 641 | var serverURL = "http://localhost" + serverPort + "/" 642 | 643 | func init() { 644 | go func() { 645 | http.HandleFunc("/", handler) 646 | http.ListenAndServe(serverPort, nil) 647 | }() 648 | } 649 | -------------------------------------------------------------------------------- /selenium.go: -------------------------------------------------------------------------------- 1 | package selenium // import "sourcegraph.com/sourcegraph/go-selenium" 2 | import "context" 3 | 4 | import "io" 5 | 6 | /* Element finding options */ 7 | const ( 8 | ById = "id" 9 | ByXPATH = "xpath" 10 | ByLinkText = "link text" 11 | ByPartialLinkText = "partial link text" 12 | ByName = "name" 13 | ByTagName = "tag name" 14 | ByClassName = "class name" 15 | ByCSSSelector = "css selector" 16 | ) 17 | 18 | /* Mouse buttons */ 19 | const ( 20 | LeftButton = iota 21 | MiddleButton 22 | RightButton 23 | ) 24 | 25 | /* Keys */ 26 | const ( 27 | NullKey = string('\ue000') 28 | CancelKey = string('\ue001') 29 | HelpKey = string('\ue002') 30 | BackspaceKey = string('\ue003') 31 | TabKey = string('\ue004') 32 | ClearKey = string('\ue005') 33 | ReturnKey = string('\ue006') 34 | EnterKey = string('\ue007') 35 | ShiftKey = string('\ue008') 36 | ControlKey = string('\ue009') 37 | AltKey = string('\ue00a') 38 | PauseKey = string('\ue00b') 39 | EscapeKey = string('\ue00c') 40 | SpaceKey = string('\ue00d') 41 | PageUpKey = string('\ue00e') 42 | PageDownKey = string('\ue00f') 43 | EndKey = string('\ue010') 44 | HomeKey = string('\ue011') 45 | LeftArrowKey = string('\ue012') 46 | UpArrowKey = string('\ue013') 47 | RightArrowKey = string('\ue014') 48 | DownArrowKey = string('\ue015') 49 | InsertKey = string('\ue016') 50 | DeleteKey = string('\ue017') 51 | SemicolonKey = string('\ue018') 52 | EqualsKey = string('\ue019') 53 | Numpad0Key = string('\ue01a') 54 | Numpad1Key = string('\ue01b') 55 | Numpad2Key = string('\ue01c') 56 | Numpad3Key = string('\ue01d') 57 | Numpad4Key = string('\ue01e') 58 | Numpad5Key = string('\ue01f') 59 | Numpad6Key = string('\ue020') 60 | Numpad7Key = string('\ue021') 61 | Numpad8Key = string('\ue022') 62 | Numpad9Key = string('\ue023') 63 | MultiplyKey = string('\ue024') 64 | AddKey = string('\ue025') 65 | SeparatorKey = string('\ue026') 66 | SubstractKey = string('\ue027') 67 | DecimalKey = string('\ue028') 68 | DivideKey = string('\ue029') 69 | F1Key = string('\ue031') 70 | F2Key = string('\ue032') 71 | F3Key = string('\ue033') 72 | F4Key = string('\ue034') 73 | F5Key = string('\ue035') 74 | F6Key = string('\ue036') 75 | F7Key = string('\ue037') 76 | F8Key = string('\ue038') 77 | F9Key = string('\ue039') 78 | F10Key = string('\ue03a') 79 | F11Key = string('\ue03b') 80 | F12Key = string('\ue03c') 81 | MetaKey = string('\ue03d') 82 | ) 83 | 84 | /* Browser capabilities, see 85 | http://code.google.com/p/selenium/wiki/JsonWireProtocol#Capabilities_JSON_Object 86 | */ 87 | type Capabilities map[string]interface{} 88 | 89 | /* Build object, part of Status return. */ 90 | type Build struct { 91 | Version, Revision, Time string 92 | } 93 | 94 | /* OS object, part of Status return. */ 95 | type OS struct { 96 | Arch, Name, Version string 97 | } 98 | 99 | /* Information retured by Status method. */ 100 | type Status struct { 101 | Build `json:"build"` 102 | OS `json:"os"` 103 | } 104 | 105 | /* Point */ 106 | type Point struct { 107 | X, Y float64 108 | } 109 | 110 | /* Size */ 111 | type Size struct { 112 | Width float64 `json:"width"` 113 | Height float64 `json:"height"` 114 | } 115 | 116 | /* Cookie */ 117 | type Cookie struct { 118 | Name string `json:"name"` 119 | Value string `json:"value"` 120 | Path string `json:"path"` 121 | Domain string `json:"domain"` 122 | Secure bool `json:"secure"` 123 | Expiry uint `json:"-"` 124 | } 125 | 126 | type WebDriver interface { 127 | SetContext(context.Context) 128 | 129 | /* Status (info) on server */ 130 | Status() (*Status, error) 131 | 132 | /* List of actions on the server. */ 133 | Sessions() ([]Session, error) 134 | 135 | /* Start a new session, return session id */ 136 | NewSession() (string, error) 137 | 138 | /* Current session capabilities */ 139 | Capabilities() (Capabilities, error) 140 | 141 | /* Configure the amount of time a particular type of operation can execute for before it is aborted. 142 | Valid types: "script" for script timeouts, "implicit" for modifying the implicit wait timeout and "page load" for setting a page load timeout. */ 143 | SetTimeout(timeoutType string, ms uint) error 144 | /* Set the amount of time, in milliseconds, that asynchronous scripts are permitted to run before they are aborted. */ 145 | SetAsyncScriptTimeout(ms uint) error 146 | /* Set the amount of time, in milliseconds, the driver should wait when searching for elements. */ 147 | SetImplicitWaitTimeout(ms uint) error 148 | 149 | // IME 150 | /* List all available engines on the machine. */ 151 | AvailableEngines() ([]string, error) 152 | /* Get the name of the active IME engine. */ 153 | ActiveEngine() (string, error) 154 | /* Indicates whether IME input is active at the moment. */ 155 | IsEngineActivated() (bool, error) 156 | /* De-activates the currently-active IME engine. */ 157 | DeactivateEngine() error 158 | /* Make an engines active */ 159 | ActivateEngine(engine string) error 160 | 161 | /* Quit (end) current session */ 162 | Quit() error 163 | 164 | // Page information and manipulation 165 | /* Return id of current window handle. */ 166 | CurrentWindowHandle() (string, error) 167 | /* Return ids of current open windows. */ 168 | WindowHandles() ([]string, error) 169 | /* Current url. */ 170 | CurrentURL() (string, error) 171 | /* Page title. */ 172 | Title() (string, error) 173 | /* Get page source. */ 174 | PageSource() (string, error) 175 | /* Close current window. */ 176 | Close() error 177 | /* Switch to frame, frame parameter can be name or id. */ 178 | SwitchFrame(frame string) error 179 | /* Switch to parent frame */ 180 | SwitchFrameParent() error 181 | /* Swtich to window. */ 182 | SwitchWindow(name string) error 183 | /* Close window. */ 184 | CloseWindow(name string) error 185 | /* Get window size */ 186 | WindowSize(name string) (*Size, error) 187 | /* Get window position */ 188 | WindowPosition(name string) (*Point, error) 189 | 190 | // ResizeWindow resizes the named window. 191 | ResizeWindow(name string, to Size) error 192 | 193 | // Navigation 194 | /* Open url. */ 195 | Get(url string) error 196 | /* Move forward in history. */ 197 | Forward() error 198 | /* Move backward in history. */ 199 | Back() error 200 | /* Refresh page. */ 201 | Refresh() error 202 | 203 | // Finding element(s) 204 | /* Find, return one element. */ 205 | FindElement(by, value string) (WebElement, error) 206 | /* Find, return list of elements. */ 207 | FindElements(by, value string) ([]WebElement, error) 208 | /* Current active element. */ 209 | ActiveElement() (WebElement, error) 210 | 211 | // Shortcut for FindElement(ByCSSSelector, sel) 212 | Q(sel string) (WebElement, error) 213 | // Shortcut for FindElements(ByCSSSelector, sel) 214 | QAll(sel string) ([]WebElement, error) 215 | 216 | // Cookies 217 | /* Get all cookies */ 218 | GetCookies() ([]Cookie, error) 219 | /* Add a cookie */ 220 | AddCookie(cookie *Cookie) error 221 | /* Delete all cookies */ 222 | DeleteAllCookies() error 223 | /* Delete a cookie */ 224 | DeleteCookie(name string) error 225 | 226 | // Mouse 227 | /* Click mouse button, button should be on of RightButton, MiddleButton or 228 | LeftButton. 229 | */ 230 | Click(button int) error 231 | /* Dobule click */ 232 | DoubleClick() error 233 | /* Mouse button down */ 234 | ButtonDown() error 235 | /* Mouse button up */ 236 | ButtonUp() error 237 | 238 | // Misc 239 | /* Send modifier key to active element. 240 | modifier can be one of ShiftKey, ControlKey, AltKey, MetaKey. 241 | */ 242 | SendModifier(modifier string, isDown bool) error 243 | Screenshot() (io.Reader, error) 244 | 245 | // Alerts 246 | /* Dismiss current alert. */ 247 | DismissAlert() error 248 | /* Accept current alert. */ 249 | AcceptAlert() error 250 | /* Current alert text. */ 251 | AlertText() (string, error) 252 | /* Set current alert text. */ 253 | SetAlertText(text string) error 254 | 255 | // Scripts 256 | /* Execute a script. */ 257 | ExecuteScript(script string, args []interface{}) (interface{}, error) 258 | /* Execute a script async. */ 259 | ExecuteScriptAsync(script string, args []interface{}) (interface{}, error) 260 | 261 | // Get a WebDriverT of this element that has methods that call t.Fatalf upon 262 | // encountering errors instead of using multiple returns to indicate errors. 263 | // The argument t is typically a *testing.T, but here it's a similar 264 | // interface to avoid needing to import "testing" (which registers global 265 | // command-line flags). 266 | T(t TestingT) WebDriverT 267 | } 268 | 269 | type WebElement interface { 270 | // Manipulation 271 | 272 | /* Click on element */ 273 | Click() error 274 | /* Send keys (type) into element */ 275 | SendKeys(keys string) error 276 | /* Submit */ 277 | Submit() error 278 | /* Clear */ 279 | Clear() error 280 | /* Move mouse to relative coordinates */ 281 | MoveTo(xOffset, yOffset int) error 282 | 283 | // Finding 284 | 285 | /* Find children, return one element. */ 286 | FindElement(by, value string) (WebElement, error) 287 | /* Find children, return list of elements. */ 288 | FindElements(by, value string) ([]WebElement, error) 289 | 290 | // Shortcut for FindElement(ByCSSSelector, sel) 291 | Q(sel string) (WebElement, error) 292 | // Shortcut for FindElements(ByCSSSelector, sel) 293 | QAll(sel string) ([]WebElement, error) 294 | 295 | // Porperties 296 | 297 | /* Element name */ 298 | TagName() (string, error) 299 | /* Text of element */ 300 | Text() (string, error) 301 | /* Check if element is selected. */ 302 | IsSelected() (bool, error) 303 | /* Check if element is enabled. */ 304 | IsEnabled() (bool, error) 305 | /* Check if element is displayed. */ 306 | IsDisplayed() (bool, error) 307 | /* Get element attribute. */ 308 | GetAttribute(name string) (string, error) 309 | /* Element location. */ 310 | Location() (*Point, error) 311 | /* Element location once it has been scrolled into view. 312 | Note: This is considered an internal command and should only be used to determine an element's location for correctly generating native events.*/ 313 | LocationInView() (*Point, error) 314 | /* Element size */ 315 | Size() (*Size, error) 316 | /* Get element CSS property value. */ 317 | CSSProperty(name string) (string, error) 318 | 319 | // Get a WebElementT of this element that has methods that call t.Fatalf 320 | // upon encountering errors instead of using multiple returns to indicate 321 | // errors. The argument t is typically a *testing.T, but here it's a similar 322 | // interface to avoid needing to import "testing" (which registers global 323 | // command-line flags). 324 | T(t TestingT) WebElementT 325 | } 326 | 327 | // TestingT is a subset of the testing.T interface (to avoid needing 328 | // to import "testing", which registers global command-line flags). 329 | type TestingT interface { 330 | Fatalf(fmt string, v ...interface{}) 331 | } 332 | -------------------------------------------------------------------------------- /selenium_test.go: -------------------------------------------------------------------------------- 1 | package selenium 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | var ( 11 | // mux is the HTTP request multiplexer used with the test server. 12 | mux *http.ServeMux 13 | 14 | // client is the Selenium client being tested. 15 | client WebDriver 16 | 17 | // server is a test HTTP server used to provide mock API responses. 18 | server *httptest.Server 19 | ) 20 | 21 | // setup sets up a test HTTP server along with a WebDriver that is 22 | // configured to talk to that test server. Tests should register handlers on 23 | // mux which provide mock responses for the API method being tested. 24 | func setup() { 25 | // test server 26 | mux = http.NewServeMux() 27 | server = httptest.NewServer(mux) 28 | 29 | mux.HandleFunc("/session", func(w http.ResponseWriter, r *http.Request) { 30 | fmt.Fprintf(w, `{"sessionId": "123"}`) 31 | }) 32 | 33 | // selenium client configured to use test server 34 | var err error 35 | client, err = NewRemote(caps, server.URL) 36 | if err != nil { 37 | panic("NewRemote: " + err.Error()) 38 | } 39 | } 40 | 41 | // teardown closes the test HTTP server. 42 | func teardown() { 43 | server.Close() 44 | } 45 | 46 | func testMethod(t *testing.T, r *http.Request, want string) { 47 | if want != r.Method { 48 | t.Errorf("Request method = %v, want %v", r.Method, want) 49 | } 50 | } 51 | 52 | func testHeader(t *testing.T, r *http.Request, header string, want string) { 53 | if value := r.Header.Get(header); want != value { 54 | t.Errorf("Header %s = %s, want: %s", header, value, want) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test_helpers.go: -------------------------------------------------------------------------------- 1 | package selenium 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "path/filepath" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // A single-return-value interface to WebDriverT that is useful when using WebDrivers in test code. 13 | // Obtain a WebDriverT by calling webDriver.T(t), where t *testing.T is the test handle for the 14 | // current test. The methods of WebDriverT call wt.t.Fatalf upon encountering errors instead of using 15 | // multiple returns to indicate errors. 16 | type WebDriverT interface { 17 | WebDriver() WebDriver 18 | 19 | NewSession() string 20 | 21 | SetTimeout(timeoutType string, ms uint) 22 | SetAsyncScriptTimeout(ms uint) 23 | SetImplicitWaitTimeout(ms uint) 24 | 25 | Quit() 26 | 27 | CurrentWindowHandle() string 28 | WindowHandles() []string 29 | CurrentURL() string 30 | Title() string 31 | PageSource() string 32 | Close() 33 | SwitchFrame(frame string) 34 | SwitchFrameParent() 35 | SwitchWindow(name string) 36 | CloseWindow(name string) 37 | WindowSize(name string) *Size 38 | WindowPosition(name string) *Point 39 | ResizeWindow(name string, to Size) 40 | 41 | Get(url string) 42 | Forward() 43 | Back() 44 | Refresh() 45 | 46 | FindElement(by, value string) WebElementT 47 | FindElements(by, value string) []WebElementT 48 | ActiveElement() WebElement 49 | 50 | // Shortcut for FindElement(ByCSSSelector, sel) 51 | Q(sel string) WebElementT 52 | // Shortcut for FindElements(ByCSSSelector, sel) 53 | QAll(sel string) []WebElementT 54 | 55 | GetCookies() []Cookie 56 | AddCookie(cookie *Cookie) 57 | DeleteAllCookies() 58 | DeleteCookie(name string) 59 | 60 | Click(button int) 61 | DoubleClick() 62 | ButtonDown() 63 | ButtonUp() 64 | 65 | SendModifier(modifier string, isDown bool) 66 | Screenshot() io.Reader 67 | 68 | DismissAlert() 69 | AcceptAlert() 70 | AlertText() string 71 | SetAlertText(text string) 72 | 73 | ExecuteScript(script string, args []interface{}) interface{} 74 | ExecuteScriptAsync(script string, args []interface{}) interface{} 75 | } 76 | 77 | type webDriverT struct { 78 | d WebDriver 79 | t TestingT 80 | } 81 | 82 | func (wt *webDriverT) WebDriver() WebDriver { 83 | return wt.d 84 | } 85 | 86 | func (wt *webDriverT) NewSession() (id string) { 87 | var err error 88 | if id, err = wt.d.NewSession(); err != nil { 89 | fatalf(wt.t, "NewSession: %s", err) 90 | } 91 | return 92 | } 93 | 94 | func (wt *webDriverT) SetTimeout(timeoutType string, ms uint) { 95 | if err := wt.d.SetTimeout(timeoutType, ms); err != nil { 96 | fatalf(wt.t, "SetTimeout(timeoutType=%q, ms=%d): %s", timeoutType, ms, err) 97 | } 98 | } 99 | 100 | func (wt *webDriverT) SetAsyncScriptTimeout(ms uint) { 101 | if err := wt.d.SetAsyncScriptTimeout(ms); err != nil { 102 | fatalf(wt.t, "SetAsyncScriptTimeout(%d msec): %s", ms, err) 103 | } 104 | } 105 | 106 | func (wt *webDriverT) SetImplicitWaitTimeout(ms uint) { 107 | if err := wt.d.SetImplicitWaitTimeout(ms); err != nil { 108 | fatalf(wt.t, "SetImplicitWaitTimeout(%d msec): %s", ms, err) 109 | } 110 | } 111 | 112 | func (wt *webDriverT) Quit() { 113 | if err := wt.d.Quit(); err != nil { 114 | fatalf(wt.t, "Quit: %s", err) 115 | } 116 | } 117 | 118 | func (wt *webDriverT) CurrentWindowHandle() (v string) { 119 | var err error 120 | if v, err = wt.d.CurrentWindowHandle(); err != nil { 121 | fatalf(wt.t, "CurrentWindowHandle: %s", err) 122 | } 123 | return 124 | } 125 | 126 | func (wt *webDriverT) WindowHandles() (hs []string) { 127 | var err error 128 | if hs, err = wt.d.WindowHandles(); err != nil { 129 | fatalf(wt.t, "WindowHandles: %s", err) 130 | } 131 | return 132 | } 133 | 134 | func (wt *webDriverT) CurrentURL() (v string) { 135 | var err error 136 | if v, err = wt.d.CurrentURL(); err != nil { 137 | fatalf(wt.t, "CurrentURL: %s", err) 138 | } 139 | return 140 | } 141 | 142 | func (wt *webDriverT) Title() (v string) { 143 | var err error 144 | if v, err = wt.d.Title(); err != nil { 145 | fatalf(wt.t, "Title: %s", err) 146 | } 147 | return 148 | } 149 | 150 | func (wt *webDriverT) PageSource() (v string) { 151 | var err error 152 | if v, err = wt.d.PageSource(); err != nil { 153 | fatalf(wt.t, "PageSource: %s", err) 154 | } 155 | return 156 | } 157 | 158 | func (wt *webDriverT) Close() { 159 | if err := wt.d.Close(); err != nil { 160 | fatalf(wt.t, "Close: %s", err) 161 | } 162 | } 163 | 164 | func (wt *webDriverT) SwitchFrame(frame string) { 165 | if err := wt.d.SwitchFrame(frame); err != nil { 166 | fatalf(wt.t, "SwitchFrame(%q): %s", frame, err) 167 | } 168 | } 169 | 170 | func (wt *webDriverT) SwitchFrameParent() { 171 | if err := wt.d.SwitchFrameParent(); err != nil { 172 | fatalf(wt.t, "SwitchFrameParent(): %s", err) 173 | } 174 | } 175 | 176 | func (wt *webDriverT) SwitchWindow(name string) { 177 | if err := wt.d.SwitchWindow(name); err != nil { 178 | fatalf(wt.t, "SwitchWindow(%q): %s", name, err) 179 | } 180 | } 181 | 182 | func (wt *webDriverT) CloseWindow(name string) { 183 | if err := wt.d.CloseWindow(name); err != nil { 184 | fatalf(wt.t, "CloseWindow(%q): %s", name, err) 185 | } 186 | } 187 | 188 | func (wt *webDriverT) WindowSize(name string) *Size { 189 | sz, err := wt.d.WindowSize(name) 190 | if err != nil { 191 | fatalf(wt.t, "WindowSize(%q): %s", name, err) 192 | } 193 | return sz 194 | } 195 | 196 | func (wt *webDriverT) WindowPosition(name string) *Point { 197 | pt, err := wt.d.WindowPosition(name) 198 | if err != nil { 199 | fatalf(wt.t, "WindowPosition(%q): %s", name, err) 200 | } 201 | return pt 202 | } 203 | 204 | func (wt *webDriverT) ResizeWindow(name string, to Size) { 205 | if err := wt.d.ResizeWindow(name, to); err != nil { 206 | fatalf(wt.t, "ResizeWindow(%s, %+v): %s", name, to, err) 207 | } 208 | } 209 | 210 | func (wt *webDriverT) Get(name string) { 211 | if err := wt.d.Get(name); err != nil { 212 | fatalf(wt.t, "Get(%q): %s", name, err) 213 | } 214 | } 215 | 216 | func (wt *webDriverT) Forward() { 217 | if err := wt.d.Forward(); err != nil { 218 | fatalf(wt.t, "Forward: %s", err) 219 | } 220 | } 221 | 222 | func (wt *webDriverT) Back() { 223 | if err := wt.d.Back(); err != nil { 224 | fatalf(wt.t, "Back: %s", err) 225 | } 226 | } 227 | 228 | func (wt *webDriverT) Refresh() { 229 | if err := wt.d.Refresh(); err != nil { 230 | fatalf(wt.t, "Refresh: %s", err) 231 | } 232 | } 233 | 234 | func (wt *webDriverT) FindElement(by, value string) (elem WebElementT) { 235 | if elem_, err := wt.d.FindElement(by, value); err == nil { 236 | elem = elem_.T(wt.t) 237 | } else { 238 | fatalf(wt.t, "FindElement(by=%q, value=%q): %s", by, value, err) 239 | } 240 | return 241 | } 242 | 243 | func (wt *webDriverT) FindElements(by, value string) (elems []WebElementT) { 244 | if elems_, err := wt.d.FindElements(by, value); err == nil { 245 | for _, elem := range elems_ { 246 | elems = append(elems, elem.T(wt.t)) 247 | } 248 | } else { 249 | fatalf(wt.t, "FindElements(by=%q, value=%q): %s", by, value, err) 250 | } 251 | return 252 | } 253 | 254 | func (wt *webDriverT) Q(sel string) (elem WebElementT) { 255 | return wt.FindElement(ByCSSSelector, sel) 256 | } 257 | 258 | func (wt *webDriverT) QAll(sel string) (elems []WebElementT) { 259 | return wt.FindElements(ByCSSSelector, sel) 260 | } 261 | 262 | func (wt *webDriverT) ActiveElement() (elem WebElement) { 263 | var err error 264 | if elem, err = wt.d.ActiveElement(); err != nil { 265 | fatalf(wt.t, "ActiveElement: %s", err) 266 | } 267 | return 268 | } 269 | 270 | func (wt *webDriverT) GetCookies() (c []Cookie) { 271 | var err error 272 | if c, err = wt.d.GetCookies(); err != nil { 273 | fatalf(wt.t, "GetCookies: %s", err) 274 | } 275 | return 276 | } 277 | 278 | func (wt *webDriverT) AddCookie(cookie *Cookie) { 279 | if err := wt.d.AddCookie(cookie); err != nil { 280 | fatalf(wt.t, "AddCookie(%+q): %s", cookie, err) 281 | } 282 | return 283 | } 284 | 285 | func (wt *webDriverT) DeleteAllCookies() { 286 | if err := wt.d.DeleteAllCookies(); err != nil { 287 | fatalf(wt.t, "DeleteAllCookies: %s", err) 288 | } 289 | } 290 | 291 | func (wt *webDriverT) DeleteCookie(name string) { 292 | if err := wt.d.DeleteCookie(name); err != nil { 293 | fatalf(wt.t, "DeleteCookie(%q): %s", name, err) 294 | } 295 | } 296 | 297 | func (wt *webDriverT) Click(button int) { 298 | if err := wt.d.Click(button); err != nil { 299 | fatalf(wt.t, "Click(%d): %s", button, err) 300 | } 301 | } 302 | 303 | func (wt *webDriverT) DoubleClick() { 304 | if err := wt.d.DoubleClick(); err != nil { 305 | fatalf(wt.t, "DoubleClick: %s", err) 306 | } 307 | } 308 | 309 | func (wt *webDriverT) ButtonDown() { 310 | if err := wt.d.ButtonDown(); err != nil { 311 | fatalf(wt.t, "ButtonDown: %s", err) 312 | } 313 | } 314 | 315 | func (wt *webDriverT) ButtonUp() { 316 | if err := wt.d.ButtonUp(); err != nil { 317 | fatalf(wt.t, "ButtonUp: %s", err) 318 | } 319 | } 320 | 321 | func (wt *webDriverT) SendModifier(modifier string, isDown bool) { 322 | if err := wt.d.SendModifier(modifier, isDown); err != nil { 323 | fatalf(wt.t, "SendModifier(modifier=%q, isDown=%s): %s", modifier, isDown, err) 324 | } 325 | } 326 | 327 | func (wt *webDriverT) Screenshot() (data io.Reader) { 328 | var err error 329 | if data, err = wt.d.Screenshot(); err != nil { 330 | fatalf(wt.t, "Screenshot: %s", err) 331 | } 332 | return 333 | } 334 | 335 | func (wt *webDriverT) DismissAlert() { 336 | if err := wt.d.DismissAlert(); err != nil { 337 | fatalf(wt.t, "DismissAlert: %s", err) 338 | } 339 | } 340 | 341 | func (wt *webDriverT) AcceptAlert() { 342 | if err := wt.d.AcceptAlert(); err != nil { 343 | fatalf(wt.t, "AcceptAlert: %s", err) 344 | } 345 | } 346 | 347 | func (wt *webDriverT) AlertText() (text string) { 348 | var err error 349 | if text, err = wt.d.AlertText(); err != nil { 350 | fatalf(wt.t, "AlertText: %s", err) 351 | } 352 | return 353 | } 354 | 355 | func (wt *webDriverT) SetAlertText(text string) { 356 | var err error 357 | if err = wt.d.SetAlertText(text); err != nil { 358 | fatalf(wt.t, "SetAlertText(%q): %s", text, err) 359 | } 360 | } 361 | 362 | func (wt *webDriverT) ExecuteScript(script string, args []interface{}) (res interface{}) { 363 | var err error 364 | if res, err = wt.d.ExecuteScript(script, args); err != nil { 365 | fatalf(wt.t, "ExecuteScript(script=%q, args=%+q): %s", script, args, err) 366 | } 367 | return 368 | } 369 | 370 | func (wt *webDriverT) ExecuteScriptAsync(script string, args []interface{}) (res interface{}) { 371 | var err error 372 | if res, err = wt.d.ExecuteScriptAsync(script, args); err != nil { 373 | fatalf(wt.t, "ExecuteScriptAsync(script=%q, args=%+q): %s", script, args, err) 374 | } 375 | return 376 | } 377 | 378 | // A single-return-value interface to WebElement that is useful when using WebElements in test code. 379 | // Obtain a WebElementT by calling webElement.T(t), where t *testing.T is the test handle for the 380 | // current test. The methods of WebElementT call wt.fatalf upon encountering errors instead of using 381 | // multiple returns to indicate errors. 382 | type WebElementT interface { 383 | WebElement() WebElement 384 | 385 | Click() 386 | SendKeys(keys string) 387 | Submit() 388 | Clear() 389 | MoveTo(xOffset, yOffset int) 390 | 391 | FindElement(by, value string) WebElementT 392 | FindElements(by, value string) []WebElementT 393 | 394 | // Shortcut for FindElement(ByCSSSelector, sel) 395 | Q(sel string) WebElementT 396 | // Shortcut for FindElements(ByCSSSelector, sel) 397 | QAll(sel string) []WebElementT 398 | 399 | TagName() string 400 | Text() string 401 | IsSelected() bool 402 | IsEnabled() bool 403 | IsDisplayed() bool 404 | GetAttribute(name string) string 405 | Location() *Point 406 | LocationInView() *Point 407 | Size() *Size 408 | CSSProperty(name string) string 409 | } 410 | 411 | type webElementT struct { 412 | e WebElement 413 | t TestingT 414 | } 415 | 416 | func (wt *webElementT) WebElement() WebElement { 417 | return wt.e 418 | } 419 | 420 | func (wt *webElementT) Click() { 421 | if err := wt.e.Click(); err != nil { 422 | fatalf(wt.t, "Click: %s", err) 423 | } 424 | } 425 | 426 | func (wt *webElementT) SendKeys(keys string) { 427 | if err := wt.e.SendKeys(keys); err != nil { 428 | fatalf(wt.t, "SendKeys(%q): %s", keys, err) 429 | } 430 | } 431 | 432 | func (wt *webElementT) Submit() { 433 | if err := wt.e.Submit(); err != nil { 434 | fatalf(wt.t, "Submit: %s", err) 435 | } 436 | } 437 | 438 | func (wt *webElementT) Clear() { 439 | if err := wt.e.Clear(); err != nil { 440 | fatalf(wt.t, "Clear: %s", err) 441 | } 442 | } 443 | 444 | func (wt *webElementT) MoveTo(xOffset, yOffset int) { 445 | if err := wt.e.MoveTo(xOffset, yOffset); err != nil { 446 | fatalf(wt.t, "MoveTo(xOffset=%d, yOffset=%d): %s", xOffset, yOffset, err) 447 | } 448 | } 449 | 450 | func (wt *webElementT) FindElement(by, value string) WebElementT { 451 | if elem, err := wt.e.FindElement(by, value); err == nil { 452 | return elem.T(wt.t) 453 | } else { 454 | fatalf(wt.t, "FindElement(by=%q, value=%q): %s", by, value, err) 455 | panic("unreachable") 456 | } 457 | } 458 | 459 | func (wt *webElementT) FindElements(by, value string) []WebElementT { 460 | if elems, err := wt.e.FindElements(by, value); err == nil { 461 | elemsT := make([]WebElementT, len(elems)) 462 | for i, elem := range elems { 463 | elemsT[i] = elem.T(wt.t) 464 | } 465 | return elemsT 466 | } else { 467 | fatalf(wt.t, "FindElements(by=%q, value=%q): %s", by, value, err) 468 | panic("unreachable") 469 | } 470 | } 471 | 472 | func (wt *webElementT) Q(sel string) (elem WebElementT) { 473 | return wt.FindElement(ByCSSSelector, sel) 474 | } 475 | 476 | func (wt *webElementT) QAll(sel string) (elems []WebElementT) { 477 | return wt.FindElements(ByCSSSelector, sel) 478 | } 479 | 480 | func (wt *webElementT) TagName() (v string) { 481 | var err error 482 | if v, err = wt.e.TagName(); err != nil { 483 | fatalf(wt.t, "TagName: %s", err) 484 | } 485 | return 486 | } 487 | 488 | func (wt *webElementT) Text() (v string) { 489 | var err error 490 | if v, err = wt.e.Text(); err != nil { 491 | fatalf(wt.t, "Text: %s", err) 492 | } 493 | return 494 | } 495 | 496 | func (wt *webElementT) IsSelected() (v bool) { 497 | var err error 498 | if v, err = wt.e.IsSelected(); err != nil { 499 | fatalf(wt.t, "IsSelected: %s", err) 500 | } 501 | return 502 | } 503 | 504 | func (wt *webElementT) IsEnabled() (v bool) { 505 | var err error 506 | if v, err = wt.e.IsEnabled(); err != nil { 507 | fatalf(wt.t, "IsEnabled: %s", err) 508 | } 509 | return 510 | } 511 | 512 | func (wt *webElementT) IsDisplayed() (v bool) { 513 | var err error 514 | if v, err = wt.e.IsDisplayed(); err != nil { 515 | fatalf(wt.t, "IsDisplayed: %s", err) 516 | } 517 | return 518 | } 519 | 520 | func (wt *webElementT) GetAttribute(name string) (v string) { 521 | var err error 522 | if v, err = wt.e.GetAttribute(name); err != nil { 523 | fatalf(wt.t, "GetAttribute(%q): %s", name, err) 524 | } 525 | return 526 | } 527 | 528 | func (wt *webElementT) Location() (v *Point) { 529 | var err error 530 | if v, err = wt.e.Location(); err != nil { 531 | fatalf(wt.t, "Location: %s", err) 532 | } 533 | return 534 | } 535 | 536 | func (wt *webElementT) LocationInView() (v *Point) { 537 | var err error 538 | if v, err = wt.e.LocationInView(); err != nil { 539 | fatalf(wt.t, "LocationInView: %s", err) 540 | } 541 | return 542 | } 543 | 544 | func (wt *webElementT) Size() (v *Size) { 545 | var err error 546 | if v, err = wt.e.Size(); err != nil { 547 | fatalf(wt.t, "Size: %s", err) 548 | } 549 | return 550 | } 551 | 552 | func (wt *webElementT) CSSProperty(name string) (v string) { 553 | var err error 554 | if v, err = wt.e.CSSProperty(name); err != nil { 555 | fatalf(wt.t, "CSSProperty(%q): %s", name, err) 556 | } 557 | return 558 | } 559 | 560 | func fatalf(t TestingT, fmtStr string, v ...interface{}) { 561 | // Backspace (delete) the file and line that t.Fatalf will add 562 | // that points to *this* invocation and replace it with that of 563 | // invocation of the webDriverT/webElementT method. 564 | _, thisFile, thisLine, _ := runtime.Caller(1) 565 | undoThisPrefix := strings.Repeat("\x08", len(fmt.Sprintf("%s:%d: ", filepath.Base(thisFile), thisLine))) 566 | _, file, line, _ := runtime.Caller(5) 567 | t.Fatalf(undoThisPrefix+filepath.Base(file)+":"+strconv.Itoa(line)+": "+fmtStr, v...) 568 | } 569 | --------------------------------------------------------------------------------