Permalink
535 lines (454 sloc) 14.8 KB
package agouti
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/sclevine/agouti/api"
)
// A Page represents an open browser session. Pages may be created using the
// *WebDriver.Page() method or by calling the NewPage or SauceLabs functions.
type Page struct {
selectable
logs map[string][]Log
}
// A Log represents a single log message
type Log struct {
// Message is the text of the log message.
Message string
// Location is the code location of the log message, if present
Location string
// Level is the log level ("DEBUG", "INFO", "WARNING", or "SEVERE").
Level string
// Time is the time the message was logged.
Time time.Time
}
// NewPage opens a Page using the provided WebDriver URL. This method takes
// the same Options as *WebDriver.NewPage. Unlike *WebDriver.NewPage, this
// method will respect the HTTPClient Option if provided.
func NewPage(url string, options ...Option) (*Page, error) {
pageOptions := config{}.Merge(options)
session, err := api.OpenWithClient(url, pageOptions.Capabilities(), pageOptions.HTTPClient)
if err != nil {
return nil, fmt.Errorf("failed to connect to WebDriver: %s", err)
}
return newPage(session), nil
}
// JoinPage creates a Page using existing session URL. This method takes Options
// but respects only the HTTPClient Option if provided.
func JoinPage(url string, options ...Option) *Page {
pageOptions := config{}.Merge(options)
session := api.NewWithClient(url, pageOptions.HTTPClient)
return newPage(session)
}
func newPage(session *api.Session) *Page {
return &Page{selectable{session, nil}, nil}
}
// String returns a string representation of the Page. Currently: "page"
func (p *Page) String() string {
return "page"
}
// Session returns a *api.Session that can be used to send direct commands
// to the WebDriver. See: https://code.google.com/p/selenium/wiki/JsonWireProtocol
func (p *Page) Session() *api.Session {
return p.session.(*api.Session)
}
// Destroy closes any open browsers by ending the session.
func (p *Page) Destroy() error {
if err := p.session.Delete(); err != nil {
return fmt.Errorf("failed to destroy session: %s", err)
}
return nil
}
// Reset deletes all cookies set for the current domain and navigates to a blank page.
// Unlike Destroy, Reset will permit the page to be re-used after it is called.
// Reset is faster than Destroy, but any cookies from domains outside the current
// domain will remain after a page is reset.
func (p *Page) Reset() error {
p.ConfirmPopup()
url, err := p.URL()
if err != nil {
return err
}
if url == "about:blank" {
return nil
}
if err := p.ClearCookies(); err != nil {
return err
}
if err := p.session.DeleteLocalStorage(); err != nil {
if err := p.RunScript("localStorage.clear();", nil, nil); err != nil {
return err
}
}
if err := p.session.DeleteSessionStorage(); err != nil {
if err := p.RunScript("sessionStorage.clear();", nil, nil); err != nil {
return err
}
}
return p.Navigate("about:blank")
}
// Navigate navigates to the provided URL.
func (p *Page) Navigate(url string) error {
if err := p.session.SetURL(url); err != nil {
return fmt.Errorf("failed to navigate: %s", err)
}
return nil
}
// GetCookies returns all cookies on the page.
func (p *Page) GetCookies() ([]*http.Cookie, error) {
apiCookies, err := p.session.GetCookies()
if err != nil {
return nil, fmt.Errorf("failed to get cookies: %s", err)
}
cookies := []*http.Cookie{}
for _, apiCookie := range apiCookies {
expSeconds := int64(apiCookie.Expiry)
expNano := int64(apiCookie.Expiry-float64(expSeconds)) * 1000000000
cookie := &http.Cookie{
Name: apiCookie.Name,
Value: apiCookie.Value,
Path: apiCookie.Path,
Domain: apiCookie.Domain,
Secure: apiCookie.Secure,
HttpOnly: apiCookie.HTTPOnly,
Expires: time.Unix(expSeconds, expNano),
}
cookies = append(cookies, cookie)
}
return cookies, nil
}
// SetCookie sets a cookie on the page.
func (p *Page) SetCookie(cookie *http.Cookie) error {
if cookie == nil {
return errors.New("nil cookie is invalid")
}
var expiry int64
if !cookie.Expires.IsZero() {
expiry = cookie.Expires.Unix()
}
apiCookie := &api.Cookie{
Name: cookie.Name,
Value: cookie.Value,
Path: cookie.Path,
Domain: cookie.Domain,
Secure: cookie.Secure,
HTTPOnly: cookie.HttpOnly,
Expiry: float64(expiry),
}
if err := p.session.SetCookie(apiCookie); err != nil {
return fmt.Errorf("failed to set cookie: %s", err)
}
return nil
}
// DeleteCookie deletes a cookie on the page by name.
func (p *Page) DeleteCookie(name string) error {
if err := p.session.DeleteCookie(name); err != nil {
return fmt.Errorf("failed to delete cookie %s: %s", name, err)
}
return nil
}
// ClearCookies deletes all cookies on the page.
func (p *Page) ClearCookies() error {
if err := p.session.DeleteCookies(); err != nil {
return fmt.Errorf("failed to clear cookies: %s", err)
}
return nil
}
// URL returns the current page URL.
func (p *Page) URL() (string, error) {
url, err := p.session.GetURL()
if err != nil {
return "", fmt.Errorf("failed to retrieve URL: %s", err)
}
return url, nil
}
// Size sets the current page size in pixels.
func (p *Page) Size(width, height int) error {
window, err := p.session.GetWindow()
if err != nil {
return fmt.Errorf("failed to retrieve window: %s", err)
}
if err := window.SetSize(width, height); err != nil {
return fmt.Errorf("failed to set window size: %s", err)
}
return nil
}
// Screenshot takes a screenshot and saves it to the provided filename.
// The provided filename may be an absolute or relative path.
func (p *Page) Screenshot(filename string) error {
absFilePath, err := filepath.Abs(filename)
if err != nil {
return fmt.Errorf("failed to find absolute path for filename: %s", err)
}
screenshot, err := p.session.GetScreenshot()
if err != nil {
return fmt.Errorf("failed to retrieve screenshot: %s", err)
}
if err := ioutil.WriteFile(absFilePath, screenshot, 0666); err != nil {
return fmt.Errorf("failed to save screenshot: %s", err)
}
return nil
}
// Title returns the page title.
func (p *Page) Title() (string, error) {
title, err := p.session.GetTitle()
if err != nil {
return "", fmt.Errorf("failed to retrieve page title: %s", err)
}
return title, nil
}
// HTML returns the current contents of the DOM for the entire page.
func (p *Page) HTML() (string, error) {
html, err := p.session.GetSource()
if err != nil {
return "", fmt.Errorf("failed to retrieve page HTML: %s", err)
}
return html, nil
}
// RunScript runs the JavaScript provided in the body. Any keys present in
// the arguments map will be available as variables in the body.
// Values provided in arguments are converted into javascript objects.
// If the body returns a value, it will be unmarshalled into the result argument.
// Simple example:
// var number int
// page.RunScript("return test;", map[string]interface{}{"test": 100}, &number)
// fmt.Println(number)
// -> 100
func (p *Page) RunScript(body string, arguments map[string]interface{}, result interface{}) error {
var (
keys []string
values []interface{}
)
for key, value := range arguments {
keys = append(keys, key)
values = append(values, value)
}
argumentList := strings.Join(keys, ", ")
cleanBody := fmt.Sprintf("return (function(%s) { %s; }).apply(this, arguments);", argumentList, body)
if err := p.session.Execute(cleanBody, values, result); err != nil {
return fmt.Errorf("failed to run script: %s", err)
}
return nil
}
// PopupText returns the current alert, confirm, or prompt popup text.
func (p *Page) PopupText() (string, error) {
text, err := p.session.GetAlertText()
if err != nil {
return "", fmt.Errorf("failed to retrieve popup text: %s", err)
}
return text, nil
}
// EnterPopupText enters text into an open prompt popup.
func (p *Page) EnterPopupText(text string) error {
if err := p.session.SetAlertText(text); err != nil {
return fmt.Errorf("failed to enter popup text: %s", err)
}
return nil
}
// ConfirmPopup confirms an alert, confirm, or prompt popup.
func (p *Page) ConfirmPopup() error {
if err := p.session.AcceptAlert(); err != nil {
return fmt.Errorf("failed to confirm popup: %s", err)
}
return nil
}
// CancelPopup cancels an alert, confirm, or prompt popup.
func (p *Page) CancelPopup() error {
if err := p.session.DismissAlert(); err != nil {
return fmt.Errorf("failed to cancel popup: %s", err)
}
return nil
}
// Forward navigates forward in history.
func (p *Page) Forward() error {
if err := p.session.Forward(); err != nil {
return fmt.Errorf("failed to navigate forward in history: %s", err)
}
return nil
}
// Back navigates backwards in history.
func (p *Page) Back() error {
if err := p.session.Back(); err != nil {
return fmt.Errorf("failed to navigate backwards in history: %s", err)
}
return nil
}
// Refresh refreshes the page.
func (p *Page) Refresh() error {
if err := p.session.Refresh(); err != nil {
return fmt.Errorf("failed to refresh page: %s", err)
}
return nil
}
// SwitchToParentFrame focuses on the immediate parent frame of a frame selected
// by Selection.Frame. After switching, all new and existing selections will refer
// to the parent frame. All further Page methods will apply to this frame as well.
//
// This method is not supported by PhantomJS. Please use SwitchToRootFrame instead.
func (p *Page) SwitchToParentFrame() error {
if err := p.session.FrameParent(); err != nil {
return fmt.Errorf("failed to switch to parent frame: %s", err)
}
return nil
}
// SwitchToRootFrame focuses on the original, default page frame before any calls
// to Selection.Frame were made. After switching, all new and existing selections
// will refer to the root frame. All further Page methods will apply to this frame
// as well.
func (p *Page) SwitchToRootFrame() error {
if err := p.session.Frame(nil); err != nil {
return fmt.Errorf("failed to switch to original page frame: %s", err)
}
return nil
}
// SwitchToWindow switches to the first available window with the provided name
// (JavaScript `window.name` attribute).
func (p *Page) SwitchToWindow(name string) error {
if err := p.session.SetWindowByName(name); err != nil {
return fmt.Errorf("failed to switch to named window: %s", err)
}
return nil
}
// NextWindow switches to the next available window.
func (p *Page) NextWindow() error {
windows, err := p.session.GetWindows()
if err != nil {
return fmt.Errorf("failed to find available windows: %s", err)
}
var windowIDs []string
for _, window := range windows {
windowIDs = append(windowIDs, window.ID)
}
// order not defined according to W3 spec
sort.Strings(windowIDs)
activeWindow, err := p.session.GetWindow()
if err != nil {
return fmt.Errorf("failed to find active window: %s", err)
}
for position, windowID := range windowIDs {
if windowID == activeWindow.ID {
activeWindow.ID = windowIDs[(position+1)%len(windowIDs)]
break
}
}
if err := p.session.SetWindow(activeWindow); err != nil {
return fmt.Errorf("failed to change active window: %s", err)
}
return nil
}
// CloseWindow closes the active window.
func (p *Page) CloseWindow() error {
if err := p.session.DeleteWindow(); err != nil {
return fmt.Errorf("failed to close active window: %s", err)
}
return nil
}
// WindowCount returns the number of available windows.
func (p *Page) WindowCount() (int, error) {
windows, err := p.session.GetWindows()
if err != nil {
return 0, fmt.Errorf("failed to find available windows: %s", err)
}
return len(windows), nil
}
// LogTypes returns all of the valid log types that may be used with a LogReader.
func (p *Page) LogTypes() ([]string, error) {
types, err := p.session.GetLogTypes()
if err != nil {
return nil, fmt.Errorf("failed to retrieve log types: %s", err)
}
return types, nil
}
// ReadNewLogs returns new log messages of the provided log type. For example,
// page.ReadNewLogs("browser") returns browser console logs, such as JavaScript
// logs and errors. Only logs since the last call to ReadNewLogs are returned.
// Valid log types may be obtained using the LogTypes method.
func (p *Page) ReadNewLogs(logType string) ([]Log, error) {
if p.logs == nil {
p.logs = map[string][]Log{}
}
clientLogs, err := p.session.NewLogs(logType)
if err != nil {
return nil, fmt.Errorf("failed to retrieve logs: %s", err)
}
messageMatcher := regexp.MustCompile(`^(?s:(.+))\s\(([^)]*:\w*)\)$`)
var logs []Log
for _, clientLog := range clientLogs {
matches := messageMatcher.FindStringSubmatch(clientLog.Message)
message, location := clientLog.Message, ""
if len(matches) > 2 {
message, location = matches[1], matches[2]
}
log := Log{message, location, clientLog.Level, msToTime(clientLog.Timestamp)}
logs = append(logs, log)
}
p.logs[logType] = append(p.logs[logType], logs...)
return logs, nil
}
// ReadAllLogs returns all log messages of the provided log type. For example,
// page.ReadAllLogs("browser") returns browser console logs, such as JavaScript logs
// and errors. All logs since the session was created are returned.
// Valid log types may be obtained using the LogTypes method.
func (p *Page) ReadAllLogs(logType string) ([]Log, error) {
if _, err := p.ReadNewLogs(logType); err != nil {
return nil, err
}
return append([]Log(nil), p.logs[logType]...), nil
}
func msToTime(ms int64) time.Time {
seconds := ms / 1000
nanoseconds := (ms % 1000) * 1000000
return time.Unix(seconds, nanoseconds)
}
// MoveMouseBy moves the mouse by the provided offset.
func (p *Page) MoveMouseBy(xOffset, yOffset int) error {
if err := p.session.MoveTo(nil, api.XYOffset{X: xOffset, Y: yOffset}); err != nil {
return fmt.Errorf("failed to move mouse: %s", err)
}
return nil
}
// DoubleClick double clicks the left mouse button at the current mouse
// position.
func (p *Page) DoubleClick() error {
if err := p.session.DoubleClick(); err != nil {
return fmt.Errorf("failed to double click: %s", err)
}
return nil
}
// Click performs the provided Click event using the provided Button at the
// current mouse position.
func (p *Page) Click(event Click, button Button) error {
var err error
switch event {
case SingleClick:
err = p.session.Click(api.Button(button))
case HoldClick:
err = p.session.ButtonDown(api.Button(button))
case ReleaseClick:
err = p.session.ButtonUp(api.Button(button))
default:
err = errors.New("invalid touch event")
}
if err != nil {
return fmt.Errorf("failed to %s %s: %s", event, button, err)
}
return nil
}
// SetImplicitWait sets the implicit wait timeout (in ms)
func (p *Page) SetImplicitWait(timeout int) error {
return p.session.SetImplicitWait(timeout)
}
// SetPageLoad sets the page load timeout (in ms)
func (p *Page) SetPageLoad(timeout int) error {
return p.session.SetPageLoad(timeout)
}
// SetScriptTimeout sets the script timeout (in ms)
func (p *Page) SetScriptTimeout(timeout int) error {
return p.session.SetScriptTimeout(timeout)
}