├── .gitignore ├── LICENSE ├── README.md ├── chromedriver.go ├── common.go ├── doc.go ├── firefoxdriver.go ├── utils.go ├── webdriver.go └── webdriver_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Federico Sogaro 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is not maintained and it is effectively abandoned, if you have interest in this code you should probably clone it. 2 | 3 | webdriver 4 | ========= 5 | 6 | The package implements a WebDriver that communicate with a browser using the JSON Wire Protocol (See https://code.google.com/p/selenium/wiki/JsonWireProtocol). 7 | This is a pure go library and doesn't require a running Selenium driver. It currently supports Firefox (using the WebDriver extension) and Chrome (using the standalone server chromedriver). It should be fairly easy to add other browser that directly implement the wire protocol. 8 | 9 | **Version: 0.1** 10 | Tests are partial and have been run only on Linux (with firefox webdriver 2.32.0 and chromedriver 2.1). 11 | 12 | **Install:** 13 | $ go get github.com/fedesog/webdriver 14 | 15 | **Requires:** 16 | * chromedriver (for chrome): 17 | https://code.google.com/p/chromedriver/ 18 | * webdriver.xpi (for firefox): that is founds in the selenium-server-standalone file 19 | https://code.google.com/p/selenium/ 20 | 21 | 22 | Example: 23 | -------- 24 | 25 | chromeDriver := webdriver.NewChromeDriver("/path/to/chromedriver") 26 | err := chromeDriver.Start() 27 | if err != nil { 28 | log.Println(err) 29 | } 30 | desired := webdriver.Capabilities{"Platform": "Linux"} 31 | required := webdriver.Capabilities{} 32 | session, err := chromeDriver.NewSession(desired, required) 33 | if err != nil { 34 | log.Println(err) 35 | } 36 | err = session.Url("http://golang.org") 37 | if err != nil { 38 | log.Println(err) 39 | } 40 | time.Sleep(10 * time.Second) 41 | session.Delete() 42 | chromeDriver.Stop() 43 | 44 | -------------------------------------------------------------------------------- /chromedriver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Federico Sogaro. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package webdriver 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | type ChromeSwitches map[string]interface{} 18 | 19 | type ChromeDriver struct { 20 | WebDriverCore 21 | //The port that ChromeDriver listens on. Default: 9515 22 | Port int 23 | //The URL path prefix to use for all incoming WebDriver REST requests. Default: "" 24 | BaseUrl string 25 | //The number of threads to use for handling HTTP requests. Default: 4 26 | Threads int 27 | //The path to use for the ChromeDriver server log. Default: ./chromedriver.log 28 | LogPath string 29 | // Log file to dump chromedriver stdout/stderr. If "" send to terminal. Default: "" 30 | LogFile string 31 | // Start method fails if Chromedriver doesn't start in less than StartTimeout. Default 20s. 32 | StartTimeout time.Duration 33 | 34 | path string 35 | cmd *exec.Cmd 36 | logFile *os.File 37 | } 38 | 39 | //create a new service using chromedriver. 40 | //function returns an error if not supported switches are passed. Actual content 41 | //of valid-named switches is not validate and is passed as it is. 42 | //switch silent is removed (output is needed to check if chromedriver started correctly) 43 | func NewChromeDriver(path string) *ChromeDriver { 44 | d := &ChromeDriver{} 45 | d.path = path 46 | d.Port = 9515 47 | d.BaseUrl = "" 48 | d.Threads = 4 49 | d.LogPath = "chromedriver.log" 50 | d.StartTimeout = 20 * time.Second 51 | return d 52 | } 53 | 54 | var switchesFormat = "-port=%d -url-base=%s -log-path=%s -http-threads=%d" 55 | 56 | var cmdchan = make(chan error) 57 | 58 | func (d *ChromeDriver) Start() error { 59 | csferr := "chromedriver start failed: " 60 | if d.cmd != nil { 61 | return errors.New(csferr + "chromedriver already running") 62 | } 63 | 64 | if d.LogPath != "" { 65 | //check if log-path is writable 66 | file, err := os.OpenFile(d.LogPath, os.O_WRONLY|os.O_CREATE, 0664) 67 | if err != nil { 68 | return errors.New(csferr + "unable to write in log path: " + err.Error()) 69 | } 70 | file.Close() 71 | } 72 | 73 | d.url = fmt.Sprintf("http://127.0.0.1:%d%s", d.Port, d.BaseUrl) 74 | var switches []string 75 | switches = append(switches, "-port="+strconv.Itoa(d.Port)) 76 | switches = append(switches, "-log-path="+d.LogPath) 77 | switches = append(switches, "-http-threads="+strconv.Itoa(d.Threads)) 78 | if d.BaseUrl != "" { 79 | switches = append(switches, "-url-base="+d.BaseUrl) 80 | } 81 | 82 | d.cmd = exec.Command(d.path, switches...) 83 | stdout, err := d.cmd.StdoutPipe() 84 | if err != nil { 85 | return errors.New(csferr + err.Error()) 86 | } 87 | stderr, err := d.cmd.StderrPipe() 88 | if err != nil { 89 | return errors.New(csferr + err.Error()) 90 | } 91 | if err := d.cmd.Start(); err != nil { 92 | return errors.New(csferr + err.Error()) 93 | } 94 | if d.LogFile != "" { 95 | flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC 96 | d.logFile, err = os.OpenFile(d.LogFile, flags, 0640) 97 | if err != nil { 98 | return err 99 | } 100 | go io.Copy(d.logFile, stdout) 101 | go io.Copy(d.logFile, stderr) 102 | } else { 103 | go io.Copy(os.Stdout, stdout) 104 | go io.Copy(os.Stderr, stderr) 105 | } 106 | if err = probePort(d.Port, d.StartTimeout); err != nil { 107 | return err 108 | } 109 | return nil 110 | } 111 | 112 | func (d *ChromeDriver) Stop() error { 113 | if d.cmd == nil { 114 | return errors.New("stop failed: chromedriver not running") 115 | } 116 | defer func() { 117 | d.cmd = nil 118 | }() 119 | d.cmd.Process.Signal(os.Interrupt) 120 | if d.logFile != nil { 121 | d.logFile.Close() 122 | } 123 | return nil 124 | } 125 | 126 | func (d *ChromeDriver) NewSession(desired, required Capabilities) (*Session, error) { 127 | //id, capabs, err := d.newSession(desired, required) 128 | //return &Session{id, capabs, d}, err 129 | session, err := d.newSession(desired, required) 130 | if err != nil { 131 | return nil, err 132 | } 133 | session.wd = d 134 | return session, nil 135 | } 136 | 137 | func (d *ChromeDriver) Sessions() ([]Session, error) { 138 | sessions, err := d.sessions() 139 | if err != nil { 140 | return nil, err 141 | } 142 | for i := range sessions { 143 | sessions[i].wd = d 144 | } 145 | return sessions, nil 146 | } 147 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Federico Sogaro. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package webdriver 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "strconv" 15 | ) 16 | 17 | const ( 18 | Success = 0 19 | NoSuchDriver = 6 20 | NoSuchElement = 7 21 | NoSuchFrame = 8 22 | UnknownCommand = 9 23 | StaleElementReference = 10 24 | ElementNotVisible = 11 25 | InvalidElementState = 12 26 | UnknownError = 13 27 | ElementIsNotSelectable = 15 28 | JavaScriptError = 17 29 | XPathLookupError = 19 30 | Timeout = 21 31 | NoSuchWindow = 23 32 | InvalidCookieDomain = 24 33 | UnableToSetCookie = 25 34 | UnexpectedAlertOpen = 26 35 | NoAlertOpenError = 27 36 | ScriptTimeout = 28 37 | InvalidElementCoordinates = 29 38 | IMENotAvailable = 30 39 | IMEEngineActivationFailed = 31 40 | InvalidSelector = 32 41 | SessionNotCreatedException = 33 42 | MoveTargetOutOfBounds = 34 43 | ) 44 | 45 | var statusCodeStrings = map[int]string{ 46 | 0: "The command executed successfully.", 47 | 6: "A session is either terminated or not started.", 48 | 7: "An element could not be located on the page using the given search parameters.", 49 | 8: "A request to switch to a frame could not be satisfied because the frame could not be found.", 50 | 9: "The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource.", 51 | 10: "An element command failed because the referenced element is no longer attached to the DOM.", 52 | 11: "An element command could not be completed because the element is not visible on the page.", 53 | 12: "An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element).", 54 | 13: "An unknown server-side error occurred while processing the command.", 55 | 15: "An attempt was made to select an element that cannot be selected.", 56 | 17: "An error occurred while executing user supplied JavaScript.", 57 | 19: "An error occurred while searching for an element by XPath.", 58 | 21: "An operation did not complete before its timeout expired.", 59 | 23: "A request to switch to a different window could not be satisfied because the window could not be found.", 60 | 24: "An illegal attempt was made to set a cookie under a different domain than the current page.", 61 | 25: "A request to set a cookie's value could not be satisfied.", 62 | 26: "A modal dialog was open, blocking this operation.", 63 | 27: "An attempt was made to operate on a modal dialog when one was not open.", 64 | 28: "A script did not complete before its timeout expired.", 65 | 29: "The coordinates provided to an interactions operation are invalid.", 66 | 30: "IME was not available.", 67 | 31: "An IME engine could not be started.", 68 | 32: "Argument was an invalid selector (e.g. XPath/CSS).", 69 | 33: "A new session could not be created.", 70 | 34: "Target provided for a move action is out of bounds.", 71 | } 72 | 73 | //type StatusErrorCode int 74 | 75 | type StackFrame struct { 76 | FileName string 77 | ClassName string 78 | MethodName string 79 | LineNumber int 80 | } 81 | 82 | type CommandError struct { 83 | StatusCode int 84 | ErrorType string 85 | Message string 86 | Screen string 87 | Class string 88 | StackTrace []StackFrame 89 | } 90 | 91 | func (e CommandError) Error() string { 92 | //TODO print Screen, Class, StackTrace 93 | m := e.ErrorType 94 | if m != "" { 95 | m += ": " 96 | } 97 | if e.StatusCode == -1 { 98 | m += "status code not specified" 99 | } else if str, found := statusCodeStrings[e.StatusCode]; found { 100 | m += str + ": " + e.Message 101 | } else { 102 | m += fmt.Sprintf("unknown status code (%d): %s", e.StatusCode, e.Message) 103 | } 104 | return m 105 | } 106 | 107 | //type matching the structure standard JSON object response. 108 | type jsonResponse struct { 109 | RawSessionId json.RawMessage `json:"sessionId"` 110 | Status int `json:"status"` 111 | RawValue json.RawMessage `json:"value"` 112 | } 113 | 114 | func parseError(c int, jr jsonResponse) error { 115 | var responseCodeError string 116 | switch c { 117 | // workaround: chromedriver could returns 200 code on errors 118 | case 200: 119 | case 400: 120 | responseCodeError = "400: Missing Command Parameters" 121 | case 404: 122 | responseCodeError = "404: Unknown command/Resource Not Found" 123 | case 405: 124 | responseCodeError = "405: Invalid Command Method" 125 | case 500: 126 | responseCodeError = "500: Failed Command" 127 | case 501: 128 | responseCodeError = "501: Unimplemented Command" 129 | default: 130 | responseCodeError = "Unknown error" 131 | } 132 | if jr.Status == 0 { 133 | return &CommandError{StatusCode: -1, ErrorType: responseCodeError} 134 | } 135 | commandError := &CommandError{StatusCode: jr.Status, ErrorType: responseCodeError} 136 | err := json.Unmarshal(jr.RawValue, commandError) 137 | if err != nil { 138 | // workaround: firefox could returns a string instead of a JSON object on errors 139 | commandError.Message = string(jr.RawValue) 140 | } 141 | return commandError 142 | } 143 | 144 | func isRedirect(response *http.Response) bool { 145 | r := response.StatusCode 146 | return r == 302 || r == 303 147 | } 148 | 149 | func newRequest(method, url string, data []byte) (*http.Request, error) { 150 | request, err := http.NewRequest(method, url, bytes.NewBuffer(data)) 151 | if err != nil { 152 | return nil, err 153 | } 154 | if method == "POST" { 155 | request.Header.Add("Content-Type", "application/json;charset=utf-8") 156 | } 157 | //TODO add png format for screenshots 158 | request.Header.Set("Accept", "application/json") 159 | request.Header.Set("Accept-charset", "utf-8") 160 | return request, nil 161 | } 162 | 163 | type WebDriverCore struct { 164 | url string 165 | } 166 | 167 | func (w WebDriverCore) Start() error { return nil } 168 | func (w WebDriverCore) Stop() error { return nil } 169 | 170 | func (w WebDriverCore) do(params interface{}, method, urlFormat string, urlParams ...interface{}) (string, []byte, error) { 171 | if method != "GET" && method != "POST" && method != "DELETE" { 172 | return "", nil, errors.New("invalid method: " + method) 173 | } 174 | url := w.url + fmt.Sprintf(urlFormat, urlParams...) 175 | return w.doInternal(params, method, url) 176 | } 177 | 178 | //communicate with the server. 179 | func (w WebDriverCore) doInternal(params interface{}, method, url string) (string, []byte, error) { 180 | debugprint(">> " + method + " " + url) 181 | var jsonParams []byte 182 | var err error 183 | if method == "POST" { 184 | if params == nil { 185 | params = map[string]interface{}{} 186 | } 187 | jsonParams, err = json.Marshal(params) 188 | if err != nil { 189 | return "", nil, err 190 | } 191 | } 192 | request, err := newRequest(method, url, jsonParams) 193 | if err != nil { 194 | return "", nil, err 195 | } 196 | response, err := http.DefaultClient.Do(request) 197 | if err != nil { 198 | return "", nil, err 199 | } 200 | debugprint("StatusCode: " + strconv.Itoa(response.StatusCode)) 201 | //http.Client doesn't follow POST redirected (/session command) 202 | if method == "POST" && isRedirect(response) { 203 | debugprint("redirected") 204 | url, err := response.Location() 205 | if err != nil { 206 | return "", nil, err 207 | } 208 | return w.doInternal(nil, "GET", url.String()) 209 | } 210 | 211 | buf, err := ioutil.ReadAll(response.Body) 212 | if err != nil { 213 | return "", nil, err 214 | } 215 | head := string(buf) 216 | if len(buf) > 1024 { 217 | head = fmt.Sprintf("%s ...%d more bytes", string(buf[0:1024]), len(buf)-1024) 218 | } 219 | debugprint("<< " + head) 220 | 221 | jr := &jsonResponse{} 222 | err = json.Unmarshal(buf, jr) 223 | if err != nil && response.StatusCode == 200 { 224 | return "", nil, errors.New("error: response must be a JSON object") 225 | } 226 | //if err = json.Unmarshal(buf, jr); err != nil { 227 | // return "", nil, errors.New("error: response must be a JSON object: "+err.Error()) 228 | //} 229 | if response.StatusCode >= 400 || jr.Status != 0 { 230 | return "", nil, parseError(response.StatusCode, *jr) 231 | } 232 | sessionId := string(bytes.Trim(jr.RawSessionId, "{}\"")) 233 | return sessionId, []byte(jr.RawValue), nil 234 | } 235 | 236 | //Query the server's status. 237 | func (w WebDriverCore) Status() (*Status, error) { 238 | _, data, err := w.do(nil, "GET", "/status") 239 | if err != nil { 240 | return nil, err 241 | } 242 | status := &Status{} 243 | err = json.Unmarshal(data, status) 244 | return status, err 245 | } 246 | 247 | //Create a new session. 248 | //The server should attempt to create a session that most closely matches the desired and required capabilities. Required capabilities have higher priority than desired capabilities and must be set for the session to be created. 249 | func (w WebDriverCore) newSession(desired, required Capabilities) (*Session, error) { 250 | if desired == nil { 251 | desired = map[string]interface{}{} 252 | } 253 | p := params{"desiredCapabilities": desired, "requiredCapabilities": required} 254 | sessionId, data, err := w.do(p, "POST", "/session") 255 | if err != nil { 256 | return nil, err 257 | } 258 | var capabilities Capabilities 259 | err = json.Unmarshal(data, &capabilities) 260 | return &Session{Id: sessionId, Capabilities: capabilities}, err 261 | } 262 | 263 | //Returns a list of the currently active sessions. 264 | func (w WebDriverCore) sessions() ([]Session, error) { 265 | _, data, err := w.do(nil, "GET", "/sessions") 266 | if err != nil { 267 | return nil, err 268 | } 269 | var sessions []Session 270 | err = json.Unmarshal(data, &sessions) 271 | return sessions, err 272 | //return nil, errors.New("unsupported") 273 | } 274 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Federico Sogaro. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The package implementation a WebDriver that communicate with a browser 6 | // using the JSON Wire Protocol. 7 | // 8 | // See https://code.google.com/p/selenium/wiki/JsonWireProtocol 9 | // 10 | // Example: 11 | // chromeDriver := webdriver.NewChromeDriver("/path/to/chromedriver") 12 | // err := chromeDriver.Start() 13 | // if err != nil { 14 | // log.Println(err) 15 | // } 16 | // desired := webdriver.Capabilities{"Platform": "Linux"} 17 | // required := webdriver.Capabilities{} 18 | // session, err := chromeDriver.NewSession(desired, required) 19 | // if err != nil { 20 | // log.Println(err) 21 | // } 22 | // err = session.Url("http://golang.org") 23 | // if err != nil { 24 | // log.Println(err) 25 | // } 26 | // time.Sleep(60 * time.Second) 27 | // session.Delete() 28 | // chromeDriver.Stop() 29 | // 30 | package webdriver 31 | -------------------------------------------------------------------------------- /firefoxdriver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Federico Sogaro. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package webdriver 6 | 7 | import ( 8 | "archive/zip" 9 | "encoding/xml" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "net" 15 | "os" 16 | "os/exec" 17 | "path/filepath" 18 | "strconv" 19 | "time" 20 | ) 21 | 22 | type FirefoxDriver struct { 23 | WebDriverCore 24 | // The port firefox webdriver listens on. This port - 1 will be used as a mutex to avoid starting multiple firefox instances listening to the same port. Default: 7055 25 | Port int 26 | // Start method fails if lock (see Port) is not acquired before LockPortTimeout. Default 60s 27 | LockPortTimeout time.Duration 28 | // Start method fails if Firefox doesn't start in less than StartTimeout. Default 20s. 29 | StartTimeout time.Duration 30 | // Log file to dump firefox stdout/stderr. If "" send to terminal. Default: "" 31 | LogFile string 32 | // Firefox preferences. Default: see method GetDefaultPrefs 33 | Prefs map[string]interface{} 34 | // If temporary profile has to be deleted when closing. Default: true 35 | DeleteProfileOnClose bool 36 | 37 | firefoxPath string 38 | xpiPath string 39 | profilePath string 40 | cmd *exec.Cmd 41 | logFile *os.File 42 | } 43 | 44 | func NewFirefoxDriver(firefoxPath string, xpiPath string) *FirefoxDriver { 45 | d := &FirefoxDriver{} 46 | d.firefoxPath = firefoxPath 47 | d.xpiPath = xpiPath 48 | d.Port = 0 49 | d.LockPortTimeout = 60 * time.Second 50 | d.StartTimeout = 20 * time.Second 51 | d.LogFile = "" 52 | d.Prefs = GetDefaultPrefs() 53 | d.DeleteProfileOnClose = true 54 | return d 55 | } 56 | 57 | // Equivalent to setting the following firefox preferences to: 58 | // "webdriver.log.file": path/jsconsole.log 59 | // "webdriver.log.driver.file": path/driver.log 60 | // "webdriver.log.profiler.file": path/profiler.log 61 | // "webdriver.log.browser.file": path/browser.log 62 | func (d *FirefoxDriver) SetLogPath(path string) { 63 | d.Prefs["webdriver.log.file"] = filepath.Join(path, "jsconsole.log") 64 | d.Prefs["webdriver.log.driver.file"] = filepath.Join(path, "driver.log") 65 | d.Prefs["webdriver.log.profiler.file"] = filepath.Join(path, "profiler.log") 66 | d.Prefs["webdriver.log.browser.file"] = filepath.Join(path, "browser.log") 67 | } 68 | 69 | func (d *FirefoxDriver) Start() error { 70 | if d.Port == 0 { //otherwise try to use that port 71 | d.Port = 7055 72 | lockPortAddress := fmt.Sprintf("127.0.0.1:%d", d.Port-1) 73 | now := time.Now() 74 | //try to lock port d.Port - 1 75 | for { 76 | if ln, err := net.Listen("tcp", lockPortAddress); err == nil { 77 | defer ln.Close() 78 | break 79 | } 80 | if time.Since(now) > d.LockPortTimeout { 81 | return errors.New("timeout expired trying to lock mutex port") 82 | } 83 | time.Sleep(1 * time.Second) 84 | } 85 | //find the first available port starting with d.Port 86 | for i := d.Port; i < 65535; i++ { 87 | address := fmt.Sprintf("127.0.0.1:%d", i) 88 | if ln, err := net.Listen("tcp", address); err == nil { 89 | if err = ln.Close(); err != nil { 90 | return err 91 | } 92 | d.Port = i 93 | break 94 | } 95 | } 96 | } 97 | //start firefox with custom profile 98 | //TODO it should be possible to use an existing profile 99 | d.Prefs["webdriver_firefox_port"] = d.Port 100 | var err error 101 | d.profilePath, err = createTempProfile(d.xpiPath, d.Prefs) 102 | if err != nil { 103 | return err 104 | } 105 | debugprint(d.profilePath) 106 | d.cmd = exec.Command(d.firefoxPath, "-no-remote", "-profile", d.profilePath) 107 | stdout, err := d.cmd.StdoutPipe() 108 | if err != nil { 109 | fmt.Println(err) 110 | } 111 | stderr, err := d.cmd.StderrPipe() 112 | if err != nil { 113 | fmt.Println(err) 114 | } 115 | if err := d.cmd.Start(); err != nil { 116 | return errors.New("unable to start firefox: " + err.Error()) 117 | } 118 | if d.LogFile != "" { 119 | flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC 120 | d.logFile, err = os.OpenFile(d.LogFile, flags, 0640) 121 | if err != nil { 122 | return err 123 | } 124 | go io.Copy(d.logFile, stdout) 125 | go io.Copy(d.logFile, stderr) 126 | } else { 127 | go io.Copy(os.Stdout, stdout) 128 | go io.Copy(os.Stderr, stderr) 129 | } 130 | //probe d.Port until firefox replies or StartTimeout is up 131 | if err = probePort(d.Port, d.StartTimeout); err != nil { 132 | return err 133 | } 134 | 135 | d.url = fmt.Sprintf("http://127.0.0.1:%d/hub", d.Port) 136 | return nil 137 | } 138 | 139 | // Populate a map with default firefox preferences 140 | func GetDefaultPrefs() map[string]interface{} { 141 | prefs := map[string]interface{}{ 142 | // Disable cache 143 | "browser.cache.disk.enable": false, 144 | "browser.cache.disk.capacity": 0, 145 | "browser.cache.memory.enable": true, 146 | //Allow extensions to be installed into the profile and still work 147 | "extensions.autoDisableScopes": 10, 148 | //Disable "do you want to remember this password?" 149 | "signon.rememberSignons": false, 150 | //Disable re-asking for license agreement 151 | "browser.EULA.3.accepted": true, 152 | "browser.EULA.override": true, 153 | //set blank homepage, no welcome page 154 | "browser.startup.homepage": "about:blank", 155 | "browser.startup.page": 0, 156 | "browser.startup.homepage_override.mstone": "ignore", 157 | //browser mode online 158 | "browser.offline": false, 159 | // Don't ask if we want to switch default browsers 160 | "browser.shell.checkDefaultBrowser": false, 161 | //TODO configure proxy if needed ("network.proxy.type", "network.proxy.autoconfig_url" 162 | //enable pop-ups 163 | "dom.disable_open_during_load": false, 164 | //disable dialog for long username/password in url 165 | "network.http.phishy-userpass-length": 255, 166 | //Disable security warnings 167 | "security.warn_entering_secure": false, 168 | "security.warn_entering_secure.show_once": false, 169 | "security.warn_entering_weak": false, 170 | "security.warn_entering_weak.show_once": false, 171 | "security.warn_leaving_secure": false, 172 | "security.warn_leaving_secure.show_once": false, 173 | "security.warn_submit_insecure": false, 174 | "security.warn_submit_insecure.show_once": false, 175 | "security.warn_viewing_mixed": false, 176 | "security.warn_viewing_mixed.show_once": false, 177 | //Do not use NetworkManager to detect offline/online status. 178 | "toolkit.networkmanager.disable": true, 179 | //TODO disable script timeout (should be same as server timeout) 180 | //"dom.max_script_run_time" 181 | //("dom.max_chrome_script_run_time") 182 | // Disable various autostuff 183 | "app.update.auto": false, 184 | "app.update.enabled": false, 185 | "extensions.update.enabled": false, 186 | "browser.search.update": false, 187 | "extensions.blocklist.enabled": false, 188 | "browser.safebrowsing.enabled": false, 189 | "browser.safebrowsing.malware.enabled": false, 190 | "browser.download.manager.showWhenStarting": false, 191 | "browser.sessionstore.resume_from_crash": false, 192 | "browser.tabs.warnOnClose": false, 193 | "browser.tabs.warnOnOpen": false, 194 | "devtools.errorconsole.enabled": true, 195 | "extensions.logging.enabled": true, 196 | "extensions.update.notifyUser": false, 197 | "network.manage-offline-status": false, 198 | "offline-apps.allow_by_default": true, 199 | "prompts.tab_modal.enabled": false, 200 | "security.fileuri.origin_policy": 3, 201 | "security.fileuri.strict_origin_policy": false, 202 | "toolkit.telemetry.prompted": 2, 203 | "toolkit.telemetry.enabled": false, 204 | "toolkit.telemetry.rejected": true, 205 | "browser.dom.window.dump.enabled": true, 206 | "dom.report_all_js_exceptions": true, 207 | "javascript.options.showInConsole": true, 208 | "network.http.max-connections-per-server": 10, 209 | // Webdriver settings 210 | "webdriver_accept_untrusted_certs": true, 211 | "webdriver_assume_untrusted_issuer": true, 212 | "webdriver_enable_native_events": false, 213 | "webdriver_unexpected_alert_behaviour": "dismiss", 214 | } 215 | return prefs 216 | } 217 | 218 | type InstallRDF struct { 219 | Description InstallRDFDescription 220 | } 221 | 222 | type InstallRDFDescription struct { 223 | Id string `xml:"id"` 224 | } 225 | 226 | func createTempProfile(xpiPath string, prefs map[string]interface{}) (string, error) { 227 | cpferr := "create profile failed: " 228 | profilePath, err := ioutil.TempDir(os.TempDir(), "webdriver") 229 | if err != nil { 230 | return "", errors.New(cpferr + err.Error()) 231 | } 232 | extsPath := filepath.Join(profilePath, "extensions") 233 | err = os.Mkdir(extsPath, 0770) 234 | if err != nil { 235 | return "", errors.New(cpferr + err.Error()) 236 | } 237 | zr, err := zip.OpenReader(xpiPath) 238 | if err != nil { 239 | return "", errors.New(cpferr + err.Error()) 240 | } 241 | defer zr.Close() 242 | var extName string 243 | for _, f := range zr.File { 244 | if f.Name == "install.rdf" { 245 | rc, err := f.Open() 246 | if err != nil { 247 | return "", errors.New(cpferr + err.Error()) 248 | } 249 | buf, err := ioutil.ReadAll(rc) 250 | if err != nil { 251 | return "", errors.New(cpferr + err.Error()) 252 | } 253 | rc.Close() 254 | installRDF := InstallRDF{} 255 | err = xml.Unmarshal(buf, &installRDF) 256 | if err != nil { 257 | return "", errors.New(cpferr + err.Error()) 258 | } 259 | if installRDF.Description.Id == "" { 260 | return "", errors.New(cpferr + "unable to find extension Id from install.rdf") 261 | } 262 | extName = installRDF.Description.Id 263 | break 264 | } 265 | } 266 | extPath := filepath.Join(extsPath, extName) 267 | err = os.Mkdir(extPath, 0770) 268 | if err != nil { 269 | return "", errors.New(cpferr + err.Error()) 270 | } 271 | for _, f := range zr.File { 272 | if err = writeExtensionFile(f, extPath); err != nil { 273 | return "", err 274 | } 275 | } 276 | fuserName := filepath.Join(profilePath, "user.js") 277 | fuser, err := os.OpenFile(fuserName, os.O_WRONLY|os.O_CREATE, 0600) 278 | if err != nil { 279 | return "", errors.New(cpferr + err.Error()) 280 | } 281 | defer fuser.Close() 282 | for k, i := range prefs { 283 | fuser.WriteString("user_pref(\"" + k + "\", ") 284 | switch x := i.(type) { 285 | case bool: 286 | if x { 287 | fuser.WriteString("true") 288 | } else { 289 | fuser.WriteString("false") 290 | } 291 | case int: 292 | fuser.WriteString(strconv.Itoa(x)) 293 | case string: 294 | fuser.WriteString("\"" + x + "\"") 295 | default: 296 | return "", errors.New(cpferr + "unexpected preference type: " + k) 297 | } 298 | fuser.WriteString(");\n") 299 | } 300 | return profilePath, nil 301 | } 302 | 303 | func writeExtensionFile(f *zip.File, extPath string) error { 304 | weferr := "write extension failed: " 305 | rc, err := f.Open() 306 | if err != nil { 307 | return errors.New(weferr + err.Error()) 308 | } 309 | defer rc.Close() 310 | filename := filepath.Join(extPath, f.Name) 311 | if f.FileInfo().IsDir() { 312 | err = os.Mkdir(filename, 0770) 313 | if err != nil { 314 | return err 315 | } 316 | } else { 317 | dst, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0600) 318 | if err != nil { 319 | return errors.New(weferr + err.Error()) 320 | } 321 | defer dst.Close() 322 | _, err = io.Copy(dst, rc) 323 | if err != nil { 324 | return errors.New(weferr + err.Error()) 325 | } 326 | } 327 | return nil 328 | } 329 | 330 | func (d *FirefoxDriver) Stop() error { 331 | if d.cmd == nil { 332 | return errors.New("stop failed: firefoxdriver not running") 333 | } 334 | defer func() { 335 | d.cmd = nil 336 | }() 337 | d.cmd.Process.Signal(os.Interrupt) 338 | if d.logFile != nil { 339 | d.logFile.Close() 340 | } 341 | if d.DeleteProfileOnClose { 342 | os.RemoveAll(d.profilePath) 343 | } 344 | return nil 345 | } 346 | 347 | func (d *FirefoxDriver) NewSession(desired, required Capabilities) (*Session, error) { 348 | session, err := d.newSession(desired, required) 349 | if err != nil { 350 | return nil, err 351 | } 352 | session.wd = d 353 | return session, nil 354 | } 355 | 356 | func (d *FirefoxDriver) Sessions() ([]Session, error) { 357 | sessions, err := d.sessions() 358 | if err != nil { 359 | return nil, err 360 | } 361 | for i := range sessions { 362 | sessions[i].wd = d 363 | } 364 | return sessions, nil 365 | } 366 | 367 | /*func (d *FirefoxDriver) NewSession(desired, required Capabilities) (*Session, error) { 368 | id, capabs, err := d.newSession(desired, required) 369 | return &Session{id, capabs, d}, err 370 | }*/ 371 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Federico Sogaro. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package webdriver 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net" 11 | "runtime" 12 | "time" 13 | ) 14 | 15 | var debug = false 16 | 17 | func debugprint(message interface{}) { 18 | if debug { 19 | pc, _, line, ok := runtime.Caller(1) 20 | if ok { 21 | f := runtime.FuncForPC(pc) 22 | fmt.Printf("%s:%d: %v\n", f.Name(), line, message) 23 | } else { 24 | fmt.Printf("?:?: %s\n", message) 25 | } 26 | } 27 | } 28 | 29 | //probe d.Port until get a reply or timeout is up 30 | func probePort(port int, timeout time.Duration) error { 31 | address := fmt.Sprintf("127.0.0.1:%d", port) 32 | now := time.Now() 33 | for { 34 | if conn, err := net.Dial("tcp", address); err == nil { 35 | if err = conn.Close(); err != nil { 36 | return err 37 | } 38 | break 39 | } 40 | if time.Since(now) > timeout { 41 | return errors.New("start failed: timeout expired") 42 | } 43 | time.Sleep(1 * time.Second) 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /webdriver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Federico Sogaro. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package webdriver 6 | 7 | import ( 8 | "bytes" 9 | "encoding/base64" 10 | "encoding/json" 11 | "errors" 12 | "io/ioutil" 13 | 14 | // "fmt" 15 | // "net/http" 16 | ) 17 | 18 | type WebDriver interface { 19 | //Start webdriver service 20 | Start() error 21 | //Stop webdriver service 22 | Stop() error 23 | //Query the server's status. 24 | Status() (*Status, error) 25 | //Create a new session. 26 | NewSession(desired, required Capabilities) (*Session, error) 27 | //Returns a list of the currently active sessions. 28 | Sessions() ([]Session, error) 29 | 30 | do(params interface{}, method, urlFormat string, urlParams ...interface{}) (string, []byte, error) 31 | } 32 | 33 | //typing saver 34 | type params map[string]interface{} 35 | 36 | //Server details. 37 | type Status struct { 38 | Build Build 39 | OS OS 40 | } 41 | 42 | //Server built details. 43 | type Build struct { 44 | Version string 45 | Revision string 46 | Time string 47 | } 48 | 49 | //Server OS details 50 | type OS struct { 51 | Arch string 52 | Name string 53 | Version string 54 | } 55 | 56 | //Capabilities is a map that stores capabilities of a session. 57 | type Capabilities map[string]interface{} 58 | 59 | //A session. 60 | type Session struct { 61 | Id string 62 | Capabilities Capabilities 63 | wd WebDriver 64 | } 65 | 66 | type WindowHandle struct { 67 | s *Session 68 | id string 69 | } 70 | 71 | type Size struct { 72 | Width int 73 | Height int 74 | } 75 | 76 | type Position struct { 77 | X int 78 | Y int 79 | } 80 | 81 | type FindElementStrategy string 82 | 83 | const ( 84 | //Returns an element whose class name contains the search value; compound class names are not permitted. 85 | ClassName = FindElementStrategy("class name") 86 | //Returns an element matching a CSS selector. 87 | CSS_Selector = FindElementStrategy("css selector") 88 | //Returns an element whose ID attribute matches the search value. 89 | ID = FindElementStrategy("id") 90 | //Returns an element whose NAME attribute matches the search value. 91 | Name = FindElementStrategy("name") 92 | //Returns an anchor element whose visible text matches the search value. 93 | LinkText = FindElementStrategy("link text") 94 | //Returns an anchor element whose visible text partially matches the search value. 95 | PartialLinkText = FindElementStrategy("partial link text") 96 | //Returns an element whose tag name matches the search value. 97 | TagName = FindElementStrategy("tag name") 98 | //Returns an element matching an XPath expression. 99 | XPath = FindElementStrategy("xpath") 100 | ) 101 | 102 | type element struct { 103 | ELEMENT string 104 | } 105 | 106 | type WebElement struct { 107 | s *Session 108 | id string 109 | } 110 | 111 | type Cookie struct { 112 | Name string 113 | Value string 114 | Path string 115 | Domain string 116 | Secure bool 117 | Expiry int 118 | } 119 | 120 | type GeoLocation struct { 121 | Latitude float64 `json:"latitude"` 122 | Longitude float64 `json:"longitude"` 123 | Altitude float64 `json:"altitude"` 124 | } 125 | 126 | type LogLevel string 127 | 128 | const ( 129 | LogAll = LogLevel("ALL") 130 | LogDebug = LogLevel("DEBUG") 131 | LogInfo = LogLevel("INFO") 132 | LogWarning = LogLevel("WARNING") 133 | LogSevere = LogLevel("SEVERE") 134 | LogOff = LogLevel("OFF") 135 | ) 136 | 137 | type LogEntry struct { 138 | TimeStamp int //TODO timestamp number type? 139 | Level string 140 | Message string 141 | } 142 | 143 | type HTML5CacheStatus int 144 | 145 | const ( 146 | CacheStatusUncached = HTML5CacheStatus(0) 147 | CacheStatusIdle = HTML5CacheStatus(1) 148 | CacheStatusChecking = HTML5CacheStatus(2) 149 | CacheStatusDownloading = HTML5CacheStatus(3) 150 | CacheStatusUpdateReady = HTML5CacheStatus(4) 151 | CacheStatusObsolete = HTML5CacheStatus(5) 152 | ) 153 | 154 | //////////////////////////////////////////////////////////////////////////////// 155 | // COMMAND LIST 156 | // Command descriptions are from: 157 | // https://code.google.com/p/selenium/wiki/JsonWireProtocol 158 | //////////////////////////////////////////////////////////////////////////////// 159 | 160 | //Retrieve the capabilities of the specified session. 161 | func (s Session) GetCapabilities() Capabilities { 162 | // GET /session/:sessionId 163 | // I have the capabilities stored in Session already 164 | return s.Capabilities 165 | } 166 | 167 | //Delete the session. 168 | func (s Session) Delete() error { 169 | _, _, err := s.wd.do(nil, "DELETE", "/session/%s", s.Id) 170 | return err 171 | } 172 | 173 | //Configure the amount of time that a particular type of operation can execute for before they are aborted and a |Timeout| error is returned to the client. Valid values are: "script" for script timeouts, "implicit" for modifying the implicit wait timeout and "page load" for setting a page load timeout. 174 | func (s Session) SetTimeouts(typ string, ms int) error { 175 | p := params{"type": typ, "ms": ms} 176 | _, _, err := s.wd.do(p, "POST", "/session/%s/timeouts", s.Id) 177 | return err 178 | } 179 | 180 | //Set the amount of time, in milliseconds, that asynchronous scripts executed by ExecuteScriptAsync() are permitted to run before they are aborted and a |Timeout| error is returned to the client. 181 | func (s Session) SetTimeoutsAsyncScript(ms int) error { 182 | p := params{"ms": ms} 183 | _, _, err := s.wd.do(p, "POST", "/session/%s/timeouts/async_script", s.Id) 184 | return err 185 | } 186 | 187 | //Set the amount of time the driver should wait when searching for elements. When searching for a single element, the driver should poll the page until an element is found or the timeout expires, whichever occurs first. When searching for multiple elements, the driver should poll the page until at least one element is found or the timeout expires, at which point it should return an empty list. 188 | //If this command is never sent, the driver should default to an implicit wait of 0ms. 189 | func (s Session) SetTimeoutsImplicitWait(ms int) error { 190 | p := params{"ms": ms} 191 | _, _, err := s.wd.do(p, "POST", "/session/%s/timeouts/implicit_wait", s.Id) 192 | return err 193 | } 194 | 195 | func (s Session) GetCurrentWindowHandle() WindowHandle { 196 | return WindowHandle{&s, "current"} 197 | } 198 | 199 | //Retrieve the current window handle. 200 | func (s Session) WindowHandle() (WindowHandle, error) { 201 | _, data, err := s.wd.do(nil, "GET", "/session/%s/window_handle", s.Id) 202 | if err != nil { 203 | return WindowHandle{}, err 204 | } 205 | var handle string 206 | err = json.Unmarshal(data, &handle) 207 | return WindowHandle{&s, handle}, err 208 | } 209 | 210 | //Retrieve the list of all window handles available to the session. 211 | func (s Session) WindowHandles() ([]WindowHandle, error) { 212 | _, data, err := s.wd.do(nil, "GET", "/session/%s/window_handles", s.Id) 213 | if err != nil { 214 | return nil, err 215 | } 216 | var hv []string 217 | err = json.Unmarshal(data, &hv) 218 | if err != nil { 219 | return nil, err 220 | } 221 | var handles = make([]WindowHandle, len(hv)) 222 | for i, h := range hv { 223 | handles[i] = WindowHandle{&s, h} 224 | } 225 | return handles, nil 226 | } 227 | 228 | //Retrieve the URL of the current page. 229 | func (s Session) GetUrl() (string, error) { 230 | _, data, err := s.wd.do(nil, "GET", "/session/%s/url", s.Id) 231 | if err != nil { 232 | return "", err 233 | } 234 | var url string 235 | err = json.Unmarshal(data, &url) 236 | return url, err 237 | } 238 | 239 | //Navigate to a new URL. 240 | func (s Session) Url(url string) error { 241 | p := params{"url": url} 242 | _, _, err := s.wd.do(p, "POST", "/session/%s/url", s.Id) 243 | return err 244 | } 245 | 246 | //Navigate forwards in the browser history, if possible. 247 | func (s Session) Forward() error { 248 | _, _, err := s.wd.do(nil, "POST", "/session/%s/forward", s.Id) 249 | return err 250 | } 251 | 252 | //Navigate backwards in the browser history, if possible. 253 | func (s Session) Back() error { 254 | _, _, err := s.wd.do(nil, "POST", "/session/%s/back", s.Id) 255 | return err 256 | } 257 | 258 | //Refresh the current page. 259 | func (s Session) Refresh() error { 260 | _, _, err := s.wd.do(nil, "POST", "/session/%s/refresh", s.Id) 261 | return err 262 | } 263 | 264 | // Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. The executed script is assumed to be synchronous and the result of evaluating the script is returned to the client. 265 | // The script argument defines the script to execute in the form of a function body. The value returned by that function will be returned to the client. The function will be invoked with the provided args array and the values may be accessed via the arguments object in the order specified. 266 | // Arguments may be any JSON-primitive, array, or JSON object. JSON objects that define a WebElement reference will be converted to the corresponding DOM element. Likewise, any WebElements in the script result will be returned to the client as WebElement JSON objects. 267 | func (s Session) ExecuteScript(script string, args []interface{}) ([]byte, error) { 268 | p := params{"script": script, "args": args} 269 | _, data, err := s.wd.do(p, "POST", "/session/%s/execute", s.Id) 270 | return data, err 271 | } 272 | 273 | // Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. The executed script is assumed to be asynchronous and must signal that is done by invoking the provided callback, which is always provided as the final argument to the function. The value to this callback will be returned to the client. 274 | // Asynchronous script commands may not span page loads. If an unload event is fired while waiting for a script result, an error should be returned to the client. 275 | // The script argument defines the script to execute in teh form of a function body. The function will be invoked with the provided args array and the values may be accessed via the arguments object in the order specified. The final argument will always be a callback function that must be invoked to signal that the script has finished. 276 | // Arguments may be any JSON-primitive, array, or JSON object. JSON objects that define a WebElement reference will be converted to the corresponding DOM element. Likewise, any WebElements in the script result will be returned to the client as WebElement JSON objects. 277 | func (s Session) ExecuteScriptAsync(script string, args []interface{}) ([]byte, error) { 278 | p := params{"script": script, "args": args} 279 | _, data, err := s.wd.do(p, "POST", "/session/%s/execute_async", s.Id) 280 | return data, err 281 | } 282 | 283 | //Take a screenshot of the current page. 284 | func (s Session) Screenshot() ([]byte, error) { 285 | _, data, err := s.wd.do(nil, "GET", "/session/%s/screenshot", s.Id) 286 | if err != nil { 287 | return nil, err 288 | } 289 | reader := bytes.NewBuffer(data[1 : len(data)-1]) 290 | decoder := base64.NewDecoder(base64.StdEncoding, reader) 291 | return ioutil.ReadAll(decoder) 292 | } 293 | 294 | //List all available engines on the machine. 295 | func (s Session) IMEAvailableEngines() ([]string, error) { 296 | _, data, err := s.wd.do(nil, "GET", "session/%s/ime/available_engines", s.Id) 297 | if err != nil { 298 | return nil, err 299 | } 300 | var engines []string 301 | err = json.Unmarshal(data, &engines) 302 | return engines, err 303 | } 304 | 305 | //Get the name of the active IME engine. 306 | func (s Session) IMEActiveEngine() (string, error) { 307 | _, data, err := s.wd.do(nil, "GET", "session/%s/ime/active_engine", s.Id) 308 | if err != nil { 309 | return "", err 310 | } 311 | var engine string 312 | err = json.Unmarshal(data, &engine) 313 | return engine, err 314 | } 315 | 316 | //Indicates whether IME input is active at the moment (not if it's available). 317 | func (s Session) IsIMEActivated() (bool, error) { 318 | _, data, err := s.wd.do(nil, "GET", "session/%s/ime/activated", s.Id) 319 | if err != nil { 320 | return false, err 321 | } 322 | var activated bool 323 | err = json.Unmarshal(data, &activated) 324 | return activated, err 325 | } 326 | 327 | //De-activates the currently-active IME engine. 328 | func (s Session) IMEDeactivate() error { 329 | _, _, err := s.wd.do(nil, "GET", "session/%s/ime/deactivate", s.Id) 330 | return err 331 | } 332 | 333 | //Make an engines that is available (appears on the list returned by getAvailableEngines) active. 334 | func (s Session) IMEActivate(engine string) error { 335 | p := params{"engine": engine} 336 | _, _, err := s.wd.do(p, "POST", "/session/%s/ime/activate", s.Id) 337 | return err 338 | } 339 | 340 | //Change focus to another frame on the page. 341 | func (s Session) FocusOnFrame(frameId interface{}) error { 342 | if frameId != nil { 343 | switch frameId.(type) { 344 | case string: 345 | case int: 346 | case WebElement: 347 | default: 348 | return errors.New("invalid frame, must be string|int|nil|WebElement") 349 | } 350 | } 351 | p := params{"id": frameId} 352 | _, _, err := s.wd.do(p, "POST", "/session/%s/frame", s.Id) 353 | return err 354 | } 355 | 356 | // Change focus back to parent frame 357 | func (s Session) FocusParentFrame() error { 358 | _, _, err := s.wd.do(nil, "POST", "/session/%s/frame/parent", s.Id) 359 | return err 360 | } 361 | 362 | //Change focus to another window. The window to change focus to may be specified by its server assigned window handle, or by the value of its name attribute. 363 | func (s Session) FocusOnWindow(name string) error { 364 | p := params{"name": name} 365 | _, _, err := s.wd.do(p, "POST", "/session/%s/window", s.Id) 366 | return err 367 | } 368 | 369 | //Close the current window. 370 | func (s Session) CloseCurrentWindow() error { 371 | _, _, err := s.wd.do(nil, "DELETE", "/session/%s/window", s.Id) 372 | return err 373 | } 374 | 375 | //Change the size of the specified window. 376 | func (w WindowHandle) SetSize(size Size) error { 377 | p := params{"width": size.Width, "height": size.Height} 378 | _, _, err := w.s.wd.do(p, "POST", "/session/%s/window/%s/size", w.s.Id, w.id) 379 | return err 380 | } 381 | 382 | //Get the size of the specified window. 383 | func (w WindowHandle) GetSize() (Size, error) { 384 | _, data, err := w.s.wd.do(nil, "GET", "/session/%s/window/%s/size", w.s.Id, w.id) 385 | if err != nil { 386 | return Size{}, err 387 | } 388 | var outSize Size 389 | err = json.Unmarshal(data, &outSize) 390 | return outSize, err 391 | } 392 | 393 | //Change the position of the specified window. 394 | func (w WindowHandle) SetPosition(position Position) error { 395 | p := params{"x": position.X, "y": position.Y} 396 | _, _, err := w.s.wd.do(p, "POST", "/session/%s/window/%s/position", w.s.Id, w.id) 397 | return err 398 | } 399 | 400 | //Get the position of the specified window. 401 | func (w WindowHandle) GetPosition() (Position, error) { 402 | _, data, err := w.s.wd.do(nil, "GET", "/session/%s/window/%s/position", w.s.Id, w.id) 403 | if err != nil { 404 | return Position{}, err 405 | } 406 | var position Position 407 | err = json.Unmarshal(data, &position) 408 | return position, err 409 | } 410 | 411 | //Maximize the specified window if not already maximized. 412 | func (w WindowHandle) MaximizeWindow() error { 413 | _, _, err := w.s.wd.do(nil, "POST", "/session/%s/window/%s/maximize", w.s.Id, w.id) 414 | return err 415 | } 416 | 417 | //Retrieve all cookies visible to the current page. 418 | func (s Session) GetCookies() ([]Cookie, error) { 419 | _, data, err := s.wd.do(nil, "GET", "/session/%s/cookie", s.Id) 420 | if err != nil { 421 | return nil, err 422 | } 423 | var cookies []Cookie 424 | err = json.Unmarshal(data, &cookies) 425 | return cookies, err 426 | } 427 | 428 | //Set a cookie. 429 | func (s Session) SetCookie(cookie Cookie) error { 430 | p := params{"cookie": cookie} 431 | _, _, err := s.wd.do(p, "POST", "/session/%s/cookie", s.Id) 432 | return err 433 | } 434 | 435 | //Delete all cookies visible to the current page. 436 | func (s Session) DeleteCookies() error { 437 | _, _, err := s.wd.do(nil, "DELETE", "/session/%s/cookie", s.Id) 438 | return err 439 | } 440 | 441 | //Delete the cookie with the given name. 442 | func (s Session) DeleteCookieByName(name string) error { 443 | _, _, err := s.wd.do(nil, "DELETE", "/session/%s/cookie/%s", s.Id, name) 444 | return err 445 | } 446 | 447 | //Get the current page source. 448 | func (s Session) Source() (string, error) { 449 | _, data, err := s.wd.do(nil, "GET", "/session/%s/source", s.Id) 450 | if err != nil { 451 | return "", err 452 | } 453 | var source string 454 | err = json.Unmarshal(data, &source) 455 | return source, err 456 | } 457 | 458 | //Get the current page title. 459 | func (s Session) Title() (string, error) { 460 | _, data, err := s.wd.do(nil, "GET", "/session/%s/title", s.Id) 461 | if err != nil { 462 | return "", err 463 | } 464 | var title string 465 | err = json.Unmarshal(data, &title) 466 | return title, err 467 | } 468 | 469 | func (s Session) WebElementFromId(id string) WebElement { 470 | return WebElement{&s, id} 471 | } 472 | 473 | //Search for an element on the page, starting from the document root. 474 | func (s Session) FindElement(using FindElementStrategy, value string) (WebElement, error) { 475 | p := params{"using": using, "value": value} 476 | _, data, err := s.wd.do(p, "POST", "/session/%s/element", s.Id) 477 | if err != nil { 478 | return WebElement{}, err 479 | } 480 | var elem element 481 | err = json.Unmarshal(data, &elem) 482 | return WebElement{&s, elem.ELEMENT}, err 483 | } 484 | 485 | //Search for multiple elements on the page, starting from the document root. 486 | func (s Session) FindElements(using FindElementStrategy, value string) ([]WebElement, error) { 487 | p := params{"using": using, "value": value} 488 | _, data, err := s.wd.do(p, "POST", "/session/%s/elements", s.Id) 489 | if err != nil { 490 | return nil, err 491 | } 492 | var v []element 493 | err = json.Unmarshal(data, &v) 494 | if err != nil { 495 | return nil, err 496 | } 497 | elements := make([]WebElement, len(v)) 498 | for i, elem := range v { 499 | elements[i] = WebElement{&s, elem.ELEMENT} 500 | } 501 | return elements, err 502 | } 503 | 504 | //Get the element on the page that currently has focus. 505 | func (s Session) GetActiveElement() (WebElement, error) { 506 | _, data, err := s.wd.do(nil, "POST", "/session/%s/element/active", s.Id) 507 | if err != nil { 508 | return WebElement{}, err 509 | } 510 | var elem element 511 | err = json.Unmarshal(data, &elem) 512 | return WebElement{&s, elem.ELEMENT}, err 513 | } 514 | 515 | //Describe the identified element. This command is reserved for future use; its return type is currently undefined. 516 | /*func (e WebElement) Id() error { 517 | // GET /session/:sessionId/element/:id 518 | }*/ 519 | 520 | //Search for an element on the page, starting from the identified element. 521 | func (e WebElement) FindElement(using FindElementStrategy, value string) (WebElement, error) { 522 | p := params{"using": using, "value": value} 523 | _, data, err := e.s.wd.do(p, "POST", "/session/%s/element/%s/element", e.s.Id, e.id) 524 | if err != nil { 525 | return WebElement{}, err 526 | } 527 | var elem element 528 | err = json.Unmarshal(data, &elem) 529 | return WebElement{e.s, elem.ELEMENT}, err 530 | } 531 | 532 | //Search for multiple elements on the page, starting from the identified element. 533 | func (e WebElement) FindElements(using FindElementStrategy, value string) ([]WebElement, error) { 534 | p := params{"using": using, "value": value} 535 | _, data, err := e.s.wd.do(p, "POST", "/session/%s/element/%s/elements", e.s.Id, e.id) 536 | if err != nil { 537 | return nil, err 538 | } 539 | var v []element 540 | err = json.Unmarshal(data, &v) 541 | if err != nil { 542 | return nil, err 543 | } 544 | elements := make([]WebElement, len(v)) 545 | for i, z := range v { 546 | elements[i] = WebElement{e.s, z.ELEMENT} 547 | } 548 | return elements, err 549 | } 550 | 551 | //Click on an element. 552 | func (e WebElement) Click() error { 553 | _, _, err := e.s.wd.do(nil, "POST", "/session/%s/element/%s/click", e.s.Id, e.id) 554 | return err 555 | } 556 | 557 | //Submit a FORM element. 558 | func (e WebElement) Submit() error { 559 | _, _, err := e.s.wd.do(nil, "POST", "/session/%s/element/%s/submit", e.s.Id, e.id) 560 | return err 561 | } 562 | 563 | //Returns the visible text for the element. 564 | func (e WebElement) Text() (string, error) { 565 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/text", e.s.Id, e.id) 566 | if err != nil { 567 | return "", err 568 | } 569 | var text string 570 | err = json.Unmarshal(data, &text) 571 | return text, err 572 | } 573 | 574 | //Send a sequence of key strokes to an element. 575 | func (e WebElement) SendKeys(sequence string) error { 576 | keys := make([]string, len(sequence)) 577 | for i, k := range sequence { 578 | keys[i] = string(k) 579 | } 580 | p := params{"value": keys} 581 | _, _, err := e.s.wd.do(p, "POST", "/session/%s/element/%s/value", e.s.Id, e.id) 582 | return err 583 | } 584 | 585 | //Send a sequence of key strokes to the active element. 586 | func (s Session) SendKeysOnActiveElement(sequence string) error { 587 | keys := make([]string, len(sequence)) 588 | for i, k := range sequence { 589 | keys[i] = string(k) 590 | } 591 | p := params{"value": keys} 592 | _, _, err := s.wd.do(p, "POST", "/session/%s/keys", s.Id) 593 | return err 594 | } 595 | 596 | //Query for an element's tag name. 597 | func (e WebElement) Name() (string, error) { 598 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/name", e.s.Id, e.id) 599 | if err != nil { 600 | return "", err 601 | } 602 | var name string 603 | err = json.Unmarshal(data, &name) 604 | return name, err 605 | } 606 | 607 | //Clear a TEXTAREA or text INPUT element's value. 608 | func (e WebElement) Clear() error { 609 | _, _, err := e.s.wd.do(nil, "POST", "/session/%s/element/%s/clear", e.s.Id, e.id) 610 | return err 611 | } 612 | 613 | //Determine if an OPTION element, or an INPUT element of type checkbox or radiobutton is currently selected. 614 | func (e WebElement) IsSelected() (bool, error) { 615 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/value", e.s.Id, e.id) 616 | if err != nil { 617 | return false, err 618 | } 619 | var isSelected bool 620 | err = json.Unmarshal(data, &isSelected) 621 | return isSelected, err 622 | } 623 | 624 | //Determine if an element is currently enabled. 625 | func (e WebElement) IsEnabled() (bool, error) { 626 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/enabled", e.s.Id, e.id) 627 | if err != nil { 628 | return false, err 629 | } 630 | var isEnabled bool 631 | err = json.Unmarshal(data, &isEnabled) 632 | return isEnabled, err 633 | } 634 | 635 | //Get the value of an element's attribute. 636 | func (e WebElement) GetAttribute(name string) (string, error) { 637 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/attribute/%s", e.s.Id, e.id, name) 638 | if err != nil { 639 | return "", err 640 | } 641 | var attribute string 642 | err = json.Unmarshal(data, &attribute) 643 | return attribute, err 644 | //return z, e.do("GET", u, nil, &z) 645 | } 646 | 647 | //Test if two element IDs refer to the same DOM element. 648 | func (e WebElement) Equal(element WebElement) (bool, error) { 649 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/equal/%s", e.s.Id, e.id, element.id) 650 | if err != nil { 651 | return false, err 652 | } 653 | var equal bool 654 | err = json.Unmarshal(data, &equal) 655 | return equal, err 656 | } 657 | 658 | //Determine if an element is currently displayed. 659 | func (e WebElement) IsDisplayed() (bool, error) { 660 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/displayed", e.s.Id, e.id) 661 | if err != nil { 662 | return false, err 663 | } 664 | var isDisplayed bool 665 | err = json.Unmarshal(data, &isDisplayed) 666 | return isDisplayed, err 667 | } 668 | 669 | //Determine an element's location on the page. 670 | //The point (0, 0) refers to the upper-left corner of the page. The element's coordinates are returned as a JSON object with x and y properties. 671 | func (e WebElement) GetLocation() (Position, error) { 672 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/location", e.s.Id, e.id) 673 | if err != nil { 674 | return Position{}, err 675 | } 676 | var position Position 677 | err = json.Unmarshal(data, &position) 678 | return position, err 679 | } 680 | 681 | //Determine an element's location on the screen once it has been scrolled into view. 682 | // 683 | //Note: This is considered an internal command and should only be used to determine an element's location for correctly generating native events. 684 | func (e WebElement) GetLocationInView() (Position, error) { 685 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/location_in_view", e.s.Id, e.id) 686 | if err != nil { 687 | return Position{}, err 688 | } 689 | var position Position 690 | err = json.Unmarshal(data, &position) 691 | return position, err 692 | } 693 | 694 | //Determine an element's size in pixels. 695 | func (e WebElement) Size() (Size, error) { 696 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/size", e.s.Id, e.id) 697 | if err != nil { 698 | return Size{}, err 699 | } 700 | var size Size 701 | err = json.Unmarshal(data, &size) 702 | return size, err 703 | } 704 | 705 | //Query the value of an element's computed CSS property. 706 | func (e WebElement) GetCssProperty(name string) (string, error) { 707 | _, data, err := e.s.wd.do(nil, "GET", "/session/%s/element/%s/css/%s", e.s.Id, e.id, name) 708 | if err != nil { 709 | return "", err 710 | } 711 | var cssProperty string 712 | err = json.Unmarshal(data, &cssProperty) 713 | return cssProperty, err 714 | } 715 | 716 | type ScreenOrientation string 717 | 718 | const ( 719 | //TODO what is actually returned? 720 | LANDSCAPE = iota 721 | PORTRAIT 722 | ) 723 | 724 | //Get the current browser orientation. 725 | func (s Session) GetOrientation() (ScreenOrientation, error) { 726 | _, data, err := s.wd.do(nil, "GET", "/session/%s/orientation", s.Id) 727 | if err != nil { 728 | return "", err 729 | } 730 | var orientation ScreenOrientation 731 | err = json.Unmarshal(data, &orientation) 732 | return orientation, err 733 | } 734 | 735 | //Set the browser orientation. 736 | func (s Session) SetOrientation(orientation ScreenOrientation) error { 737 | p := params{"orientation": orientation} 738 | _, _, err := s.wd.do(p, "POST", "/session/%s/orientation", s.Id) 739 | return err 740 | } 741 | 742 | //Gets the text of the currently displayed JavaScript alert(), confirm(), or prompt() dialog. 743 | func (s Session) GetAlertText() (string, error) { 744 | _, data, err := s.wd.do(nil, "GET", "/session/%s/alert_text", s.Id) 745 | if err != nil { 746 | return "", err 747 | } 748 | var alertText string 749 | err = json.Unmarshal(data, &alertText) 750 | return alertText, err 751 | } 752 | 753 | //Sends keystrokes to a JavaScript prompt() dialog. 754 | func (s Session) SetAlertText(text string) error { 755 | p := params{"text": text} 756 | _, _, err := s.wd.do(p, "POST", "/session/%s/alert_text", s.Id) 757 | return err 758 | } 759 | 760 | //Accepts the currently displayed alert dialog. 761 | func (s Session) AcceptAlert() error { 762 | _, _, err := s.wd.do(nil, "POST", "/session/%s/accept_alert", s.Id) 763 | return err 764 | } 765 | 766 | //Dismisses the currently displayed alert dialog. 767 | func (s Session) DismissAlert() error { 768 | _, _, err := s.wd.do(nil, "POST", "/session/%s/dismiss_alert", s.Id) 769 | return err 770 | } 771 | 772 | //Move the mouse by an offset of the specificed element. 773 | //If no element is specified, the move is relative to the current mouse cursor. If an element is provided but no offset, the mouse will be moved to the center of the element. If the element is not visible, it will be scrolled into view. 774 | func (s Session) MoveTo(element WebElement, xoffset, yoffset int) error { 775 | p := params{"element": element.id, "xoffset": xoffset, "yoffset": yoffset} 776 | _, _, err := s.wd.do(p, "POST", "/session/%s/moveto", s.Id) 777 | return err 778 | } 779 | 780 | type MouseButton int 781 | 782 | const ( 783 | LeftButton = MouseButton(0) 784 | MiddleButton = MouseButton(1) 785 | RightButton = MouseButton(2) 786 | ) 787 | 788 | //Click any mouse button (at the coordinates set by the last moveto command). 789 | // 790 | //Note that calling this command after calling buttondown and before calling button up (or any out-of-order interactions sequence) will yield undefined behaviour). 791 | func (s Session) Click(button MouseButton) error { 792 | p := params{"button": button} 793 | _, _, err := s.wd.do(p, "POST", "/session/%s/click", s.Id) 794 | return err 795 | } 796 | 797 | //Click and hold the left mouse button (at the coordinates set by the last moveto command). 798 | func (s Session) ButtonDown(button MouseButton) error { 799 | p := params{"button": button} 800 | _, _, err := s.wd.do(p, "POST", "/session/%s/buttondown", s.Id) 801 | return err 802 | } 803 | 804 | //Releases the mouse button previously held (where the mouse is currently at). 805 | func (s Session) ButtonUp(button MouseButton) error { 806 | p := params{"button": button} 807 | _, _, err := s.wd.do(p, "POST", "/session/%s/buttonup", s.Id) 808 | return err 809 | } 810 | 811 | //Double-clicks at the current mouse coordinates (set by moveto). 812 | func (s Session) DoubleClick() error { 813 | _, _, err := s.wd.do(nil, "POST", "/session/%s/doubleclick", s.Id) 814 | return err 815 | } 816 | 817 | //Single tap on the touch enabled device. 818 | func (s Session) TouchClick(element WebElement) error { 819 | p := params{"element": element.id} 820 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/click", s.Id) 821 | return err 822 | } 823 | 824 | //Finger down on the screen. 825 | func (s Session) TouchDown(x, y int) error { 826 | p := params{"x": x, "y": y} 827 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/down", s.Id) 828 | return err 829 | } 830 | 831 | //Finger up on the screen. 832 | func (s Session) TouchUp(x, y int) error { 833 | p := params{"x": x, "y": y} 834 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/up", s.Id) 835 | return err 836 | } 837 | 838 | //Finger move on the screen. 839 | func (s Session) TouchMove(x, y int) error { 840 | p := params{"x": x, "y": y} 841 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/move", s.Id) 842 | return err 843 | } 844 | 845 | //Scroll on the touch screen using finger based motion events. 846 | func (s Session) TouchScroll(element WebElement, xoffset, yoffset int) error { 847 | p := params{"element": element.id, "xoffset": xoffset, "yoffset": yoffset} 848 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/scroll", s.Id) 849 | return err 850 | } 851 | 852 | //Double tap on the touch screen using finger motion events. 853 | func (s Session) TouchDoubleClick(element WebElement) error { 854 | p := params{"element": element.id} 855 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/doubleclick", s.Id) 856 | return err 857 | } 858 | 859 | //Long press on the touch screen using finger motion events. 860 | func (s Session) TouchLongClick(element WebElement) error { 861 | p := params{"element": element.id} 862 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/longclick", s.Id) 863 | return err 864 | } 865 | 866 | //Flick on the touch screen using finger motion events. 867 | //This flickcommand starts at a particulat screen location. 868 | func (s Session) TouchFlick(element WebElement, xoffset, yoffset, speed int) error { 869 | p := params{"element": element.id, "xoffset": xoffset, "yoffset": yoffset, "speed": speed} 870 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/flick", s.Id) 871 | return err 872 | } 873 | 874 | //Flick on the touch screen using finger motion events. 875 | //Use this flick command if you don't care where the flick starts on the screen. 876 | func (s Session) TouchFlickAnywhere(xspeed, yspeed int) error { 877 | p := params{"xspeed": xspeed, "yspeed": yspeed} 878 | _, _, err := s.wd.do(p, "POST", "/session/%s/touch/flick", s.Id) 879 | return err 880 | } 881 | 882 | //Get the current geo location. 883 | func (s Session) GetGeoLocation() (GeoLocation, error) { 884 | _, data, err := s.wd.do(nil, "GET", "/session/%s/location", s.Id) 885 | if err != nil { 886 | return GeoLocation{}, err 887 | } 888 | var location GeoLocation 889 | err = json.Unmarshal(data, &location) 890 | return location, err 891 | } 892 | 893 | //Set the current geo location. 894 | func (s Session) SetGeoLocation(location GeoLocation) error { 895 | p := params{"location": location} 896 | _, _, err := s.wd.do(p, "POST", "/session/%s/location", s.Id) 897 | return err 898 | } 899 | 900 | //helper functions, storageType can be "local_storage" or "session_storage" 901 | func (s Session) storageGetKeys(storageType string) ([]string, error) { 902 | _, data, err := s.wd.do(nil, "GET", "/session/%s/%s", s.Id, storageType) 903 | if err != nil { 904 | return nil, err 905 | } 906 | var keys []string 907 | err = json.Unmarshal(data, &keys) 908 | return keys, err 909 | } 910 | 911 | func (s Session) storageSetKey(storageType, key, value string) error { 912 | p := params{"key": key, "value": value} 913 | _, _, err := s.wd.do(p, "POST", "/session/%s/%s", s.Id, storageType) 914 | return err 915 | } 916 | 917 | func (s Session) storageClear(storageType string) error { 918 | _, _, err := s.wd.do(nil, "DELETE", "/session/%s/%s", s.Id, storageType) 919 | return err 920 | } 921 | 922 | //TODO protocol specification doesn't specify what is returned, I guess a string 923 | func (s Session) storageGetKey(storageType, key string) (string, error) { 924 | _, data, err := s.wd.do(nil, "GET", "/session/%s/%s/key/%s", s.Id, storageType, key) 925 | if err != nil { 926 | return "", err 927 | } 928 | var value string 929 | err = json.Unmarshal(data, &value) 930 | return value, err 931 | } 932 | 933 | func (s Session) storageRemoveKey(storageType string, key string) error { 934 | _, _, err := s.wd.do(nil, "DELETE", "/session/%s/%s/key/%s", s.Id, storageType, key) 935 | return err 936 | } 937 | 938 | //Get the number of items in the storage. 939 | func (s Session) storageSize(storageType string) (int, error) { 940 | _, data, err := s.wd.do(nil, "GET", "/session/%s/%s/size", s.Id, storageType) 941 | if err != nil { 942 | return -1, err 943 | } 944 | var size int 945 | err = json.Unmarshal(data, &size) 946 | return size, err 947 | } 948 | 949 | //Get all keys of the storage. 950 | func (s Session) LocalStorageGetKeys() ([]string, error) { 951 | return s.storageGetKeys("local_storage") 952 | } 953 | 954 | //Set the storage item for the given key. 955 | func (s Session) LocalStorageSetKey(key, value string) error { 956 | return s.storageSetKey("local_storage", key, value) 957 | } 958 | 959 | //Clear the storage. 960 | func (s Session) LocalStorageClear() error { 961 | return s.storageClear("local_storage") 962 | } 963 | 964 | //Get the storage item for the given key. 965 | func (s Session) LocalStorageGetKey(key string) (string, error) { 966 | return s.storageGetKey("local_storage", key) 967 | } 968 | 969 | //Remove the storage item for the given key. 970 | func (s Session) LocalStorageRemoveKey(key string) error { 971 | return s.storageRemoveKey("local_storage", key) 972 | } 973 | 974 | //Get the number of items in the storage. 975 | func (s Session) LocalStorageSize() (int, error) { 976 | return s.storageSize("local_storage") 977 | } 978 | 979 | //Get all keys of the storage. 980 | func (s Session) SessionStorageGetKeys() ([]string, error) { 981 | return s.storageGetKeys("session_storage") 982 | } 983 | 984 | //Set the storage item for the given key. 985 | func (s Session) SessionStorageSetKey(key, value string) error { 986 | return s.storageSetKey("session_storage", key, value) 987 | } 988 | 989 | //Clear the storage. 990 | func (s Session) SessionStorageClear() error { 991 | return s.storageClear("session_storage") 992 | } 993 | 994 | //Get the storage item for the given key. 995 | func (s Session) SessionStorageGetKey(key string) (string, error) { 996 | return s.storageGetKey("session_storage", key) 997 | } 998 | 999 | //Remove the storage item for the given key. 1000 | func (s Session) SessionStorageRemoveKey(key string) error { 1001 | return s.storageRemoveKey("session_storage", key) 1002 | } 1003 | 1004 | //Get the number of items in the storage. 1005 | func (s Session) SessionStorageSize() (int, error) { 1006 | return s.storageSize("session_storage") 1007 | } 1008 | 1009 | //Get the log for a given log type. 1010 | func (s Session) Log(logType string) ([]LogEntry, error) { 1011 | p := params{"type": logType} 1012 | _, data, err := s.wd.do(p, "POST", "/session/%s/log", s.Id) 1013 | if err != nil { 1014 | return nil, err 1015 | } 1016 | var log []LogEntry 1017 | err = json.Unmarshal(data, &log) 1018 | return log, err 1019 | } 1020 | 1021 | //Get available log types. 1022 | func (s Session) LogTypes() ([]string, error) { 1023 | _, data, err := s.wd.do(nil, "GET", "/session/%s/log/types", s.Id) 1024 | if err != nil { 1025 | return nil, err 1026 | } 1027 | var logTypes []string 1028 | err = json.Unmarshal(data, &logTypes) 1029 | return logTypes, err 1030 | } 1031 | 1032 | //Get the status of the html5 application cache. 1033 | func (s Session) GetHTML5CacheStatus() (HTML5CacheStatus, error) { 1034 | _, data, err := s.wd.do(nil, "GET", "/session/%s/application_cache/status", s.Id) 1035 | if err != nil { 1036 | return 0, err 1037 | } 1038 | var cacheStatus HTML5CacheStatus 1039 | err = json.Unmarshal(data, &cacheStatus) 1040 | return cacheStatus, err 1041 | } 1042 | -------------------------------------------------------------------------------- /webdriver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Federico Sogaro. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package webdriver 6 | 7 | import ( 8 | "bytes" 9 | "flag" 10 | "fmt" 11 | "image/png" 12 | "net" 13 | "net/http" 14 | "os" 15 | "path/filepath" 16 | "strconv" 17 | "strings" 18 | "testing" 19 | "time" 20 | ) 21 | 22 | var ( 23 | target = flag.String("target", "", "target driver (chrome|firefox)") 24 | wdpath = flag.String("wdpath", "", "path to chromedriver (chrome) or webdriver.xpi (firefox)") 25 | wdlog = flag.String("wdlogdir", "", "dir where to dump log files") 26 | ) 27 | 28 | func init() { 29 | debug = true 30 | } 31 | 32 | var ( 33 | wd WebDriver 34 | session *Session 35 | addr string 36 | ) 37 | 38 | var pages = [][]string{ 39 | {"simple", `
This is a longwordlinktogolang to a page served by a go server.
51 |