2. 浏览器(Browser)
Geb 执行的入口点是 Browser 对象。每个 Browser 对象底层都绑定了一个 WebDriver 实例(用于驱动浏览器进行自动化),并且具有 “当前页面” 的概念,表示浏览器当前所处的页面。
Browser 对象是通过 Configuration (配置)来创建的。Configuration 中指定了使用的 WebDriver 实现,用于解析相对 URL 的基础 URL (baseUrl),及其他各种配置。使用配置机制允许将设置 Geb 如何运行的操作独立出来,这意味着同一份 Geb 代码或测试套件能够运行在不同的浏览器和网站上。后续关于 Configuration 的章节包含更详细的关于如何管理配置参数以及有哪些可配置参数的介绍。
Browser 类的默认构造函数就是简单的从配置机制加载配置:
import geb.Browser
def browser = new Browser()
然而,如果你需要指定 WebDriver 实现(或需要设置 Browser 上的其他可配置属性) ,就可以利用 Groovy 的基于 map 的构造函数语法来指定:
import geb.Browser
import org.openqa.selenium.htmlunit.HtmlUnitDriver
def browser = new Browser(driver: new HtmlUnitDriver())
上面的代码和这下面的等效
def browser = new Browser()
browser.driver = new HtmlUnitDriver()
任何通过此方式设置的属性值都会覆盖从配置机制中获取到的属性值。
注意:如果 Browser 的 driver 对象在第一次使用后被改变,行为将是未定义的。所以尽量避免以这种方式是在 driver,使用配置机制设置是比较推荐的方式。
如果有非常多的定制化配置需求,可以考虑创建自己的 Configuration 对象,并使用它来创建 Browser,类似于使用配置加载器的方式:
import geb.Browser
import geb.ConfigurationLoader
def loader = new ConfigurationLoader("a-custom-environment")
def browser = new Browser(loader.conf)
如果可能,应该坚持使用无参的构造函数并且通过内置的配置机制来管理 Geb,因为它提供了更大的灵活性并且使配置和代码分离。
注:Geb 的集成项目一般都会移除创建浏览器的操作,框架会帮你做好这些事,你只需要管理对应的配置即可。
2.1 Browser 的 drive 方法
Browser 类有一个静态的 drive 方法能使编写 Geb 脚本更加方便。
Browser.drive {
go "signup"
assert $("h1").text() == "Signup Page"
}
这与下面的写法等效:
def browser = new Browser()
browser.go "signup"
assert browser.$("h1").text() == "Signup Page"
drive 方法能够接受 Browser 构造函数中可用的所有参数(如:空,Configuration,或者各种属性),也可以接受一个已经打开的浏览器实例和闭包作为参数。闭包将会绑定到给定的浏览器实例上执行,结果就是闭包内所有顶层的方法调用和属性访问都会到绑定的浏览器实例上去解析。
drive 方法总会返回其使用的浏览器对象,因此如果你需要在闭包执行完后关闭浏览器的话,可以像这样做:
Browser.drive {
//…
}.quit()
更多关于什么时候以及为什么要手动关闭浏览器,可以查看后续关于 driver 的章节。
2.2 发起请求
2.2.1 基准 URL
Browser 实例内维护了一个 baseUrl 属性,该属性用于解析所有相对的 URL。这个属性的值可以来自于配置,也可以显式地设置在 Browser 对象上。
在指定基准 URL 和相对 URL 时候都要十分注意结尾或开头的斜线(/),因为 / 的位置对 URL 解析有很大影响。下表展示了各种不同 URL 的解析结果。
基准 URL | 要访问的相对 URL | 解析后的 URL |
---|---|---|
http://myapp.com/ | abc | http://myapp.com/abc |
http://myapp.com | abc | http://myapp.comabc |
http://myapp.com | /abc | http://myapp.com/abc |
http://myapp.com/abc/ | def | http://myapp.com/abc/def |
http://myapp.com/abc | def | http://myapp.com/def |
http://myapp.com/abc/ | /def | http://myapp.com/def |
http://myapp.com/abc/def/ | jkl | http://myapp.com/abc/def/jkl |
http://myapp.com/abc/def | jkl | http://myapp.com/abc/jkl |
http://myapp.com/abc/def | /jkl | http://myapp.com/jkl |
最理想的方式就是书写基准 URL 时使用后缀的 /,书写相对 URL 时不使用前缀的 /
2.2.2 使用页面
Page 对象中可以定义一个 url 属性,当导航到指定的 Page 对象时就会用到这个 url 属性。导航到 Page 对象所代表的页面是通过 Browser 对象的 to() 或 via() 方法实现的。
class SignupPage extends Page {
static url = "signup"
}
Browser.drive {
to SignupPage
assert $("h1").text() == "Signup Page"
assert page instanceof SignupPage
}
to() 和 via() 方法将会向解析后的 URL 发起请求,并把 Browser 对象中的 page 实例设置为他们所访问的 Page 类型的一个实例。绝大部分的 Geb 脚本或测试都是从一个 to() 或 via() 方法调用开始的。
后续关于高级页面导航的章节将会讲解如何在 Page 对象上使用更复杂的 URL 解析方式。
2.2.3 直接发起请求
你也可以直接使用 Browser 对象的 go() 方法向一个 URL 直接发起请求,而不设置或改变 Browser 的 page 对象。
import geb.Page
import geb.spock.GebSpec
class GoogleHomePage extends Page {
static url = "http://www.google.com"
}
class GoogleSpec extends GebSpec {
def "go 方法不设置浏览器的 page"() {
given:
Page oldPage = page
when:
go "http://www.google.com"
then:
oldPage == page
currentUrl.contains "google"
}
def "to 方法会设置浏览器的 page 并改变当前URL(currentUrl)"() {
given:
Page oldPage = page
when:
to GoogleHomePage
then:
oldPage != page
currentUrl.contains "google"
}
}
下面这个例子中使用的基准 URL 是:http://localhost/
Browser.drive {
go() //1
go "signup" //2
go "signup", param1: "value1", param2: "value2" //3
}
//1:访问基准 URL
//2:访问一个相对于基准 URL 的相对 URL
//3:访问一个带有请求参数的 URL: http://localhost/signup?param1=value1¶m2=value2
2.3 Browser 实例中的 Page 对象
Browser 实例中有一个对当前页面对象的引用,保存在 page 属性中。刚开始,所有 Browser 实例都保存了一个类型为 Page 都 page 对象,这个 Page 类型提供了基本都导航器功能,是所有具体页面对象的超类。
但其实我们很少直接访问 page 属性。Browser 对象将会把所有它不能处理的方法调用和属性读写委派给其 page 对象处理。
Browser.drive {
go "signup"
assert $("h1").text() == "Signup Page" //1
assert page.$("h1").text() == "Signup Page" //1
}
//1:这两种调用方式是等效的,因为浏览器会把它不能处理的 $("h1") 方法调用自动委派给其 page 实例去处理
page 对象提供了 $() 方法,Browser 对象没有此方法。这种转发机制使得代码很简洁,减少了不必要的代码“噪声”。
注:$() 是用来和页面内容交互的,关于此方法的更多信息请参考后续的 Navigator API 章节
当使用页面对象模式时,我们需要创建 Page 类型的子类,Page 使用了强大的领域特定语言(DSL) 来定义页面内容,使得你可以使用有意义的名字来引用页面内容,而不是直接使用普通的 tag 名字或 CSS 表达式。
class SignupPage extends Page {
static url = "signup"
static content = {
heading { $("h1").text() }
}
}
Browser.drive {
to SignupPage
assert heading == "Signup Page"
}
页面对象会在后续的 Pages 章节详谈,该章中我们也会探索定义页面内容的 DSL。
2.3.1 改变 Browser 的 page 对象
前面我们以及提到过 to() 方法能够改变 Browser 的 page 对象。除此之外,我们还可以使用 Browser 的 page() 方法来改变其 page 对象,而不需要向 to() 一样发起一次新的请求。
<T extends Page> T page(Class<T> pageType) 方法允许我们把 Browser 实例的 page 对象设置为给定类型 pageType 的一个新实例。这里的类型 pageType 必须是 Page 或 Page 的子类。该方法不会验证指定的 page 对象是否真的和浏览器当前的页面内容相匹配(不会做 at 检查)。
<T extends Page> T page(T pageInstance) 方法允许我们把 Browser 实例的 page 对象设置为给定实例 pageInstance。和前面的方法类似,该方法也不会验证指定的 page 对象是否真的和浏览器当前的页面内容相匹配。
Page page(Class<? extends Page>[] potentialPageTypes) 方法允许我们指定一组可能的页面类型。每个可能的类型都会被实例化,并且会检查每个实例化的页面对象是否与浏览器当前的页面内容匹配(通过允许每个页面实例的 at 方法来检查)。所有传入的页面类都必须定义 at 检查器,否则将会抛出 UndefinedAtCheckerException 异常。
Page page(Page[] potentialPageInstances) 方法允许我们指定一组可能的页面实例。每个可能的页面实例都会被初始化并检查其是否与浏览器当前的页面内容匹配(通过允许每个页面实例的 at 方法来检查)。所有传入的页面实例都必须定义 at 检查器,否则将会抛出 UndefinedAtCheckerException 异常。
2.4 At 检查器
Page 中可以定义 at 检查器,Browser 对象可以使用 at 检查器来校验它当前是否处于某个页面。
class SignupPage extends Page {
static url = "signup"
static at = {
$("h1").text() == "Signup Page"
}
}
Browser.drive {
to SignupPage
}
注意:最好不要在 at 检查器中使用显式的 return 语句。Geb 会对 at 检查器中的所有检查点进行转换,使得检查器中的每条语句都被加上断言(就像 Spock 中的 then: 语句块一样)。多亏这个机制,它使得 at 检查器失败的时候,你能立刻看到检查器求值失败的原因。
to() 方法接受一个页面类型或页面实例作为参数,并且会验证浏览器最终会停留在其参数所指定的页面。如果在发起到某个页面的请求过程中会发生重定向,最终把浏览器带到一个不同的页面,我们就应该使用 via() 方法:
Browser.drive {
via SecurePage
at AccessDeniedPage
}
Brower 对象有 <T extends Page> T at(Class<T> pageType) 和 <T extends Page> T at(T page) 两种形式的 at 方法能够用来测试浏览器当前是否在参数所指定的页面。
上面的 at AccessDeniedPage 方法调用在 at 检查器通过的时候将会返回一个页面对象。而如果 at 检查器校验没有通过的话,默认情况下(查看隐式断言章节获取更多信息),将会抛出 AssertionError 异常,即使 at 检查器中并没有显式的断言语句;如果隐式断言被关闭的话,将会返回 null
当页面对象改变时,为了使不合预期的测试快速失败,通常比较推荐的做法是使用 to() 方法(该方法会隐式的对页面对象进行 at 检查)或 via() 方法后面紧跟着一个 at() 方法来校验是否在预期的页面。如果不做 at 检查的话,后续步骤的失败可能比较奇怪又不好定位,因为页面内容和预期的不匹配,内容查找可能返回离奇的结果。
如果向 at() 方法中传入了一个没有定义 at 检查器的 Page 子类或实例,将会抛出 UndefinedAtCheckerException,因为做显式的 at 检查时,页面中必须要定义 at 检查器,这与使用 to() 方法时进行的隐式的 at 检查不同。这种行为是有意要提醒你,当你尝试显式验证是否在某个页面时,你本意可能也想为这个页面定义 at 检查器。但隐式 at 检查时,却不会强制要求你这样做。
如果 at 检查器验证成功,at() 也会同时更新 Browser 对象的 page 实例,这就是说,当成功验证 Browser 处于某个页面后,你不需要再次手动的将其 page 对象设置为刚才经过验证的页面,at() 已经帮你做了。
Page 上定义的页面内容(content),支持声明该内容被点击后,浏览器页面的类型应该如何改变。当这种页面内容被点击后,浏览器会自动对该内容中声明的页面进行 at 检查,若果该页面定义了 at 检查器的话(详见后续 DSL 章节中关于 to 参数的说明)。
class LoginPage extends Page {
static url = "/login"
static content = {
loginButton(to: AdminPage) { $("input", type: "submit") }
username { $("input", name: "username") }
password { $("input", name: "password") }
}
}
class AdminPage extends Page {
static at = {
$("h1").text() == "Admin Page"
}
}
Browser.drive {
to LoginPage
username.value("admin")
password.value("p4sw0rd")
loginButton.click()
assert page instanceof AdminPage
}
2.5 页面变化监听器
当浏览器的 page 实例发生变化时,是可以通过 PageChangeListener 接口监听到的。一旦监听器被注册,它的 pageWillChange() 方法就会被立即被调用一次,它的 newPage 参数会被设置成当前 page 对象,oldPage 参数被设置成 null。在之后每一次页面发生改动时,oldPage 的值将会是浏览器当前的 page 对象,newPage 的值将是浏览器马上要被设置成的那个 page 对象。
class RecordingPageChangeListener implements PageChangeListener {
Page oldPage
Page newPage
@Override
void pageWillChange(Browser browser, Page oldPage, Page newPage) {
this.oldPage = oldPage
this.newPage = newPage
}
}
class FirstPage extends Page {
}
class SecondPage extends Page {
}
def listener = new RecordingPageChangeListener()
def browser = new Browser()
browser.page(FirstPage)
browser.registerPageChangeListener(listener)
assert listener.oldPage == null
assert listener.newPage instanceof FirstPage
browser.page(SecondPage)
assert listener.oldPage instanceof FirstPage
assert listener.newPage instanceof SecondPage
可以随时移除页面变动监听器:
browser.removePageChangeListener(listener)
removePageChangeListener(PageChangeListener listener) 将返回 true,如果 listener 是已经注册过的,并且现在已被移除,否则将返回 false。
监听器不可以注册两次。如果尝试注册一个已经注册过的监听器(例如已经有另一个和此监听器相等的(基于equals比较)监听器被注册了),将会抛出 PageChangeListenerAlreadyRegisteredException。
2.6 处理多个标签和窗口
当你的 Web 应用涉及多 Tab 或 Window 时(例如:当点击一个设置了 target 属性的链接时打开了一个新标签页),你可以使用 withWindow() 或 withNewWindow() 方法在其他窗口的上下文中执行操作。如果你真需要知道当前窗口的名称或想获得所有打开窗口的名称,可以使用 Browser.getCurrentWindow() 和 Browser.getAvailableWindow() 方法来获取,但是对于处理多窗口这种场景,还是推荐使用 withWindow() 和 withNewWindow() 方法。
2.6.1 处理已经打开的窗口
如果你已经知道你想操作的窗口的名称,就可以使用 withWindow(String windowName, Closure block) 方法在该窗口的上下文中执行操作。
假设下面这段 HTML 代码是在 baseUrl 指定的页面中渲染的:
<a href="http://www.gebish.org" target="myWindow">Geb</a>
下面这段代码将能执行成功:
Browser.drive {
go()
$("a").click()
withWindow("myWindow") {
assert title == "Geb - Very Groovy Browser Automation"
}
}
如果你不知道要操作的窗口的名称,但是你知道一些关于该窗口页面内容的信息,你就可以使用 withWindow(Closure specification, Closure block) 方法来操作。该方法的第一个闭包参数在你想要操作的窗口中执行后,应该要能返回 true,第二个闭包参数就是你想要在该窗口的上下文中执行的操作。如果没有参数能使窗口定位闭包 specification 返回 true,那么就会抛出 NoSuchWindowException。
因此,假设页面上有如下代码:
<a href="http://www.gebish.org" target="_blank">Geb</a>
下面这些操作将会成功:
Browser.drive {
go()
$("a").click()
withWindow({ title == "Geb - Very Groovy Browser Automation" }) {
assert $(".slogan").text().startsWith("Very Groovy browser automation.")
}
}
注意:如果 withWindow 方法的第二个闭包参数 block,在执行过程中改变了 browser 对象的当前页面 page 实例(例如,使用了 page(Page) or at(Page) 方法),当 withWindow() 方法返回后,browser 的 page 实例将会恢复回其原来的值(即调用 withWindow 方法前的值)
withWindow() 方法接受的选项
在调用 withWindow() 方法时,可以传入一些额外的选项,来简化和这些已经打开的窗口的交互。通用的语法如下:
withWindow(«window specification», «option name»: «option value», ...) {
«action executed within the context of the window»
}
close 选项:默认值 false
如果你向 close 选项传入了任何 Groovy 中的 “真值”,那个在传递给 withWindow() 方法的最后一个闭包参数中的代码执行完后,该窗口就会被关闭。
page 选项:默认值 null
如果你向 page 选项传入了 Page 或其子类,或他们的实例,那么在执行 withWindow() 方法的最后一个闭包参数中的代码前,Geb 会先将 browser 的 page 实例设置为通过 page 选项传入的值,并且在代码执行完后进行恢复。如果传入的 page 所属的类定义了 at 检查器,Geb 会进对 at 检查器进行校验。
2.6.2 处理新打开的窗口
如果你想在你执行操作过程中新打开的窗口的上下文中执行代码,可以使用 withNewWindow(Closure windowOpeningBlock, Closure block) 方法来处理。
假设下面这段 HTML 代码是在 baseUrl 指定的页面中渲染的:
<a href="http://www.gebish.org" target="_blank">Geb</a>
下面这段代码将能执行成功:
Browser.drive {
go()
withNewWindow({ $('a').click() }) {
assert title == 'Geb - Very Groovy Browser Automation'
}
}
如果第一个参数指定的闭包执行后,打开了 0 个或多个窗口,Geb 将会抛出 NoNewWindowException。
注意:如果 withNewWindow 方法的第二个闭包参数 block,在执行过程中改变了 browser 对象的当前页面 page 实例(例如,使用了 page(Page) or at(Page) 方法),当 withNewWindow() 方法返回后,browser 的 page 实例将会恢复回其原来的值(即调用 withNewWindow 方法前的值)
withNewWindow() 方法接受的选项
在调用 withNewWindow() 方法时,可以传入一些额外的选项,来简化和这些新打开的窗口的交互。通用的语法如下:
withNewWindow({ «window opening action» }, «option name»: «option value», ...) {
«action executed within the context of the window»
}
close 选项:默认值 false
如果你向 close 选项传入了任何 Groovy 中的 “真值”,那个在传递给 withNewWindow() 方法的最后一个闭包参数中的代码执行完后,该新打开的窗口就会被关闭。
page 选项:默认值 null
如果你向 page 选项传入了 Page 或其子类,或他们的实例,那么在执行 withNewWindow() 方法的最后一个闭包参数中的代码前,Geb 会先将 browser 的 page 实例设置为通过 page 选项传入的值,并且在代码执行完后进行恢复。
wait 选项:默认值 null
如果传给 withNewWindow() 方法的第一用于打开新窗口的闭包中执行的打开窗口的操作是异步的,并且你需要等待新窗口打开,这时就可以通过 wait 选项来通知 Geb 等待窗口打开。wait 选项可以接受的值和后续章节中将涉及的页面内容定义中的 wait 选项的可接受的值是一样的。
假设下面这段 HTML 是在 baseUrl 指定的页面中进行渲染的:
<a href="http://google.com" target="_blank" id="new-window-link">Google</a>
假设该页面中同时还包括下面这段 javaScript 代码:
function openNewWindow() {
setTimeout(function() {
document.getElementById('new-window-link').click();
}, 200);
}
那么下面的代码将能执行成功:
Browser.drive {
go()
withNewWindow({ js.openNewWindow() }, wait: true) {
assert title == 'Google'
}
}
2.7 暂停和调试
如果能在代码中指定的地方暂停 Geb 的执行,这对于排查问题将很有帮助。可以使用两种方法来实现:
- 在 IDE 中设置断点,然后以 debug 模式运行 JVM
- 使用 Browser 的 pause() 方法
尽管前者允许检查 JVM 内的变量以及在断点处使用 Geb 的类和方法,但后者设置起来更加方便快捷,暂停后可以在浏览器打开开发者工具来检查 DOM 文档树以及 javascript 脚本中变量,而这在大多数情况下对解决问题已经足够了。
注:如果你想在调用 pause() 方法暂停后能够继续执行,将全局 javascript 变量 geb.unpause 设为 true 即可。这通常是在浏览器的开发者工具的控制台 (console) 中执行 geb.unpause = true; 来实现。如果在调用 pause() 方法及恢复执行这段时间,浏览器页面重新加载过或者导航到了一个新页面,就需要在设置 geb.unpause = true 之前重新构建顶级的 geb 对象。这通常在浏览器控制台中执行一下 geb = {}; 即可。
2.8 本地存储和会话存储
可以通过 Browser 实例的 localStorage 和 sessionStorage 属性来分别访问本地存储和会话存储。可以通过他们来读写底层的存储,就像操作普通的 map 一样。下面是读写本来存储的一个例子:
Browser.drive {
localStorage["username"] = "Alice"
assert localStorage["username"] == "Alice"
}
loaclStorage 和 sessionStorage 属性都是 geb.webstorage.WebStorage 类型。可以查看 javadoc 来了解除了读写之外,他们支持的其他操作。
注意:并不是所有的浏览器实现都支持访问 web 存储。当浏览器不支持访问 web 存储时,Geb 将抛出:geb.error.WebStorageNotSupportedException 异常。
2.9 退出浏览器
Browser 对象有 quit() 和 close() 方法来退出浏览器。它们简单的将这些方法的调用委派给底层的 WebDriver 实例。可以查看后续的 浏览器驱动管理 章节来获取更多关于何时退出以及为何需要退出浏览器的信息。