All Downloads are FREE. Search and download functionalities are using the official Maven repository.

geb.Browser.groovy Maven / Gradle / Ivy

Go to download

Geb (pronounced "jeb") is a browser automation solution. It brings together the power of WebDriver, the elegance of jQuery content selection, the robustness of Page Object modelling and the expressiveness of the Groovy language.

There is a newer version: 0.7.2
Show newest version
/* Copyright 2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package geb

import geb.js.JavascriptInterface
import geb.driver.RemoteDriverOperations

import geb.error.PageChangeListenerAlreadyRegisteredException
import geb.error.RequiredPageContentNotPresent
import geb.error.UnexpectedPageException

import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebDriverException
import org.openqa.selenium.NoSuchWindowException

/**
 * The browser is the centre of Geb. It encapsulates a {@link org.openqa.selenium.WebDriver} implementation and references
 * a {@link geb.Page} object that provides access to the content. 
 * 

* Browser objects dynamically delegate all method calls and property read/writes that it doesn't implement to the current * page instance via {@code propertyMissing()} and {@code methodMissing()}. */ class Browser { private Page page private final Configuration config private final pageChangeListeners = new LinkedHashSet() private String reportGroup = null /** * If the driver is remote, this object allows access to its capabilities (users of Geb should not access this object, it is used internally). */ @Lazy WebDriver augmentedDriver = new RemoteDriverOperations(this.class.classLoader).getAugmentedDriver(driver) /** * Create a new browser with a default configuration loader, loading the default configuration file. * * @see geb.ConfigurationLoader */ Browser() { this(new ConfigurationLoader().conf) } /** * Create a new browser backed by the given configuration. * * @see geb.Configuration */ Browser(Configuration config) { this.config = config page(Page) } /** * Creates a new browser instance backed by the given configuration, then applies {@code props} as property overrides on the browser. * * @see geb.Configuration */ Browser(Map props, Configuration config) { this(config) this.metaClass.setProperties(this, props) } /** * Provides access to the current page object. *

* All browser objects are created with a page type of {@link geb.Page} initially. */ Page getPage() { this.page } /** * Provides access to the configuration object assoicated with this browser. */ Configuration getConfig() { this.config } /** * The driver implementation used to automate the actual browser. *

* The driver implementation to use is determined by the configuration. * * @see geb.Configuration#getDriver() */ WebDriver getDriver() { config.driver } /** * Set (or change) the webdriver underneath this browser. *

* This should only be called before making any requests as a means to override the driver instance * that would be created from the configuration. Where possible, prefer using the configuration mechanism * to specify the driver implementation to use. *

* This method delegates to {@link geb.Configuration#setDriver(org.openqa.selenium.WebDriver)}. */ void setDriver(WebDriver driver) { config.driver = driver } /** * The url to resolve all relative urls against. Typically the root of the application or system * Geb is interacting with. *

* The base url is determined by the configuration. * * @see geb.Configuration#getBaseUrl() */ String getBaseUrl() { config.baseUrl } /** * Changes the base url used for resolving relative urls. *

* This method delegates to {@link geb.Configuration#setBaseUrl(def)}. */ void setBaseUrl(String baseUrl) { config.baseUrl = baseUrl } /** * Allows new page change listeners to be registered with this browser. *

* This method will immediately call the {@link geb.PageChangeListener#pageWillChange(geb.Browser, geb.Page, geb.Page)} method on * {@code listener} with the current page as the {@code newPage} argument and {@code null} for the {@code oldPage} argument. * * @throws geb.error.PageChangeListenerAlreadyRegisteredException if the listener is already registered. * @see geb.PageChangeListener */ void registerPageChangeListener(PageChangeListener listener) { if (pageChangeListeners.add(listener)) { listener.pageWillChange(this, null, page) } else { throw new PageChangeListenerAlreadyRegisteredException(this, listener) } } /** * Removes the given page change listener. * * @return whether or not the listener was actually registered or not. */ boolean removePageChangeListener(PageChangeListener listener) { pageChangeListeners.remove(listener) } /** * Delegates the method call directly to the current page object. */ def methodMissing(String name, args) { page."$name"(*args) } /** * Delegates the property access directly to the current page object. */ def propertyMissing(String name) { page."$name" } /** * Delegates the property assignment directly to the current page object. */ def propertyMissing(String name, value) { page."$name" = value } /** * Changes the browser's page to be an instance of the given class. *

* This method performs the following: *

    *
  • Create a new instance of the given class (which must be {@link geb.Page} or a subclass thereof) and connect it to this browser object *
  • Inform any registered page change listeners *
  • Set the browser's page property to the created instance (if it is not already of this type) *

    * Note that it does not verify that the page matches the current content by running its at checker */ void page(Class pageClass) { makeCurrentPage(createPage(pageClass)) } /** * Changes the browser's page to be an instance of the first given type whose at checker returns a true value. *

    * This method performs the following: *

      *
    • For each given page type: *
        *
      • Create a new instance of the class (which must be {@link geb.Page} or a subclass thereof) and connect it to the browser object *
      • Test if the page represents the new instance by running its at checker *
      • If the page's at checker is successful: *
          *
        • Inform any registered page change listeners *
        • Set the browser's page property to the instance (if it is not already of this type) *
        • Discard the rest of the potentials *
        *
      • If the page's at checker is not successful: *
          *
        • Try the next potential */ void page(Class[] potentialPageClasses) { def potentialPageClassesClone = potentialPageClasses.toList() def match = null while (match == null && !potentialPageClassesClone.empty) { def potential = createPage(potentialPageClassesClone.remove(0)) if (potential.verifyAtSafely()) { match = potential } } if (match) { makeCurrentPage(match) } else { throw new UnexpectedPageException(potentialPageClasses) } } /** * Sets this browser's page to be the given page after initializing it. * * * @see #page(Class) */ void page(Page page) { makeCurrentPage(initialisePage(page)) } /** * Checks if the browser is at the current page by running the at checker for this page type. *

          * A new instance of the page is created for the at check. If the at checker is successful, * this browser object's page instance is updated to the new instance of the given page type. */ boolean at(Class pageType) { doAt(page.class == pageType ? page : createPage(pageType)) } /** * Checks if the browser is at the current page by running the at checker for the given page after initializing it. *

          * If the given page at checker is successful, this browser object's page instance is updated * to the one the method is called with. */ boolean at(Page page) { doAt(page) } /** * Sets this browser's page to be the given page, which has already been initialised with this browser instance. *

          * Any page change listeners are informed and {@link geb.Page#onUnload(geb.Page)} is called * on the previous page and {@link geb.Page#onLoad(geb.Page)} is called on the incoming page. *

          */ private void makeCurrentPage(Page page) { if (!page.is(this.page)) { informPageChangeListeners(this.page, page) this.page?.onUnload(page) def previousPage = this.page this.page = page this.page.onLoad(previousPage) } } private Page initialisePage(Page page) { if (!page.browser.is(this)) { page.init(this) } page } /** * Runs a page's at checker, expecting the page to be initialised with this browser instance. */ private boolean doAt(Page page) { initialisePage(page) def at try { at = page.verifyAt() } catch (RequiredPageContentNotPresent e) { at = false } if (at) { makeCurrentPage(page) true } else { false } } /** * Sends the browser to the configured {@link #getBaseUrl() base url}. */ void go() { go([:], null) } /** * Sends the browser to the configured {@link #getBaseUrl() base url}, appending {@code params} as * query parameters. */ void go(Map params) { go(params, null) } /** * Sends the browser to the given url. If it is relative it is resolved against the {@link #getBaseUrl() base url}. */ void go(String url) { go([:], url) } /** * Sends the browser to the given url. If it is relative it is resolved against the {@link #getBaseUrl() base url}. */ void go(Map params, String url) { def newUrl = calculateUri(url, params) def newPage = driver.get(newUrl) if (!page) { page(Page) } } /** * Sends the browser to the given page type's url and sets the page to a new instance of the given type. * * @see #page(geb.Page) * @see geb.Page#to(Map, Object[]) */ void to(Class pageType, Object[] args) { to([:], pageType, *args) } /** * Sends the browser to the given page type's url and sets the page to a new instance of the given type. * * @see #page(geb.Page) * @see geb.Page#to(Map, Object[]) */ void to(Map params, Class pageType) { to(params, pageType, null) } /** * Sends the browser to the given page type's url and sets the page to a new instance of the given type. * * @see #page(geb.Page) * @see geb.Page#to(Map, Object[]) */ void to(Map params, Class pageType, Object[] args) { createPage(pageType).to(params, *args) } /** * Clears all cookies that the browser currently has. */ void clearCookies() { driver?.manage()?.deleteAllCookies() } /** * Clears all cookies that the browser currently has, suppressing any webdriver exceptions. */ void clearCookiesQuietly() { try { clearCookies() } catch (WebDriverException e) { // ignore } } /** * Quits the driver. * * @see org.openqa.selenium.WebDriver#quit() */ void quit() { driver.quit() } /** * Closes the current driver window. * * @see org.openqa.selenium.WebDriver#close() */ void close() { driver.close() } /** * Retrieves current window * * @see org.openqa.selenium.WebDriver#getWindowHandle() */ String getCurrentWindow() { driver.windowHandle } /** * Retrieves all available windows * * @see org.openqa.selenium.WebDriver#getWindowHandles() */ Set getAvailableWindows() { driver.windowHandles } private switchToWindow(String window) { driver.switchTo().window(window) } /** * Executes a closure within the context of a window specified by a name * * @param window name of the window to use as context * @param block closure to be executed in the window context * @return The return value of {@code block} */ def withWindow(String window, Closure block) { def original = currentWindow switchToWindow(window) try { block.call() } finally { switchToWindow(original) } } /** * Executes a closure within the context of all windows for which the specification * closure returns groovy truth. * * @param specification closure executed once in context of each window, if it returns groovy truth for a given * window then also the block closure is executed in the context of that window * @param block closure to be executed in the window context * @return The return value of {@code block} */ def withWindow(Closure specification, Closure block) { def anyMatching = false def original = currentWindow try { availableWindows.each { switchToWindow(it) if (specification.call()) { block.call() anyMatching = true } } } finally { switchToWindow(original) } if (!anyMatching) { throw new NoSuchWindowException() } } /** * Expects the first closure argument to open a new window and calls the second closure argument in the context * of the newly opened window. * * @param windowOpeningBlock a closure that should open a new window * @param block closure to be executed in the new window context * @return The return value of {@code block} * @throws org.openqa.selenium.NoSuchWindowException if the window opening closure doesn't open one or opens more * than one new window */ def withNewWindow(Closure windowOpeningBlock, Closure block) { def originalWindows = availableWindows def originalWindow = currentWindow windowOpeningBlock.call() def newWindow = (availableWindows - originalWindows) as List if (newWindow.size() != 1) { def message = newWindow ? 'There has been more than one window opened' : 'No new window has been opened' throw new NoSuchWindowException(message) } try { switchToWindow(newWindow.first()) block.call() } finally { switchToWindow(originalWindow) } } /** * Creates a new instance of the given page type and initialises it. * * @return The newly created page instance */ Page createPage(Class pageType) { if (!Page.isAssignableFrom(pageType)) { throw new IllegalArgumentException("$pageType is not a subclass of ${Page}") } pageType.newInstance().init(this) } /** * Returns a newly created javascript interface connected to this browser. */ JavascriptInterface getJs() { new JavascriptInterface(this) } /** * The directory that will be used for the {@link #report(java.lang.String) method}. *

          * Uses the {@link geb.Configuration#getReportsDir()} for the base location for reports (throwing an exception if this is not set), and * appending the current report group. The returned file object is guaranteed to exist on the filesystem. *

          * If the current report group is {@code null}, this method returns the same as {@code config.reportsDir}. * * @see #reportGroup(java.lang.String) * @see #report(java.lang.String) */ File getReportGroupDir() { def reportsDir = config.reportsDir if (reportsDir == null) { throw new IllegalStateException("No reports dir has been configured, you need to set in the config file or via the build adapter.") } def reportGroupDir = reportGroup ? new File(reportsDir, reportGroup) : reportsDir if (!reportGroupDir.exists() && !reportGroupDir.mkdirs()) { throw new IllegalStateException("Could not create report group dir '${reportGroupDir}'") } reportGroupDir } /** * Sets the "group" for all subsequent reports, which is the relative path inside the reports dir that reports will be written to. * * @param path a relative path, or {@code null} to have reports written to the base reports dir */ void reportGroup(String path) { reportGroup = path } /** * Sets the report group to be the full name of the class, replacing "." with "/". * * @see #reportGroup(String) */ void reportGroup(Class clazz) { reportGroup(clazz.name.replace('.', '/')) } /** * Removes the directory returned by {@link #getReportGroupDir()} from the filesystem if it exists. */ void cleanReportGroupDir() { def dir = getReportGroupDir() if (dir != null) { if (dir.exists() && !dir.deleteDir()) { throw new IllegalStateException("Could not clean report dir '${dir}'") } } } /** * Writes a snapshot of the browser's state to the current {@link #getReportGroupDir()} using * the {@link geb.Configuration#getReporter() config's reporter}. * * @param label The name for the report file (should not include a file extension) */ void report(String label) { config.reporter.writeReport(this, label, getReportGroupDir()) } private informPageChangeListeners(Page oldPage, Page newPage) { pageChangeListeners*.pageWillChange(this, oldPage, newPage) } private String toQueryString(Map params) { if (params) { params.collect { name, value -> def values = value instanceof Collection ? value : [value] values.collect { v -> "${URLEncoder.encode(name.toString(), "UTF-8")}=${URLEncoder.encode(v.toString(), "UTF-8")}" } }.flatten().join("&") } else { "" } } private String getBaseUrlRequired() { def baseUrl = getBaseUrl() if (baseUrl == null) { throw new geb.error.NoBaseUrlDefinedException() } baseUrl } private String calculateUri(String path, Map params) { def uri if (path) { uri = new URI(path) if (!uri.absolute) { uri = new URI(getBaseUrlRequired()).resolve(uri) } } else { uri = new URI(getBaseUrlRequired()) } def queryString = toQueryString(params) if (queryString) { def joiner = uri.query ? '&' : '?' new URL(uri.toString() + joiner + queryString).toString() } else { uri.toString() } } /** * Creates a new browser object via the default constructor and executes the closure * with the browser instance as the closure's delegate. * * @return the created browser */ static Browser drive(Closure script) { drive(new Browser(), script) } /** * Creates a new browser with the configuration and executes the closure * with the browser instance as the closure's delegate. * * @return the created browser */ static Browser drive(Configuration conf, Closure script) { drive(new Browser(conf), script) } /** * Creates a new browser with the properties and executes the closure * with the browser instance as the closure's delegate. * * @return the created browser */ static Browser drive(Map browserProperties, Closure script) { drive(new Browser(browserProperties), script) } /** * Executes the closure with browser as its delegate. * * @return browser */ static Browser drive(Browser browser, Closure script) { script.delegate = browser script() browser } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy