github.com/bazelbuild/rules_webtesting@v0.2.0/go/webdriver/webdriver.go (about)

     1  // Copyright 2016 Google Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package webdriver provides a simple and incomplete WebDriver client for use by web test launcher.
    16  package webdriver
    17  
    18  import (
    19  	"bytes"
    20  	"context"
    21  	"encoding/base64"
    22  	"encoding/json"
    23  	"fmt"
    24  	"image"
    25  	"image/png"
    26  	"io"
    27  	"io/ioutil"
    28  	"log"
    29  	"net/http"
    30  	"net/url"
    31  	"path"
    32  	"strings"
    33  	"time"
    34  
    35  	"github.com/bazelbuild/rules_webtesting/go/errors"
    36  	"github.com/bazelbuild/rules_webtesting/go/healthreporter"
    37  	"github.com/bazelbuild/rules_webtesting/go/metadata/capabilities"
    38  )
    39  
    40  const (
    41  	compName           = "Go WebDriver Client"
    42  	seleniumElementKey = "ELEMENT"
    43  	w3cElementKey      = "element-6066-11e4-a52e-4f735466cecf"
    44  )
    45  
    46  // WebDriver provides access to a running WebDriver session
    47  type WebDriver interface {
    48  	healthreporter.HealthReporter
    49  	// ExecuteScript executes script inside the browser's current execution context.
    50  	ExecuteScript(ctx context.Context, script string, args []interface{}, value interface{}) error
    51  	// ExecuteScriptAsync executes script asynchronously inside the browser's current execution context.
    52  	ExecuteScriptAsync(ctx context.Context, script string, args []interface{}, value interface{}) error
    53  	// ExecuteScriptAsyncWithTimeout executes the script asynchronously, but sets the script timeout to timeout before,
    54  	// and attempts to restore it to its previous value after.
    55  	ExecuteScriptAsyncWithTimeout(ctx context.Context, timeout time.Duration, script string, args []interface{}, value interface{}) error
    56  	// Quit closes the WebDriver session.
    57  	Quit(context.Context) error
    58  	// CommandURL builds a fully resolved URL for the specified end-point.
    59  	CommandURL(endpoint ...string) (*url.URL, error)
    60  	// SetScriptTimeout sets the timeout for the callback of an ExecuteScriptAsync call to be called.
    61  	SetScriptTimeout(context.Context, time.Duration) error
    62  	// Logs gets logs of the specified type from the remote end.
    63  	Logs(ctx context.Context, logType string) ([]LogEntry, error)
    64  	// SessionID returns the id for this session.
    65  	SessionID() string
    66  	// Address returns the base address for this sessions (ending with session/<SessionID>)
    67  	Address() *url.URL
    68  	// Capabilities returns the capabilities returned from the remote end when session was created.
    69  	Capabilities() map[string]interface{}
    70  	// Screenshot takes a screenshot of the current browser window.
    71  	Screenshot(context.Context) (image.Image, error)
    72  	// WindowHandles returns a slice of the current window handles.
    73  	WindowHandles(context.Context) ([]string, error)
    74  	// ElementFromID returns a new WebElement object for the given id.
    75  	ElementFromID(string) WebElement
    76  	// ElementFromMap returns a new WebElement from a map representing a JSON object.
    77  	ElementFromMap(map[string]interface{}) (WebElement, error)
    78  	// GetWindowRect returns the current windows size and location.
    79  	GetWindowRect(context.Context) (Rectangle, error)
    80  	// SetWindowRect sets the current window size and location.
    81  	SetWindowRect(context.Context, Rectangle) error
    82  	// SetWindowSize sets the current window size.
    83  	SetWindowSize(ctx context.Context, width, height float64) error
    84  	// SetWindowPosition sest the current window position.
    85  	SetWindowPosition(ctx context.Context, x, y float64) error
    86  	// W3C return true iff connected to a W3C compliant remote end.
    87  	W3C() bool
    88  	// CurrentURL returns the URL that the current browser window is looking at.
    89  	CurrentURL(context.Context) (*url.URL, error)
    90  	// PageSource returns the source of the current browsing context active document.
    91  	PageSource(context.Context) (string, error)
    92  	// NavigateTo navigates the controlled browser to the specified URL.
    93  	NavigateTo(context.Context, *url.URL) error
    94  }
    95  
    96  // WebElement provides access to a specific DOM element in a WebDriver session.
    97  type WebElement interface {
    98  	// ID returns the WebDriver element id.
    99  	ID() string
   100  	// ToMap returns a Map representation of a WebElement suitable for use in other WebDriver commands.
   101  	ToMap() map[string]string
   102  	// ScrollIntoView scrolls a WebElement to the top of the browsers viewport.
   103  	ScrollIntoView(ctx context.Context) error
   104  	// Bounds returns the bounds of the WebElement within the viewport.
   105  	// This will not scroll the element into the viewport first.
   106  	// Will return an error if the element is not in the viewport.
   107  	Bounds(ctx context.Context) (image.Rectangle, error)
   108  }
   109  
   110  // Rectangle represents a window's position and size.
   111  type Rectangle struct {
   112  	X      float64 `json:"x"`
   113  	Y      float64 `json:"y"`
   114  	Width  float64 `json:"width"`
   115  	Height float64 `json:"height"`
   116  }
   117  
   118  // LogEntry is an entry parsed from the logs retrieved from the remote WebDriver.
   119  type LogEntry struct {
   120  	Timestamp float64 `json:"timestamp"`
   121  	Level     string  `json:"level"`
   122  	Message   string  `json:"message"`
   123  }
   124  
   125  type webDriver struct {
   126  	address       *url.URL
   127  	sessionID     string
   128  	capabilities  map[string]interface{}
   129  	client        *http.Client
   130  	scriptTimeout time.Duration
   131  	w3c           bool
   132  }
   133  
   134  type webElement struct {
   135  	driver *webDriver
   136  	id     string
   137  }
   138  
   139  type jsonResp struct {
   140  	Status     *int        `json:"status"`
   141  	SessionID  string      `json:"sessionId"`
   142  	Value      interface{} `json:"value"`
   143  	Error      string      `json:"error"`
   144  	Message    string      `json:"message"`
   145  	StackTrace interface{} `json:"stacktrace"`
   146  }
   147  
   148  func (j *jsonResp) isError() bool {
   149  	if j.Status != nil && *j.Status != 0 {
   150  		return true
   151  	}
   152  
   153  	if j.Error != "" {
   154  		return true
   155  	}
   156  
   157  	value, ok := j.Value.(map[string]interface{})
   158  	if !ok {
   159  		return false
   160  	}
   161  
   162  	e, ok := value["error"].(string)
   163  	return ok && e != ""
   164  }
   165  
   166  // CreateSession creates a new WebDriver session with desired capabilities from server at addr
   167  // and ensures that the browser connection is working. It retries up to attempts - 1 times.
   168  func CreateSession(ctx context.Context, addr string, attempts int, requestedCaps *capabilities.Capabilities) (WebDriver, error) {
   169  	reqBody := requestedCaps.ToMixedMode()
   170  
   171  	urlPrefix, err := url.Parse(addr)
   172  	if err != nil {
   173  		return nil, errors.New(compName, err)
   174  	}
   175  
   176  	urlSuffix, err := url.Parse("session/")
   177  	if err != nil {
   178  		return nil, errors.New(compName, err)
   179  	}
   180  
   181  	fullURL := urlPrefix.ResolveReference(urlSuffix)
   182  	c, err := command(fullURL, "")
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	client := &http.Client{}
   188  
   189  	for ; attempts > 0; attempts-- {
   190  		d, err := func() (*webDriver, error) {
   191  			respBody, err := post(ctx, client, c, reqBody, nil)
   192  			if err != nil {
   193  				return nil, err
   194  			}
   195  
   196  			val, ok := respBody.Value.(map[string]interface{})
   197  			if !ok {
   198  				return nil, errors.New(compName, fmt.Errorf("value field must be an object in %+v", respBody))
   199  			}
   200  
   201  			var caps map[string]interface{}
   202  
   203  			session := respBody.SessionID
   204  			if session != "" {
   205  				// OSS protocol puts Session ID at the top level:
   206  				// {
   207  				//   "value": { capabilities object },
   208  				//   "sessionId": "id",
   209  				//   "status": 0
   210  				// }
   211  				caps = val
   212  			} else {
   213  				// W3C protocol wraps everything in a "value" key:
   214  				// {
   215  				//   "value": {
   216  				//     "capabilities": { capabilities object },
   217  				//     "sessionId": "id"
   218  				//   }
   219  				// }
   220  				session, _ = val["sessionId"].(string)
   221  				if session == "" {
   222  					return nil, errors.New(compName, fmt.Errorf("no session id specified in %+v", respBody))
   223  				}
   224  				caps, ok = val["capabilities"].(map[string]interface{})
   225  				if !ok {
   226  					return nil, errors.New(compName, fmt.Errorf("no capabilities in value of %+v", respBody))
   227  				}
   228  			}
   229  
   230  			sessionURL, err := url.Parse(session + "/")
   231  			if err != nil {
   232  				return nil, errors.New(compName, err)
   233  			}
   234  
   235  			d := &webDriver{
   236  				address:       fullURL.ResolveReference(sessionURL),
   237  				sessionID:     session,
   238  				capabilities:  caps,
   239  				client:        client,
   240  				scriptTimeout: scriptTimeout(requestedCaps),
   241  				w3c:           respBody.Status == nil,
   242  			}
   243  
   244  			if err := d.Healthy(ctx); err != nil {
   245  				if err := d.Quit(ctx); err != nil {
   246  					log.Printf("error quitting WebDriver session: %v", err)
   247  				}
   248  				return nil, err
   249  			}
   250  			return d, nil
   251  		}()
   252  
   253  		if err == nil {
   254  			return d, nil
   255  		}
   256  		if errors.IsPermanent(err) || attempts <= 1 {
   257  			return nil, err
   258  		}
   259  	}
   260  
   261  	// This should only occur if called with attempts <= 0
   262  	return nil, errors.New(compName, fmt.Errorf("attempts %d <= 0", attempts))
   263  }
   264  
   265  func (d *webDriver) W3C() bool {
   266  	return d.w3c
   267  }
   268  
   269  func (d *webDriver) Address() *url.URL {
   270  	return d.address
   271  }
   272  
   273  func (d *webDriver) Capabilities() map[string]interface{} {
   274  	return d.capabilities
   275  }
   276  
   277  func (d *webDriver) SessionID() string {
   278  	return d.sessionID
   279  }
   280  
   281  func (*webDriver) Name() string {
   282  	return compName
   283  }
   284  
   285  func (d *webDriver) Healthy(ctx context.Context) error {
   286  	return d.ExecuteScript(ctx, "return navigator.userAgent", nil, nil)
   287  }
   288  
   289  func (d *webDriver) ExecuteScript(ctx context.Context, script string, args []interface{}, value interface{}) error {
   290  	if args == nil {
   291  		args = []interface{}{}
   292  	}
   293  	body := map[string]interface{}{
   294  		"script": script,
   295  		"args":   args,
   296  	}
   297  	command := "execute"
   298  	if d.W3C() {
   299  		command = "execute/sync"
   300  	}
   301  	return d.post(ctx, command, body, value)
   302  }
   303  
   304  func (d *webDriver) ExecuteScriptAsync(ctx context.Context, script string, args []interface{}, value interface{}) error {
   305  	if args == nil {
   306  		args = []interface{}{}
   307  	}
   308  	body := map[string]interface{}{
   309  		"script": script,
   310  		"args":   args,
   311  	}
   312  	command := "execute_async"
   313  	if d.W3C() {
   314  		command = "execute/async"
   315  	}
   316  	err := d.post(ctx, command, body, value)
   317  	return err
   318  }
   319  
   320  func (d *webDriver) ExecuteScriptAsyncWithTimeout(ctx context.Context, timeout time.Duration, script string, args []interface{}, value interface{}) error {
   321  	if err := d.setScriptTimeout(ctx, timeout); err != nil {
   322  		log.Printf("error setting script timeout to %v", timeout)
   323  	}
   324  	if d.scriptTimeout != 0 {
   325  		defer func() {
   326  			if err := d.setScriptTimeout(ctx, d.scriptTimeout); err != nil {
   327  				log.Printf("error restoring script timeout to %v", d.scriptTimeout)
   328  			}
   329  		}()
   330  	}
   331  	return d.ExecuteScriptAsync(ctx, script, args, value)
   332  }
   333  
   334  // CurrentURL returns the URL that the current browser window is looking at.
   335  func (d *webDriver) CurrentURL(ctx context.Context) (*url.URL, error) {
   336  	var result string
   337  
   338  	if err := d.get(ctx, "url", &result); err != nil {
   339  		return nil, err
   340  	}
   341  
   342  	current, err := url.Parse(result)
   343  	if err != nil {
   344  		return current, errors.New(d.Name(), err)
   345  	}
   346  	return current, nil
   347  }
   348  
   349  // PageSource returns the source of the current browsing context active document.
   350  func (d *webDriver) PageSource(ctx context.Context) (string, error) {
   351  	var result string
   352  
   353  	if err := d.get(ctx, "source", &result); err != nil {
   354  		return "", err
   355  	}
   356  	return result, nil
   357  }
   358  
   359  // NavigateTo navigates the controlled browser to the specified URL.
   360  func (d *webDriver) NavigateTo(ctx context.Context, u *url.URL) error {
   361  	return d.post(ctx, "url", map[string]interface{}{
   362  		"url": u.String(),
   363  	}, nil)
   364  }
   365  
   366  // Screenshot takes a screenshot of the current browser window.
   367  func (d *webDriver) Screenshot(ctx context.Context) (image.Image, error) {
   368  	var value string
   369  	if err := d.get(ctx, "screenshot", &value); err != nil {
   370  		return nil, err
   371  	}
   372  	return png.Decode(base64.NewDecoder(base64.StdEncoding, strings.NewReader(value)))
   373  }
   374  
   375  // WindowHandles returns a slice of the current window handles.
   376  func (d *webDriver) WindowHandles(ctx context.Context) ([]string, error) {
   377  	var value []string
   378  	command := "window_handles"
   379  	if d.W3C() {
   380  		command = "window/handles"
   381  	}
   382  	if err := d.get(ctx, command, &value); err != nil {
   383  		return nil, err
   384  	}
   385  	return value, nil
   386  }
   387  
   388  func (d *webDriver) GetWindowRect(ctx context.Context) (result Rectangle, err error) {
   389  	if d.W3C() {
   390  		err = d.get(ctx, "window/rect", &result)
   391  		return
   392  	}
   393  
   394  	err = d.get(ctx, "window/current/size", &result)
   395  	if err != nil {
   396  		return
   397  	}
   398  	err = d.get(ctx, "window/current/position", &result)
   399  	return
   400  }
   401  
   402  func (d *webDriver) SetWindowRect(ctx context.Context, rect Rectangle) error {
   403  	if d.W3C() {
   404  		return d.post(ctx, "window/rect", rect, nil)
   405  	}
   406  
   407  	if err := d.SetWindowSize(ctx, rect.Width, rect.Height); err != nil {
   408  		return err
   409  	}
   410  
   411  	return d.SetWindowPosition(ctx, rect.X, rect.Y)
   412  }
   413  
   414  func (d *webDriver) SetWindowSize(ctx context.Context, width, height float64) error {
   415  	args := map[string]float64{
   416  		"width":  width,
   417  		"height": height,
   418  	}
   419  	command := "window/current/size"
   420  	if d.W3C() {
   421  		command = "window/rect"
   422  	}
   423  	return d.post(ctx, command, args, nil)
   424  }
   425  
   426  func (d *webDriver) SetWindowPosition(ctx context.Context, x, y float64) error {
   427  	args := map[string]float64{
   428  		"x": x,
   429  		"y": y,
   430  	}
   431  	command := "window/current/position"
   432  	if d.W3C() {
   433  		command = "window/rect"
   434  	}
   435  	return d.post(ctx, command, args, nil)
   436  }
   437  
   438  func (d *webDriver) post(ctx context.Context, suffix string, body interface{}, value interface{}) error {
   439  	c, err := d.CommandURL(suffix)
   440  	if err != nil {
   441  		return err
   442  	}
   443  
   444  	_, err = post(ctx, d.client, c, body, value)
   445  	return err
   446  }
   447  
   448  func (d *webDriver) get(ctx context.Context, suffix string, value interface{}) error {
   449  	c, err := d.CommandURL(suffix)
   450  	if err != nil {
   451  		return err
   452  	}
   453  	_, err = getReq(ctx, d.client, c, value)
   454  	return err
   455  }
   456  
   457  func (d *webDriver) delete(ctx context.Context, suffix string, value interface{}) error {
   458  	c, err := d.CommandURL(suffix)
   459  	if err != nil {
   460  		return err
   461  	}
   462  	_, err = deleteReq(ctx, d.client, c, value)
   463  	return err
   464  }
   465  
   466  func (d *webDriver) Quit(ctx context.Context) error {
   467  	return d.delete(ctx, "", nil)
   468  }
   469  
   470  func (d *webDriver) CommandURL(endpoint ...string) (*url.URL, error) {
   471  	return command(d.Address(), endpoint...)
   472  }
   473  
   474  func (d *webDriver) SetScriptTimeout(ctx context.Context, timeout time.Duration) error {
   475  	d.scriptTimeout = timeout
   476  	return d.setScriptTimeout(ctx, timeout)
   477  }
   478  
   479  func (d *webDriver) setScriptTimeout(ctx context.Context, timeout time.Duration) error {
   480  	if d.W3C() {
   481  		return d.post(ctx, "timeouts", map[string]interface{}{
   482  			"script": int(timeout / time.Millisecond),
   483  		}, nil)
   484  	}
   485  	return d.post(ctx, "timeouts", map[string]interface{}{
   486  		"type": "script",
   487  		"ms":   int(timeout / time.Millisecond),
   488  	}, nil)
   489  }
   490  
   491  func (d *webDriver) Logs(ctx context.Context, logType string) ([]LogEntry, error) {
   492  	body := map[string]interface{}{"type": logType}
   493  	var entries []LogEntry
   494  	err := d.post(ctx, "log", body, &entries)
   495  	if err != nil {
   496  		return nil, err
   497  	}
   498  	return entries, nil
   499  }
   500  
   501  // ElementFromID returns a new WebElement object for the given id.
   502  func (d *webDriver) ElementFromID(id string) WebElement {
   503  	return &webElement{driver: d, id: id}
   504  }
   505  
   506  // ElementFromMap returns a new WebElement from a map representing a JSON object.
   507  func (d *webDriver) ElementFromMap(m map[string]interface{}) (WebElement, error) {
   508  	i, ok := m[w3cElementKey]
   509  	if !ok {
   510  		i, ok = m[seleniumElementKey]
   511  		if !ok {
   512  			return nil, errors.New(d.Name(), fmt.Errorf("map %v does not appear to represent a WebElement", m))
   513  		}
   514  	}
   515  
   516  	id, ok := i.(string)
   517  	if !ok {
   518  		return nil, errors.New(d.Name(), fmt.Errorf("map %v does not appear to represent a WebElement", m))
   519  	}
   520  	return d.ElementFromID(id), nil
   521  }
   522  
   523  func command(addr *url.URL, endpoint ...string) (*url.URL, error) {
   524  	u, err := addr.Parse(path.Join(endpoint...))
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  	return &url.URL{
   529  		Scheme: u.Scheme,
   530  		Opaque: u.Opaque,
   531  		User:   u.User,
   532  		Host:   u.Host,
   533  		// Some remote ends (notably chromedriver) do not like a trailing slash
   534  		Path:     strings.TrimRight(u.Path, "/"),
   535  		RawPath:  strings.TrimRight(u.RawPath, "/"),
   536  		RawQuery: u.RawQuery,
   537  		Fragment: u.Fragment,
   538  	}, nil
   539  }
   540  
   541  func processResponse(body io.Reader, value interface{}) (*jsonResp, error) {
   542  	bytes, err := ioutil.ReadAll(body)
   543  	if err != nil {
   544  		return nil, errors.New(compName, err)
   545  	}
   546  
   547  	respBody := &jsonResp{Value: value}
   548  	if err := json.Unmarshal(bytes, respBody); err != nil || respBody.isError() {
   549  		if value != nil {
   550  			// Reparsing to ensure we have a clean value.
   551  			respBody = &jsonResp{}
   552  
   553  			if err := json.Unmarshal(bytes, respBody); err != nil {
   554  				// The body was unparseable, so returning an error
   555  				return nil, errors.New(compName, fmt.Errorf("%v unmarshalling %q", err, string(bytes)))
   556  			}
   557  		}
   558  
   559  		if respBody.isError() {
   560  			// The remote end returned an error. Return the body and an error constructed from the body.
   561  			return respBody, newWebDriverError(respBody)
   562  		}
   563  
   564  		// The body was unparseable with the passed in value, but was otherwise parseable and not an error value.
   565  		// Return the body and an error indicating that the original parse failed.
   566  		return respBody, errors.New(compName, fmt.Errorf("%v unmarshalling %+v", err, respBody))
   567  	}
   568  
   569  	// Everything is good. Return the body.
   570  	return respBody, nil
   571  }
   572  
   573  func post(ctx context.Context, client *http.Client, command *url.URL, body interface{}, value interface{}) (*jsonResp, error) {
   574  	reqBody, err := json.Marshal(body)
   575  	if err != nil {
   576  		return nil, errors.NewPermanent(compName, err)
   577  	}
   578  
   579  	request, err := http.NewRequest("POST", command.String(), bytes.NewReader(reqBody))
   580  	if err != nil {
   581  		return nil, errors.NewPermanent(compName, err)
   582  	}
   583  
   584  	request.TransferEncoding = []string{"identity"}
   585  	request.Header.Set("Content-Type", "application/json; charset=utf-8")
   586  	request.ContentLength = int64(len(reqBody))
   587  
   588  	return doRequest(ctx, client, request, value)
   589  }
   590  
   591  func deleteReq(ctx context.Context, client *http.Client, command *url.URL, value interface{}) (*jsonResp, error) {
   592  	request, err := http.NewRequest("DELETE", command.String(), nil)
   593  	if err != nil {
   594  		return nil, errors.NewPermanent(compName, err)
   595  	}
   596  
   597  	return doRequest(ctx, client, request, value)
   598  }
   599  
   600  func getReq(ctx context.Context, client *http.Client, command *url.URL, value interface{}) (*jsonResp, error) {
   601  	request, err := http.NewRequest("GET", command.String(), nil)
   602  	if err != nil {
   603  		return nil, errors.NewPermanent(compName, err)
   604  	}
   605  
   606  	return doRequest(ctx, client, request, value)
   607  }
   608  
   609  func doRequest(ctx context.Context, client *http.Client, request *http.Request, value interface{}) (*jsonResp, error) {
   610  	request.Header.Set("Cache-Control", "no-cache")
   611  	request.Header.Set("Accept", "application/json")
   612  	request.Header.Set("Accept-Encoding", "identity")
   613  	request = request.WithContext(ctx)
   614  	resp, err := client.Do(request)
   615  	if err != nil {
   616  		return nil, errors.New(compName, err)
   617  	}
   618  
   619  	defer resp.Body.Close()
   620  	r, err := processResponse(resp.Body, value)
   621  	return r, err
   622  }
   623  
   624  // ID returns the WebDriver element id.
   625  func (e *webElement) ID() string {
   626  	return e.id
   627  }
   628  
   629  // ToMap returns a Map representation of a WebElement suitable for use in other WebDriver commands.
   630  func (e *webElement) ToMap() map[string]string {
   631  	return map[string]string{
   632  		seleniumElementKey: e.ID(),
   633  		w3cElementKey:      e.ID(),
   634  	}
   635  }
   636  
   637  // ScrollIntoView scrolls a WebElement to the top of the browsers viewport.
   638  func (e *webElement) ScrollIntoView(ctx context.Context) error {
   639  	const script = `return arguments[0].scrollIntoView(true);`
   640  	args := []interface{}{e.ToMap()}
   641  	return e.driver.ExecuteScript(ctx, script, args, nil)
   642  }
   643  
   644  // Bounds returns the bounds of the WebElement within the viewport.
   645  // This will not scroll the element into the viewport first.
   646  // Will return an error if the element is not in the viewport.
   647  func (e *webElement) Bounds(ctx context.Context) (image.Rectangle, error) {
   648  	const script = `
   649  var element = arguments[0];
   650  var rect = element.getBoundingClientRect();
   651  var top = rect.top; var left = rect.left;
   652  element = window.frameElement;
   653  var currentWindow = window.parent;
   654  while (element != null) {
   655    var currentRect = element.getBoundingClientRect();
   656    top += currentRect.top;
   657    left += currentRect.left;
   658    element = currentWindow.frameElement;
   659    currentWindow = currentWindow.parent;
   660  }
   661  return {"X0": Math.round(left), "Y0": Math.round(top), "X1": Math.round(left + rect.width), "Y1": Math.round(top + rect.height)};
   662  `
   663  	bounds := struct {
   664  		X0 int
   665  		Y0 int
   666  		X1 int
   667  		Y1 int
   668  	}{}
   669  	args := []interface{}{e.ToMap()}
   670  	err := e.driver.ExecuteScript(ctx, script, args, &bounds)
   671  	log.Printf("Err: %v", err)
   672  	return image.Rect(bounds.X0, bounds.Y0, bounds.X1, bounds.Y1), err
   673  }
   674  
   675  func scriptTimeout(caps *capabilities.Capabilities) time.Duration {
   676  	if caps == nil {
   677  		return 0
   678  	}
   679  	timeouts, ok := caps.AlwaysMatch["timeouts"].(map[string]interface{})
   680  	if !ok {
   681  		return 0
   682  	}
   683  
   684  	if script, ok := timeouts["script"].(int); ok {
   685  		return time.Duration(script) * time.Millisecond
   686  	}
   687  
   688  	if script, ok := timeouts["script"].(float64); ok {
   689  		return time.Duration(script) * time.Millisecond
   690  	}
   691  
   692  	return 0
   693  }