
1. 介绍
Geb(发音为 "jeb",最初是 "Groovy web" 的缩写)是一个以开发人员为中心的工具,用于自动化网页浏览器和网页内容之间的交互。它利用 Groovy 的动态语言特性提供强大的内容定义 DSL(用于建模内容以供重用)和 jQuery 的关键概念以提供强大的内容检查和遍历 API(用于查找和与内容交互)。
Geb 的诞生源于让浏览器自动化(最初用于网页测试)更简单、更高效的愿望。它旨在成为一个**开发人员工具**,因为它允许并鼓励使用编程和语言构造,而不是创建受限环境。它利用 Groovy 的动态性来消除冗余和样板代码,从而专注于重要的事情——内容和交互。
1.1. 浏览器自动化技术
Geb 建立在 WebDriver 浏览器自动化库之上,这意味着 Geb 可以与 WebDriver 支持的任何浏览器一起工作。虽然 Geb 提供了额外的便利性和生产力层,但在需要时,始终可以直接“下沉”到 WebDriver 层面执行操作。
有关更多信息,请参阅手册中关于 使用驱动实现 的部分。
1.2. 页面对象模式
页面对象模式为我们提供了一种常识性的方式来以可重用和可维护的方式建模内容。来自 WebDriver wiki 页面上关于页面对象模式 的描述:
在您的 Web 应用程序 UI 中,您的测试会与之交互的区域。页面对象只是将它们建模为测试代码中的对象。这减少了重复代码的数量,并且意味着如果 UI 发生变化,只需在一个地方应用修复。
此外(来自同一文档)
页面对象可以被认为是同时面向两个方向的。面向测试开发人员,它们代表特定页面提供的服务。远离开发人员,它们应该是唯一对页面(或页面的一部分)的 HTML 结构有深入了解的东西。最简单的方法是将页面对象上的方法视为提供页面提供的“服务”,而不是暴露页面的细节和机制。例如,考虑任何基于 Web 的电子邮件系统的收件箱。它提供的服务通常包括编写新电子邮件、选择阅读一封电子邮件以及列出收件箱中电子邮件的主题行。这些如何实现对测试不重要。
1.3. jQuery 风格的导航器 API
jQuery JavaScript 库提供了一个出色的 API(除其他外)用于选择或定位页面上的内容以及遍历内容。Geb 从中汲取了大量灵感。
在 Geb 中,内容通过 $
函数选择,该函数返回一个 Navigator
对象。Navigator
对象在某些方面类似于 jQuery 中的 jQuery
数据类型,因为它代表页面上的一个或多个目标元素。
我们来看一些例子:
$("div") (1)
$("div", 0) (2)
$("div", title: "section") (3)
$("div", 0, title: "section") (4)
$("div.main") (5)
$("div.main", 0) (6)
1 | 匹配页面上所有的 "div" 元素。 |
2 | 匹配页面上的第一个 "div" 元素。 |
3 | 匹配所有 "div" 元素,其 title 属性值为 "section" 。 |
4 | 匹配第一个 title 属性值为 "section" 的 "div" 元素。 |
5 | 匹配所有具有 "main" 类的 "div" 元素。 |
6 | 匹配第一个具有 "main" 类的 "div" 元素。 |
这些方法返回 Navigator
对象,可用于进一步细化内容。
$("p", 0).parent() (1)
$("p").find("table", cellspacing: '0') (2)
1 | 第一个段落的父元素。 |
2 | 所有嵌套在段落中且 cellspacing 属性值为 0 的表格。 |
这只是 Navigator API 可能性的开始。有关更多详细信息,请参阅 导航器章节。
1.4. 完整示例
让我们看一个简单的例子:访问 Geb 的主页并导航到本手册的最新版本。
1.4.1. 行内脚本
这是一个使用 Geb 的内联(即没有页面对象或预定义内容)脚本样式的示例…
import geb.Browser
Browser.drive {
go "http://gebish.org"
assert title == "Geb - Very Groovy Browser Automation" (1)
$("div.menu a.manuals").click() (2)
waitFor { !$("#manuals-menu").hasClass("animating") } (3)
$("#manuals-menu a")[0].click() (4)
assert title.startsWith("The Book Of Geb") (5)
}
1 | 检查我们是否在 Geb 的主页上。 |
2 | 点击手册菜单项将其打开。 |
3 | 等待菜单打开动画完成。 |
4 | 点击第一个手册链接。 |
5 | 检查我们是否在《Geb 之书》页面。 |
1.4.2. 使用页面对象进行脚本编写
这次让我们使用页面对象模式预先定义我们的内容…
import geb.Module
import geb.Page
class ManualsMenuModule extends Module { (1)
static content = { (2)
toggle { $("div.menu a.manuals") }
linksContainer { $("#manuals-menu") }
links { linksContainer.find("a") } (3)
}
void open() { (4)
toggle.click()
waitFor { !linksContainer.hasClass("animating") }
}
}
class GebHomePage extends Page {
static url = "http://gebish.org" (5)
static at = { title == "Geb - Very Groovy Browser Automation" } (6)
static content = {
manualsMenu { module(ManualsMenuModule) } (7)
}
}
class TheBookOfGebPage extends Page {
static at = { title.startsWith("The Book Of Geb") }
}
1 | 模块是可在不同页面之间重用的片段。在这里,我们使用模块来建模一个下拉菜单。 |
2 | 内容 DSL。 |
3 | 内容定义可以使用其他内容定义来定义相对内容元素。 |
4 | 模块可以包含方法,这些方法允许隐藏文档结构细节或交互复杂性。 |
5 | 页面可以定义它们的位置,可以是绝对的,也可以是相对于基准的。 |
6 | “at 检查器”允许验证浏览器是否在预期的页面上。 |
7 | 包含之前定义的模块。 |
现在再次使用我们上面定义的内容来编写脚本…
import geb.Browser
Browser.drive {
to GebHomePage (1)
manualsMenu.open()
manualsMenu.links[0].click()
at TheBookOfGebPage
}
1 | 访问 GebHomePage 定义的 URL,并验证其“at 检查器”。 |
1.4.3. 测试
Geb 本身不包含任何类型的测试或执行框架。相反,它与现有流行工具(如 Spock、JUnit、TestNg 和 Cucumber-JVM)配合使用。虽然 Geb 与所有这些测试工具都能很好地配合,但我们鼓励使用 Spock,因为它专注于自身风格,与 Geb 非常匹配。
这是我们的 Geb 主页示例,这次使用 Geb 的 Spock 集成…
import geb.spock.GebSpec
class GebHomepageSpec extends GebSpec {
def "can access The Book of Geb via homepage"() {
given:
to GebHomePage
when:
manualsMenu.open()
manualsMenu.links[0].click()
then:
at TheBookOfGebPage
}
}
有关使用 Geb 进行 Web 和功能测试的更多信息,请参阅 测试章节。
1.5. 安装与使用
Geb 本身可作为单个 geb-core
jar 从中央 Maven 仓库获取。要开始运行,您只需要这个 jar、一个 WebDriver 驱动实现和 selenium-support
jar。
通过 @Grab
…
@Grapes([
@Grab("org.gebish:geb-core:7.0"),
@Grab("org.seleniumhq.selenium:selenium-firefox-driver:4.2.2"),
@Grab("org.seleniumhq.selenium:selenium-support:4.2.2")
])
import geb.Browser
通过 Maven…
<dependency>
<groupId>org.gebish</groupId>
<artifactId>geb-core</artifactId>
<version>7.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-firefox-driver</artifactId>
<version>4.2.2</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-support</artifactId>
<version>4.2.2</version>
</dependency>
通过 Gradle…
compile "org.gebish:geb-core:7.0"
compile "org.seleniumhq.selenium:selenium-firefox-driver:4.2.2"
compile "org.seleniumhq.selenium:selenium-support:4.2.2"
或者,如果使用 geb-spock
或 geb-junit4
等集成,您可以依赖它们而不是 geb-core
。您可以查看 org.gebish
组中所有工件的列表 以查看可用内容。
请务必查看关于 构建集成 的章节,以获取在特定环境中使用 Geb 的信息。 |
快照存储库
如果您想体验最新功能,可以尝试 Geb 的快照工件,它们位于 Maven 仓库:https://oss.sonatype.org/content/repositories/snapshots。
2. Browser
浏览器对象使用 Configuration
创建,该配置指定要使用的驱动程序实现、用于解析相对链接的基础 URL 以及其他配置位。配置机制允许您外部化 Geb 应如何操作,这意味着您可以将同一套 Geb 代码或测试与不同的浏览器或站点实例一起使用。配置章节 包含有关如何管理配置参数及其详细信息的更多详细信息。
Browser
的默认构造函数只从配置机制加载其设置。
import geb.Browser
def browser = new Browser()
然而,如果您希望指定驱动程序实现(或 Browser
上的任何其他可设置属性),您可以使用 Groovy 的映射构造函数语法。
import geb.Browser
import org.openqa.selenium.htmlunit.HtmlUnitDriver
def browser = new Browser(driver: new HtmlUnitDriver())
这与…
def browser = new Browser()
browser.driver = new HtmlUnitDriver()
以这种方式设置的任何属性都将**覆盖**来自配置机制的任何设置。
如果在浏览器首次使用后更改其驱动程序,则行为是未定义的,因此您应该避免以这种方式设置驱动程序,而更喜欢配置机制。 |
对于高度自定义的配置要求,您可以创建自己的 Configuration
对象,并使用它来构建浏览器,可能使用 配置加载器。
import geb.Browser
import geb.ConfigurationLoader
def loader = new ConfigurationLoader("a-custom-environment")
def browser = new Browser(loader.conf)
在可能的情况下,您应努力使用无参构造函数并通过内置的 配置机制 管理 Geb,因为它提供了极大的灵活性并将您的配置与代码分离。
Geb 集成通常无需构造浏览器对象,它们会为您完成此操作,让您只需管理配置。 |
2.1. 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()
有关何时/为何需要手动退出浏览器的更多信息,请参阅关于 驱动程序 的部分。 |
2.2. 发送请求
2.2.1. 基础 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. 使用页面
class SignupPage extends Page {
static url = "signup"
}
Browser.drive {
to SignupPage
assert $("h1").text() == "Signup Page"
assert page instanceof SignupPage
}
to()
和 via()
方法向解析后的 URL 发送请求,并将浏览器的页面实例设置为给定类的实例。大多数 Geb 脚本和测试都以 to()
或 via()
调用开始。
有关如何使用更复杂的页面 URL 解析的更多信息,请参阅 高级页面导航 部分。 |
2.2.3. 直接访问
您还可以向 URL 发送新请求,而无需使用 go()
方法设置或更改页面。
import geb.Page
import geb.spock.GebSpec
class GoogleHomePage extends Page {
static url = "http://www.google.com"
}
class GoogleSpec extends GebSpec {
def "go method does NOT set the page"() {
given:
Page oldPage = page
when:
go "http://www.google.com"
then:
oldPage == page
currentUrl.contains "google"
}
def "to method does set the page and change the current url"() {
given:
Page oldPage = page
when:
to GoogleHomePage
then:
oldPage != page
currentUrl.contains "google"
}
}
以下示例使用基础 URL 为“https:///
”。
Browser.drive {
go() (1)
go "signup" (2)
go "signup", param1: "value1", param2: "value2" (3)
}
1 | 转到基础 URL。 |
2 | 转到相对于基础 URL 的 URL。 |
3 | 转到带请求参数的 URL,例如 https:///signup?param1=value1¶m2=value2 |
2.3. Page
然而,页面属性很少被直接访问。浏览器对象将把任何它无法处理的方法调用或属性读/写*转发*到当前页面实例。
Browser.drive {
go "signup"
assert $("h1").text() == "Signup Page" (1)
assert page.$("h1").text() == "Signup Page" (1)
}
1 | 这两个调用是等效的。 |
是*页面*提供了 $()
函数,而不是浏览器。这种转发促进了非常简洁的代码,避免了不必要的冗余。
有关用于与页面内容交互的 |
使用页面对象模式时,您创建 Page
的子类,通过强大的 DSL 定义内容,允许您通过有意义的名称而不是标签名或 CSS 表达式来引用内容。
class SignupPage extends Page {
static url = "signup"
static content = {
heading { $("h1").text() }
}
}
Browser.drive {
to SignupPage
assert heading == "Signup Page"
}
页面对象在 页面 章节中深入讨论,该章节也探讨了内容 DSL。
更改页面
我们已经看到 to()
方法改变了浏览器的页面实例。也可以在不发起新请求的情况下,使用 page()
方法更改页面实例。
<T extends Page> T page(Class<T> pageType)
方法允许您将页面更改为**给定类**的新实例。该类必须是 Page
或其子类。此方法**不**验证给定页面是否实际匹配内容(稍后将讨论 at 检查)。
<T extends Page> T page(T pageInstance)
方法允许您将页面更改为**给定实例**。与接受页面类的方法类似,它**不**验证给定页面是否实际匹配内容。
Page page(Class<? extends Page>[] potentialPageTypes)
方法允许您指定多个*潜在*页面类型。每个潜在页面都会实例化并检查其 at 检查器以查看其是否与浏览器当前实际所在的内容匹配。传入的所有页面类都必须定义“at”检查器,否则将抛出 UndefinedAtCheckerException
。
Page page(Page[] potentialPageInstances)
方法允许您指定多个*潜在*页面实例。每个潜在页面实例都会初始化并检查其 at 检查器以查看其是否与浏览器当前实际所在的内容匹配。传入的所有页面实例都必须定义“at”检查器,否则将抛出 UndefinedAtCheckerException
。
这两个接受数组作为参数的方法通常不被显式使用,但被 to()
方法和指定点击后内容导航到的页面的内容定义使用(有关此内容,请参阅 内容 DSL 的 to
属性 部分)。但是,如果您需要手动更改页面类型,它们就在那里。
2.4. At 检查
页面定义了 “at 检查器”,浏览器使用它来检查是否指向给定页面。
class SignupPage extends Page {
static url = "signup"
static at = {
$("h1").text() == "Signup Page"
}
}
Browser.drive {
to SignupPage
}
不推荐在“at 检查器”中使用显式 |
接受单个页面类型或实例的 to()
方法**验证**浏览器是否最终到达给定页面。如果请求可能发起重定向并使浏览器导航到不同的页面,您应该使用 via()
方法
Browser.drive {
via SecurePage
at AccessDeniedPage
}
浏览器对象具有 <T extends Page> T at(Class<T> pageType)
和 <T extends Page> T at(T page)
方法,用于测试浏览器当前是否位于由给定页面类或实例建模的页面。
at AccessDeniedPage
方法调用在“at 检查器”通过时返回一个页面实例。另一方面,如果未通过,则会抛出 AssertionError
,即使“at 检查器”中没有显式断言(默认行为,详情请参阅 隐式断言 部分),或者如果禁用隐式断言则返回 null。
无论是使用隐式验证“at 检查器”的 to()
方法,还是使用 via()
方法后紧跟 at()
检查(每当页面更改时),都是一个好主意,以便*快速失败*。否则,后续步骤可能会因内容与预期不符而以更难诊断的方式失败,导致内容查找返回奇怪的结果。
如果您将未定义“at 检查器”的页面类或实例传递给 at()
,您将收到 UndefinedAtCheckerException
—— 在执行显式“at 检查”时,“at 检查器”是强制性的。默认情况下,在执行隐式“at 检查”时(例如使用 to()
时)并非如此,但这是可配置的。此行为旨在让您意识到在显式验证是否在给定页面时可能需要定义“at 检查器”,但不会强制您在使用隐式“at 检查”时这样做。
如果“at 检查器”成功,at()
方法也会更新浏览器的页面实例。这意味着您无需在“at 检查”后手动将浏览器页面设置为新页面。
页面还可以定义内容,这些内容声明浏览器在点击该内容时应更改为何种页面类型。点击此类内容后,如果声明的页面定义了“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. 处理多个标签页和窗口
当您使用会打开新窗口或标签页的应用程序时,例如点击带有 target 属性的链接时,您可以使用 withWindow()
和 withNewWindow()
方法在其他窗口的上下文中执行代码。
如果您确实需要知道当前窗口的名称或所有打开窗口的名称,请使用 Browser.getCurrentWindow()
和 Browser.getAvailableWindows()
方法,但 withWindow()
和 withNewWindow()
是处理多个窗口时首选的方法。
2.5.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
。
如果没有窗口满足窗口规范闭包返回 |
所以给出
<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()
选项
有一些额外的选项可以传递给 withWindow()
调用,这使得处理已打开的窗口更加简单。一般语法是:
withWindow(«window specification», «option name»: «option value», ...) {
«action executed within the context of the window»
}
页面
默认值:null
如果您将一个类或一个继承自 Page
的类的实例作为 page
选项传递,那么在执行作为最后一个参数传入的闭包之前,浏览器的页面将被设置为该值,并在之后恢复为原始值。如果页面类定义了“at 检查器”,那么在页面设置为浏览器时,它将被验证。
2.5.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'
}
}
如果第一个参数没有打开任何窗口或打开了多个窗口,则会抛出 |
如果作为最后一个参数传入的闭包代码更改了浏览器的当前页面实例(例如通过使用 |
withNewWindow()
选项
有一些选项可以传递给 withNewWindow()
调用,这些选项使处理新打开的窗口变得更简单。一般语法是:
withNewWindow({ «window opening action» }, «option name»: «option value», ...) {
«action executed within the context of the window»
}
等待
默认值:null
如果作为第一个参数传入的窗口打开闭包中定义的操作是异步的,并且您需要等待新窗口打开,您可以指定 wait
选项。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.6. 暂停和调试
为了调试目的,在某些给定点暂停使用 Geb 的代码执行通常很有用。这可以通过以下两种方式实现:
-
在 IDE 中设置断点,并在调试模式下运行 JVM
-
使用浏览器的
pause()
方法
虽然前者功能更强大,因为它允许检查 JVM 中的变量值,并在设置断点的地方使用 Geb 的类和方法,但后者设置起来可能更简单快捷,并且通常只需在浏览器中打开开发人员控制台并检查 DOM 和 javascript 变量就足以解决正在调试的问题。
如果您希望在调用 请注意,如果在调用 |
2.7. 本地和会话存储
可以使用 Browser
实例的 localStorage
属性访问本地存储,使用 sessionStorage
属性访问会话存储。它们都可以用于读写底层存储的值,就像它们是映射一样。以下是设置和读取本地存储值的示例:
Browser.drive {
localStorage["username"] = "Alice"
assert localStorage["username"] == "Alice"
}
localStorage
和 sessionStorage
属性都是 geb.webstorage.WebStorage
类型——请参阅其 Javadoc 以获取除读写值之外支持的其他操作的信息。
并非所有驱动程序实现都支持访问 Web 存储。当尝试在使用不支持 Web 存储的驱动程序时访问 Web 存储,将抛出 |
3. WebDriver 实现
Browser
实例通过 WebDriver
实例与实际浏览器交互。浏览器驱动程序始终可以通过 getDriver()
方法检索。
WebDriver 奉行的关键设计原则之一是,测试/脚本应该针对 |
3.1. 显式驱动管理
指定驱动实现的一种选项是构造驱动实例并将其传递给 Browser
,以便在构造时使用。
然而,在可能的情况下,首选本章后面讨论的隐式驱动管理。
显式生命周期
当驱动程序由用户构造时,用户负责在适当的时候退出驱动程序。这可以通过 WebDriver
实例上的方法(可通过 getDriver()
获取)或通过调用 浏览器对象上的委托方法 来完成。
3.2. 隐式驱动管理
如果在构造 Browser
对象时未提供驱动程序,Geb 将通过 配置机制 隐式创建和管理一个驱动程序。
隐式生命周期
默认情况下,Geb 内部缓存并重用创建的第一个驱动程序,这意味着所有后续在没有显式驱动程序的情况下创建的浏览器实例将重用缓存的驱动程序。这避免了每次创建新驱动程序的开销,这在处理真实浏览器时可能很显著。
这意味着您可能需要调用浏览器上的 clearCookies()
或 clearCookies(String… additionalUrls)
以及 clearWebStorage()
方法,以免因先前执行留下的状态而产生奇怪的结果。
共享驱动器将在 JVM 关闭时关闭并退出。
可以通过调用 CachingDriverFactory.clearCache()
或 CachingDriverFactory.clearCacheAndQuitDriver()
中的任何一个方法随时强制使用新驱动程序,这两个方法都是 static
。调用这些方法后,下一次对默认驱动程序的请求将导致创建新的驱动程序实例。
这种缓存行为是可配置的。
3.3. 驱动器怪癖
本节详细介绍了不同驱动实现中遇到的各种怪癖或问题。
HTMLUnitDriver
处理使用 HTML 刷新的页面
HTMLUnit 驱动程序的默认行为是,一旦遇到 <meta http-equiv="refresh" content="5">
,无论指定时间如何,立即刷新页面。解决方案是使用异步处理刷新的刷新处理程序。
import com.gargoylesoftware.htmlunit.ThreadedRefreshHandler
Browser.drive {
driver.webClient.refreshHandler = new ThreadedRefreshHandler()
(1)
}
1 | 从这里开始,刷新元标签值将被遵守。 |
有关详细信息,请参阅 此邮件列表线程。
配置日志
HTMLUnit 会产生很多噪音,而且不清楚如何让它不那么吵。
有关如何调整其日志记录的提示,请参阅 此问题。
4. 与内容交互
Geb 为浏览器中的内容和控件提供了一个简洁的 Groovy 接口。这通过 Navigator
API 实现,该 API 是一种受 jQuery 启发,用于查找、过滤和与 DOM 元素交互的机制。
4.1. $()
函数
$()
函数是浏览器页面内容的访问点。它返回一个 Navigator
对象,该对象大致类似于 jQuery 对象。它之所以类似,是因为它代表页面上的一个或多个元素,并且可以用于优化匹配的内容或查询匹配的内容。当调用不匹配任何内容的 $()
函数时,会返回一个表示无内容的“空”导航器对象。对“空”导航器的操作返回 null
或另一个“空”导航器或其他有意义的值(例如,size()
方法返回 0
)。
$()
函数的签名如下…
$(«css selector», «index or range», «attribute / text / visibility matchers»)
以下是一个具体示例…
$("h1", 2, class: "heading")
这将找到其 class
*属性*恰好为“heading
”的第 3 个(元素索引从 0 开始)h1
元素。
所有参数都是可选的,这意味着以下所有调用都有效:
$("div p", 0)
$("div p", title: "something")
$(0)
$(title: "something")
如果“$”函数不符合您的口味,它有一个别名,名为“find”。 |
4.1.1. CSS 选择器
您可以使用底层 WebDriver
实现支持的任何 CSS 选择器…
$('div.some-class p:first-child[title^="someth"]')
4.1.2. 使用 WebDriver 的 By
类选择器
对于 $()
函数的所有接受 CSS 选择器的签名,都有一个等效的签名,其中可以使用 WebDriver 的 By
类的实例而不是 String
。
使用 CSS 选择器是使用 Geb 的惯用方式,应优先于使用 By
选择器。除了某些 XPath 选择器,始终可以使用 CSS 选择器选择相同的元素,这就是提供此便利机制的原因。
以下是一些使用 By
选择器的示例…
$(By.id("some-id"))
$(By.className("some-class"))
$(By.xpath('//p[@class="xpath"]'))
4.1.3. 动态导航器
默认情况下,当 Navigator
实例被创建时,会存储对当时匹配选择器的元素的常量引用。在许多情况下,这是期望的行为,因为它限制了查找元素执行的 WebDriver 命令数量,但在现代 javascript 框架和单页应用程序时代,情况可能并非如此。在处理完全在模型更改时重新渲染 DOM 部分而不是更新它的应用程序时,情况尤其如此。
通常,DOM 元素支持 Navigator
已从 DOM 中删除并重新绘制的事实可以通过从其 内容定义 获取一个新的 Navigator
实例来解决。不幸的是,当在 Geb 模块类 中定义的方法执行的操作导致 模块基本元素 被重新渲染时,此解决方法不可用。这是因为模块基本 Navigator
的引用在模块创建时已固定,这反过来又固定了对用于该 Navigator
定义的选择器匹配的元素的引用。当包含该 Navigator
元素的 DOM 部分被重新创建时,这些引用将变为陈旧,导致对其调用的任何方法抛出 StaleElementReferenceException
,并且它们的陈旧性无法从相关模块方法的代码中纠正。解决此问题的方法是将基本 Navigator
声明为动态的,这意味着每次调用访问元素的 Dynamic Navigator
上的方法时,支持该 Navigator
的 DOM 元素列表都将刷新。
任何返回 Navigator
并接受属性映射作为参数之一的方法(这些方法大多在 查找和过滤 和 遍历 中描述)都可以通过简单地传递一个 真值 作为 dynamic
属性来创建动态 Navigator
…
$("div", dynamic: true)
您应该记住,动态 |
让我们考虑一个简单的 Vue.js 应用程序,它使用以下模板渲染一个可以重新排序的列表项…
<div id="app">
<ul>
<li v-for="(berry,index) in berries" :key="`${berry}-${index}`">
<button v-if="index != 0" v-on:click="swapWithNext(index - 1)">⇑</button>
<button v-if="index != berries.length - 1" v-on:click="swapWithNext(index)">⇓</button>
<span>{{ berry }}</span>
</li>
</ul>
</div>
…以及以下 JavaScript 代码…
var app = new Vue({
el: '#app',
data: {
berries: [
'strawberry',
'raspberry',
'blueberry',
'cranberry'
]
},
methods: {
swapWithNext: function(index) {
this.berries.splice(index, 2, this.berries[index + 1], this.berries[index])
}
}
});
需要注意的是,li
元素在重新排序时会被重新渲染。
让我们使用以下模块来建模列表中的每个项目…
class ListItem extends Module {
static content = {
textElement { $("span") }
upButton { $("button", text: "⇑") }
}
void moveUpBy(int count) {
count.times { upButton.click() }
}
@Override
String text() {
textElement.text()
}
}
给定以下内容定义…
static content = {
item { text -> $("li", text: endsWith(text)).module(ListItem) }
}
以下代码将因 StaleElementReferenceException
而失败…
item("blueberry").moveUpBy(2)
如果我们将列表元素模块的基础导航器更改为动态的,如下面的页面类所示…
class PageWithListHandlingRerenders extends Page {
static content = {
items { $("li").moduleList(ListItem) }
item { text -> $("li", text: endsWith(text), dynamic: true).module(ListItem) }
}
}
那么以下内容将通过…
to PageWithListHandlingRerenders
item("blueberry").moveUpBy(2)
assert items*.text() == ["blueberry", "strawberry", "raspberry", "cranberry"]
虽然使用动态导航器似乎是完全避免 |
直接从 WebElement
对象创建
某些选择逻辑,例如根据元素大小选择元素,无法通过 Geb 的 $()
方法表达,但可以通过过滤 Selenium 的 WebElement 实例来表达。因此,例如,如果您想创建一个匹配页面中所有高度恰好为 10 像素的 div 的动态 Navigator
,您可以使用 NavigatorFactory.createDynamic() 来实现…
browser.navigatorFactory.createDynamic {
$("div").allElements().findAll { it.size.height == 10 }
}
4.1.4. 索引和范围
匹配时,可以给出一个正整数或整数范围以限制索引。
考虑以下 html…
<p>a</p>
<p>b</p>
<p>c</p>
我们可以使用索引来匹配内容,如下所示…
assert $("p", 0).text() == "a"
assert $("p", 2).text() == "c"
assert $("p", 0..1)*.text() == ["a", "b"]
assert $("p", 1..2)*.text() == ["b", "c"]
有关 text()
方法和展开运算符的用法,请参见下文。
4.1.5. 属性、文本和可见性匹配
可以通过 Groovy 的命名参数语法对属性、节点文本值以及可见性进行匹配。对于节点文本的匹配使用 text
作为参数名称,对于节点可见性的匹配使用 displayed
作为参数名称。所有其他值都与其相应的属性值进行匹配。
属性和文本
考虑以下 html…
<p attr1="a" attr2="b">p1</p>
<p attr1="a" attr2="c">p2</p>
我们可以像这样使用属性匹配器…
assert $("p", attr1: "a").size() == 2
assert $("p", attr2: "c").size() == 1
属性值是“与”在一起的…
assert $("p", attr1: "a", attr2: "b").size() == 1
我们可以像这样使用文本匹配器…
assert $("p", text: "p1").size() == 1
你可以混合使用属性和文本匹配器…
assert $("p", text: "p1", attr1: "a").size() == 1
使用模式
要匹配属性的整个值或文本,您可以使用 String
值。也可以使用 Pattern
进行正则表达式匹配…
assert $("p", text: ~/p./).size() == 2
Geb 还附带了一系列快捷模式方法…
assert $("p", text: startsWith("p")).size() == 2
assert $("p", text: endsWith("2")).size() == 1
以下是完整列表
区分大小写 | 不区分大小写 | 描述 |
---|---|---|
|
|
匹配以给定值开头的值 |
|
|
匹配包含给定值的值 |
|
|
匹配以给定值结尾的值 |
|
|
匹配包含给定值,且该值被空格或值的开头或结尾包围的值 |
|
|
匹配不以给定值开头的值 |
|
|
匹配不包含给定值的值 |
|
|
匹配不以给定值结尾的值 |
|
|
匹配不包含给定值且该值不被空格或值的开头或结尾包围的值 |
所有这些方法本身都可以接受 String
或 Pattern
…
assert $("p", text: contains(~/\d/)).size() == 2
您可能想知道这种魔法是如何运作的,即这些方法来自何处以及它们可以在何处使用。它们是 |
使用多个模式
有时,您需要搜索替代模式,例如,如果您有一个多品牌页面,每个品牌都有不同的文本,在这种情况下,您可以使用复合匹配器 allOf
和 anyOf
。两者都接受其他匹配器的任意组合。
assert $("p", text: allOf(contains("p1"), contains("p2"))).size() == 0
assert $("p", text: anyOf(contains("p1"), contains("p2"))).size() == 2
复合匹配器 | 描述 |
---|---|
|
如果所有给定的匹配器都匹配,则匹配 |
|
如果任何给定的匹配器匹配,则匹配 |
可见性
考虑以下 html…
<p>p1</p>
<p style="display: none;">p2</p>
我们可以像这样使用可见性匹配器…
assert $("p", displayed: true).size() == 1
4.1.6. 导航器是可迭代的
导航器对象实现了 Java 的 Iterable
接口,这允许您执行许多 Groovy 操作,例如使用 max()
函数…
考虑以下 html…
<p>1</p>
<p>2</p>
您可以在 Navigator
实例上使用 max()
函数…
assert $("p").max { it.text() }.text() == "2"
这也意味着导航器对象支持 Groovy 的展开运算符…
assert $("p")*.text().max() == "2"
当将导航器视为 Iterable
时,迭代的内容始终是精确匹配的元素(而不是包含子元素)。
4.1.7. 监听导航器事件
可以注册一个监听器,以便在某些导航器事件发生时得到通知。可监听事件的最佳参考是 NavigatorEventListener
接口的文档。如果您只想监听导航器事件的子集,那么 NavigatorEventListenerSupport
可能会派上用场,因为它附带了 NavigatorEventListener
所有方法的默认空实现。
利用导航器事件监听器的一个用例是,通过在每次导航器被点击、其值被更改等操作时写入报告来增强报告功能。
以下示例展示了如何在配置脚本中注册一个导航器事件监听器,该监听器在导航器被点击后简单地打印导航器标签…
import geb.Browser
import geb.navigator.Navigator
import geb.navigator.event.NavigatorEventListenerSupport
navigatorEventListener = new NavigatorEventListenerSupport() {
void afterClick(Browser browser, Navigator navigator) {
println "${navigator*.tag()} was clicked"
}
}
4.1.8. equals()
和 hashCode()
可以检查 Navigator
实例的相等性。规则很简单——两个空导航器总是相等,两个非空导航器只有在包含完全相同的元素且顺序相同时才相等。
考虑以下 HTML…
<p class="a"></p>
<p class="b"></p>
以下是一些相等的 Navigator 实例示例…
assert $("div") == $(".foo") (1)
assert $(".a") == $(".a") (2)
assert $(".a") == $("p").not(".b") (3)
assert $("p") == $("p") (4)
assert $("p") == $(".a").add(".b") (5)
1 | 两个空导航器 |
2 | 两个包含相同元素的单元素导航器 |
3 | 两个包含相同元素但使用不同方法创建的单元素导航器 |
4 | 两个包含相同元素的多元素导航器 |
5 | 两个包含相同元素但使用不同方法创建的多元素导航器 |
还有一些不相等的…
assert $("div") != $("p") (1)
assert $(".a") != $(".b") (2)
assert $(".a").add(".b") != $(".b").add(".a") (3)
1 | 空的和非空的导航器 |
2 | 包含不同元素的单元素导航器 |
3 | 包含相同元素但顺序不同的多元素导航器 |
4.2. 查找和过滤
导航器对象具有 find()
和 $()
方法用于查找后代,以及 filter()
和 not()
方法用于减少匹配内容。
考虑以下 HTML…
<div class="a">
<p class="b">geb</p>
</div>
<div class="b">
<input type="text"/>
</div>
我们可以通过…选择 p.b
$("div").find(".b")
$("div").$(".b")
我们可以通过…选择 div.b
$("div").filter(".b")
或者…
$(".b").not("p")
我们可以选择包含 p
的 div
,如下所示…
$("div").has("p")
或者选择包含类型属性为“text”的 input
的 div
,如下所示…
$("div").has("input", type: "text")
我们可以选择不包含 p
的 div
,如下所示…
$("div").hasNot("p")
或者选择不包含类型属性为“text”的 input
的 div
,如下所示…
$("div").hasNot("input", type: "text")
或者选择不包含类型属性为“submit”的 input
的两个 div
,如下所示…
$("div").hasNot("input", type: "submit")
find()
方法支持**与 $()
函数完全相同的参数类型**。
filter()
、not()
、has()
和 hasNot()
方法具有相同的签名——它们接受:选择器字符串、谓词映射或两者兼而有之。
这些方法返回一个表示新内容的新导航器对象。
4.3. 组合
也可以从其他导航器对象组合导航器对象,适用于您无法在一个查询中表达内容集的情况。为此,只需调用 $()
函数并传入要组合的导航器。
考虑以下标记…
<p class="a">1</p>
<p class="b">2</p>
<p class="c">3</p>
然后,您可以通过以下方式创建表示 a
和 b
段落的新导航器对象:
assert $($("p.a"), $("p.b"))*.text() == ["1", "2"]
另一种方法是使用 Navigator
的 add()
方法,该方法接受 String
或 WebDriver 的 By
选择器
assert $("p.a").add("p.b").add(By.className("c"))*.text() == ["1", "2", "3"]
最后,您可以从内容组合导航器对象。因此,给定一个页面内容定义:
static content = {
pElement { pClass -> $('p', class: pClass) }
}
您可以按以下方式将内容元素组合到导航器中:
assert $(pElement("a"), pElement("b"))*.text() == ["1", "2"]
4.4. 遍历
导航器还提供了用于选择匹配内容*周围*内容的方法。
考虑以下 HTML…
<div class="a">
<div class="b">
<p class="c"></p>
<p class="d"></p>
<p class="e"></p>
</div>
<div class="f"></div>
</div>
你可以通过…选择 p.d
*周围*的内容
assert $("p.d").previous() == $("p.c")
assert $("p.e").prevAll() == $("p.c").add("p.d")
assert $("p.d").next() == $("p.e")
assert $("p.c").nextAll() == $("p.d").add("p.e")
assert $("p.d").parent() == $("div.b")
assert $("p.c").siblings() == $("p.d").add("p.e")
assert $("div.a").children() == $("div.b").add("div.f")
考虑以下 HTML…
<div class="a">
<p class="a"></p>
<p class="b"></p>
<p class="c"></p>
</div>
以下代码将选择 p.b
和 p.c
…
assert $("p").next() == $("p.b").add("p.c")
previous()
、prevAll()
、next()
、nextAll()
、parent()
、parents()
、closest()
、siblings()
和 children()
方法也可以接受 CSS 选择器和属性匹配器。
使用相同的 HTML,以下示例将选择 p.c
…
assert $("p").next(".c") == $("p.c").add("p.c")
assert $("p").next(class: "c") == $("p.c").add("p.c")
assert $("p").next("p", class: "c") == $("p.c").add("p.c")
同样,考虑以下 HTML…
<div class="a">
<div class="b">
<p></p>
</div>
</div>
以下示例将选择 div.b
…
assert $("p").parent(".b") == $("div.b")
assert $("p").parent(class: "b") == $("div.b")
assert $("p").parent("div", class: "b") == $("div.b")
closest()
方法是一个特例,因为它将选择与选择器匹配的当前元素的第一个祖先。closest()
方法没有无参数版本。例如,这些将选择 div.a
…
assert $("p").closest(".a") == $("div.a")
assert $("p").closest(class: "a") == $("div.a")
assert $("p").closest("div", class: "a") == $("div.a")
这些方法不接受索引,因为它们会自动选择第一个匹配的内容。要选择多个元素,您可以使用 prevAll()
、nextAll()
和 parents()
,它们都有无参数版本和按选择器过滤的版本。
nextUntil()
、prevUntil()
和 parentsUntil()
方法返回相关轴上的所有节点,直到第一个匹配选择器或属性的节点。考虑以下标记
<div class="a"></div>
<div class="b"></div>
<div class="c"></div>
<div class="d"></div>
以下示例将选择 div.b
和 div.c
assert $(".a").nextUntil(".d") == $("div.b").add("div.c")
assert $(".a").nextUntil(class: "d") == $("div.b").add("div.c")
assert $(".a").nextUntil("div", class: "d") == $("div.b").add("div.c")
4.5. 点击
导航器对象实现了 click()
方法,该方法将指示浏览器点击导航器匹配的唯一元素。如果该方法在多元素 Navigator
上调用,则会抛出 SingleElementNavigatorOnlyMethodException
。
还有 click(Class)
、click(Page)
和 click(List)
方法,它们分别类似于浏览器对象的 page(Class<? extends Page>)
、page(Page)
和 page(Class<? extends Page>[])
、page(Page[])
方法。这允许在点击操作的同时指定页面更改。
例如…
$("a.login").click(LoginPage)
将点击 a.login
元素,然后有效调用 browser.page(LoginPage)
并验证浏览器是否在预期页面。
使用列表变体时传入的所有页面类都必须定义“at”检查器,否则将抛出 UndefinedAtCheckerException
。
4.6. 判断可见性
Navigator
对象具有 displayed
属性,指示元素是否对用户可见。不匹配任何内容的导航器对象的 displayed
属性始终为 false
。
4.7. 焦点
Geb 提供了一种简单的方法来查找当前焦点元素,即 focused()
方法,它返回一个 Navigator
。
也可以使用给定 Navigator
对象的 focused
属性来验证它是否持有当前焦点元素。不匹配任何元素的 Navigator
对象将返回 false
作为 focused
属性的值,而匹配多个元素的 Navigator
对象将抛出 SingleElementNavigatorOnlyMethodException
。
给定以下 HTML…
<input type="text" name="description"></input>
focused()
方法和导航器的 focused
属性可以按以下方式使用…
$(name: "description").click()
assert focused().attr("name") == "description"
assert $(name: "description").focused
4.8. 大小和位置
您可以获取页面上内容的大小和位置。所有单位均为像素。大小可通过 height
和 width
属性获得,而位置则以 x
和 y
属性的形式提供,它们表示从页面(或父框架)左上角到内容左上角的距离。
所有这些属性都仅对**唯一**匹配的元素进行操作,如果它们在多元素 Navigator
上访问,则会抛出 SingleElementNavigatorOnlyMethodException
。
考虑以下 html…
<div style="height: 20px; width: 40px; position: absolute; left: 20px; top: 10px"></div>
<div style="height: 40px; width: 100px; position: absolute; left: 30px; top: 150px"></div>
以下条件满足它…
assert $("div", 0).height == 20
assert $("div", 0).width == 40
assert $("div", 0).x == 20
assert $("div", 0).y == 10
要获取所有匹配元素的任何属性,您可以使用 Groovy 的展开运算符。
assert $("div")*.height == [20, 40]
assert $("div")*.width == [40, 100]
assert $("div")*.x == [20, 30]
assert $("div")*.y == [10, 150]
4.9. 访问标签名、属性、文本和类
tag()
、text()
和 classes()
方法以及通过 @
符号或 attr()
方法访问属性,都返回**唯一**匹配元素上请求的内容。如果这些方法在多元素 Navigator
上调用,则会抛出 SingleElementNavigatorOnlyMethodException
。classes()
方法返回按字母顺序排序的类名 java.util.List
。
考虑以下 HTML…
<p title="a" class="a para">a</p>
<p title="b" class="b para">b</p>
<p title="c" class="c para">c</p>
以下断言有效…
assert $(".a").text() == "a"
assert $(".a").tag() == "p"
assert $(".a").@title == "a"
assert $(".a").classes() == ["a", "para"]
要获取所有匹配内容的信息,请使用 Groovy *展开运算符*…
assert $("p")*.text() == ["a", "b", "c"]
assert $("p")*.tag() == ["p", "p", "p"]
assert $("p")*.@title == ["a", "b", "c"]
assert $("p")*.classes() == [["a", "para"], ["b", "para"], ["c", "para"]]
4.10. CSS 属性
可以使用 css()
方法访问单个元素导航器的 CSS 属性。如果该方法在多元素 Navigator
上调用,则会抛出 SingleElementNavigatorOnlyMethodException
。
考虑以下 HTML…
<div style="float: left">text</div>
您可以通过以下方式获取 float
CSS 属性的值…
assert $("div").css("float") == "left"
在检索 |
4.11. 发送按键
给定以下 HTML…
<input type="text"/>
您可以通过左移运算符将按键发送到输入(以及任何其他内容),这是 WebDriver 的 sendKeys()
方法的快捷方式。
$("input") << "foo"
assert $("input").value() == "foo"
内容对按键的响应取决于内容的类型。
非字符(例如删除键、组合键等)
可以使用 WebDriver Keys
枚举将非文本字符发送到内容…
import org.openqa.selenium.Keys
$("input") << Keys.chord(Keys.CONTROL, "c")
这里我们向一个输入框发送一个“control-c”。
有关可能按键的更多信息,请参阅 Keys
枚举的文档。
4.12. 访问输入值
input
、select
和 textarea
元素的值可以使用 value
方法获取和设置。不带参数调用 value()
将返回 Navigator 中唯一元素的字符串值。如果该方法在多元素 Navigator
上调用,则会抛出 SingleElementNavigatorOnlyMethodException
。调用 value(value)
将设置 Navigator 中所有元素的当前值。参数可以是任何类型,必要时将强制转换为 String
。例外情况是,设置 checkbox
值时,方法期望一个 boolean
(或一个现有复选框值或标签),设置多个 select
时,方法期望一个值数组或 Collection
。
4.13. 表单控件快捷方式
与表单控件(input
、select
等)交互是 Web 功能测试中常见的任务,因此 Geb 为常见功能提供了便捷的快捷方式。
Geb 支持以下用于处理表单控件的快捷方式。
考虑以下 HTML…
<form>
<input type="text" name="geb" value="testing" />
</form>
可以通过属性表示法读取和写入值……
assert $("form").geb == "testing"
$("form").geb = "goodness"
assert $("form").geb == "goodness"
这些实际上是以下内容的快捷方式……
assert $("form").find("input", name: "geb").value() == "testing"
$("form").find("input", name: "geb").value("goodness")
assert $("form").find("input", name: "geb").value() == "goodness"
还有一个快捷方式可以通过控件名称获取导航器……
assert $("form").geb() == $("form").find("input", name: "geb")
在上面和下面使用表单控件的示例中,我们使用了 |
如果您的内容定义(无论是页面还是模块)描述了作为 input
、select
或 textarea
的内容,您可以通过与上述表单相同的方式访问和设置其值。给定上述 HTML 的页面和模块定义
class ShortcutModule extends Module {
static content = {
geb { $('form').geb() }
}
}
class ShortcutPage extends Page {
static content = {
geb { $('form').geb() }
shortcutModule { module ShortcutModule }
}
}
以下内容将会通过
page ShortcutPage
assert geb == "testing"
geb = "goodness"
assert geb == "goodness"
以及
page ShortcutPage
assert shortcutModule.geb == "testing"
shortcutModule.geb = "goodness"
assert shortcutModule.geb == "goodness"
4.14. 设置表单控件值
以下示例描述了仅使用 |
尝试在不是 |
4.14.1. 选择
选择值通过分配所需选项的值或文本来设置。分配的值会自动强制转换为字符串。例如……
<form>
<select name="artist">
<option value="1">Ima Robot</option>
<option value="2">Edward Sharpe and the Magnetic Zeros</option>
<option value="3">Alexander</option>
</select>
</form>
我们可以使用以下方式选择选项……
$("form").artist = "1" (1)
$("form").artist = 2 (2)
$("form").artist = "Alexander" (3)
1 | 第一个选项由其值属性选择。 |
2 | 第二个选项通过其值属性和参数强制转换进行选择。 |
3 | 第三个选项通过其文本选择。 |
如果您尝试将选择设置为与任何选项的值或文本不匹配的值,则会抛出 IllegalArgumentException
。
4.14.2. 多选
如果选择具有 multiple
属性,则使用值数组或 Collection
设置。不在值中的任何选项都将被取消选择。例如……
<form>
<select name="genres" multiple>
<option value="1">Alt folk</option>
<option value="2">Chiptunes</option>
<option value="3">Electroclash</option>
<option value="4">G-Funk</option>
<option value="5">Hair metal</option>
</select>
</form>
我们可以使用以下方式选择选项……
$("form").genres = ["2", "3"] (1)
$("form").genres = [1, 4, 5] (2)
$("form").genres = ["Alt folk", "Hair metal"] (3)
$("form").genres = [] (4)
1 | 第二和第三个选项由其值属性选择。 |
2 | 第一、第四和第五个选项由其值属性和参数强制转换进行选择。 |
3 | 第一和最后一个选项由其文本选择。 |
4 | 所有选项都未选择。 |
如果分配的集合包含与任何选项的值或文本不匹配的值,则会抛出 IllegalArgumentException
。
4.14.3. 复选框
复选框通常通过将其值设置为 true
或 false
来选中/取消选中。
考虑以下 html…
<form>
<input type="checkbox" name="pet" value="dog" />
</form>
您可以按以下方式设置单个复选框……
$("form").pet = true
在已选中复选框上调用 value()
将返回其 value
属性的值,即
<form>
<input type="checkbox" name="pet" value="dog" checked/>
</form>
assert $("input", name: "pet").value() == "dog"
assert $("form").pet == "dog"
在未选中复选框上调用 value()
将返回 null
,即
<form>
<input type="checkbox" name="pet" value="dog"/>
</form>
assert $("input", name: 'pet').value() == null
assert $("form").pet == null
通常,在检查复选框是否选中时,您应该使用 {`}[Groovy Truth]
<form>
<input type="checkbox" name="checkedByDefault" value="checkedByDefaulValue" checked/>
<input type="checkbox" name="uncheckedByDefault" value="uncheckedByDefaulValue"/>
</form>
assert $("form").checkedByDefault
assert !$("form").uncheckedByDefault
4.14.4. 多个复选框
您还可以通过显式设置其 value
或使用其标签来选中复选框。当您有多个同名复选框时,这很有用,例如:
<form>
<label for="dog-checkbox">Canis familiaris</label>
<input type="checkbox" name="pet" value="dog" id="dog-checkbox"/>
<label for="cat-checkbox">Felis catus</label>
<input type="checkbox" name="pet" value="cat" id="cat-checkbox" />
<label for="lizard-checkbox">Lacerta</label>
<input type="checkbox" name="pet" value="lizard" id="lizard-checkbox" />
</form>
您可以如下选择“狗”作为您的宠物类型
$("form").pet = "dog"
$("form").pet = "Canis familiaris"
如果您希望选择多个复选框而不仅仅是一个,那么您可以使用集合作为值
$("form").pet = ["dog", "lizard"]
$("form").pet = ["Canis familiaris", "Lacerta"]
当检查复选框是否选中并且有多个同名复选框时,请确保在使用 value()
之前使用只包含其中一个的导航器
<form>
<input type="checkbox" name="pet" value="dog" checked/>
<input type="checkbox" name="pet" value="cat" />
</form>
assert $("input", name: "pet", value: "dog").value()
assert !$("input", name: "pet", value: "cat").value()
4.14.5. 单选按钮
单选值通过分配要选择的单选按钮的值或与单选按钮关联的标签文本来设置。
例如,使用以下单选按钮……
<form>
<label for="site-current">Search this site</label>
<input type="radio" id="site-current" name="site" value="current">
<label>Search Google
<input type="radio" name="site" value="google">
</label>
</form>
我们可以按值选择单选按钮……
$("form").site = "current"
assert $("form").site == "current"
$("form").site = "google"
assert $("form").site == "google"
或者按标签文本……
$("form").site = "Search this site"
assert $("form").site == "current"
$("form").site = "Search Google"
assert $("form").site == "google"
4.14.6. 文本输入和文本区域
在文本 input
或 textarea
的情况下,分配的值成为元素的 value 属性。
<form>
<input type="text" name="language"/>
<input type="text" name="description"/>
</form>
$("form").language = "gro"
$("form").description = "Optionally statically typed dynamic lang"
assert $("form").language == "gro"
assert $("form").description == "Optionally statically typed dynamic lang"
也可以通过使用发送键快捷方式附加文本……
$("form").language() << "ovy"
$("form").description() << "uage"
assert $("form").language == "groovy"
assert $("form").description == "Optionally statically typed dynamic language"
也可以用于非字符键……
import org.openqa.selenium.Keys
$("form").language() << Keys.BACK_SPACE
assert $("form").language == "groov"
WebDriver 在文本区域和周围的空白方面存在一些问题。即,某些驱动程序会隐式地从值的开头和结尾修剪空白。您可以在此处跟踪此问题。 |
4.14.7. 文件上传
目前,使用 WebDriver 无法模拟用户单击文件上传控件并通过正常文件选择器选择要上传的文件。但是,您可以直接将上传控件的值设置为驱动程序运行系统上文件的绝对路径,并且在表单提交时,该文件将被上传。因此,如果您的 HTML 看起来像……
<input type="file" name="csvFile"/>
并且 uploadedFile
变量持有指向您要上传的文件的 File
实例,那么您可以这样设置上传控件的值……
$("form").csvFile = uploadedFile.absolutePath
4.15. 复杂交互
WebDriver 支持比简单点击或输入项目更复杂的交互,例如拖动。您在使用 Geb 时可以直接使用此 API,或者使用更适合 Geb 的 interact
DSL。
4.15.1. 直接使用 WebDriver Actions
API
Geb 导航器对象构建在 WebDriver WebElement 对象的集合之上。可以通过导航器对象的以下方法访问包含的 WebElement
实例
WebElement singleElement()
WebElement firstElement()
WebElement lastElement()
Collection<WebElement> allElements()
通过使用 WebDriver Actions
类的方法与 WebElement
实例,可以模拟复杂的用户手势。首先,在获取 WebDriver 驱动程序后,您需要创建一个 Actions
实例
def actions = new Actions(driver)
接下来,使用 Actions
的方法来组合一系列 UI 操作,然后调用 build()
创建一个具体的 Action
WebElement someItem = $("li.clicky").firstElement()
def shiftClick = actions.keyDown(Keys.SHIFT).click(someItem).keyUp(Keys.SHIFT).build()
最后,调用 perform()
实际触发所需的鼠标或键盘行为
shiftClick.perform()
4.15.2. 使用 interact()
为了避免在模拟用户手势时通过 Actions
实例的生命周期管理构建和执行 Action
,以及从 Navigator
获取 WebElement
实例,Geb 添加了 interact()
方法。使用该方法时,会隐式创建 Actions
实例,构建成 Action
,并执行。传递给 interact()
的闭包的委托是 InteractDelegate
的实例,该实例声明了与 Actions
相同的方法,但对于接受 WebElement
的 Actions
方法,它接受 Navigator
作为参数。
此 interact()
调用执行与直接使用 WebDriver Actions
API 部分中的调用相同的工作
interact {
keyDown Keys.SHIFT
click $("li.clicky")
keyUp Keys.SHIFT
}
通常不需要,但可以直接访问支持 InteractDelegate
的 Actions
实例
interact {
keyDown Keys.SHIFT
actions.click($("li.clicky").singleElement())
keyUp Keys.SHIFT
}
有关可用于模拟用户手势的可用方法的完整列表,请参阅 InteractDelegate
类的文档。
4.15.3. 交互示例
对 interact()
的调用可用于执行比点击按钮和链接或在输入字段中键入更复杂的行为。
拖放
clickAndHold()
、moveByOffset()
,然后 release()
将在页面上拖放一个元素。
interact {
clickAndHold($('#draggable'))
moveByOffset(150, 200)
release()
}
拖放也可以通过 Actions API 的 dragAndDropBy()
便利方法实现
interact {
dragAndDropBy($("#draggable"), 150, 200)
}
在这个特定示例中,元素将被点击,然后向右拖动 150 像素,向下拖动 200 像素,然后释放。
HTMLUnit 驱动程序目前不支持鼠标移动到任意位置,但支持直接移动到元素。 |
按住 Ctrl 键点击
像列表中的项目那样,按住 Ctrl 键单击多个元素,其执行方式与按住 Shift 键单击相同。
import org.openqa.selenium.Keys
interact {
keyDown(Keys.CONTROL)
click($("ul.multiselect li", text: "Order 1"))
click($("ul.multiselect li", text: "Order 2"))
click($("ul.multiselect li", text: "Order 3"))
keyUp(Keys.CONTROL)
}
5. 页面
在阅读本章之前,请确保您已阅读关于 |
5.1. 页面对象模式
Browser.drive {
go "search"
$("input[name='q']").value "Chuck Norris"
$("input[value='Search']").click()
assert $("li", 0).text().contains("Chuck")
}
这是有效的 Geb 代码,它对于一次性脚本运行良好,但这种方法存在两个大问题。想象一下,您有许多涉及搜索和检查结果的测试。搜索和查找结果的实现将在每个测试中重复,也许每个测试中会重复多次。一旦搜索字段的名称这样微不足道的东西发生变化,您就必须更新大量代码。页面对象模式允许我们将模块化、重用和封装的相同原则应用于编程的其他方面,以避免浏览器自动化代码中的此类问题。
这是相同的脚本,使用了页面对象……
import geb.Page
class SearchPage extends Page {
static url = "search"
static at = { title == "Search engine" }
static content = {
searchField { $("input[name=q]") }
searchButton(to: ResultsPage) { $("input[value='Search']") }
}
void search(String searchTerm) {
searchField.value searchTerm
searchButton.click()
}
}
class ResultsPage extends Page {
static at = { title == "Results" }
static content = {
results { $("li") }
result { index -> results[index] }
}
}
Browser.drive {
to SearchPage
search "Chuck Norris"
assert result(0).text().contains("Chuck")
}
您现在已经以可重用的方式封装了关于每个页面以及如何与它交互的信息。正如任何尝试过的人都知道的,维护一个大型的功能性 Web 测试套件以应对不断变化的应用程序可能成为一个昂贵且令人沮丧的过程。Geb 对页面对象模式的支持解决了这个问题。
5.2. Page
超类
所有页面对象必须继承自 Page
。
5.3. 内容 DSL
Geb 提供了一个 DSL,用于以模板化方式定义页面内容,这使得页面定义非常简洁灵活。页面定义了一个名为 content
的 static
闭包属性,用于描述页面内容。
考虑以下 HTML…
<div id="a">a</div>
我们可以这样定义此内容……
class PageWithDiv extends Page {
static content = {
theDiv { $('div', id: 'a') }
}
}
内容 DSL 的结构是……
«name» { «definition» }
其中 «definition»
是针对页面实例进行评估的 Groovy 代码。
下面是它的用法……
Browser.drive {
to PageWithDiv
assert theDiv.text() == "a"
}
那么这是如何工作的呢?首先,请记住 Browser
实例将其不了解的任何方法调用或属性访问委托给当前页面实例。因此,上面的代码与以下内容相同……
Browser.drive {
to PageWithDiv
assert page.theDiv.text() == "a"
}
其次,定义的内容作为页面实例的属性和方法可用……
Browser.drive {
to PageWithDiv
// Following two lines are equivalent
assert theDiv.text() == "a"
assert theDiv().text() == "a"
}
内容 DSL 实际上定义了内容模板。最好的说明方式是通过示例……
class TemplatedPageWithDiv extends Page {
static content = {
theDiv { id -> $('div', id: id) }
}
}
Browser.drive {
to TemplatedPageWithDiv
assert theDiv("a").text() == "a"
}
对内容模板可以传递的参数没有任何限制。
内容模板可以返回任何东西。通常它们会通过使用 $()
函数返回一个 Navigator
对象,但它可以是任何东西。
class PageWithStringContent extends Page {
static content = {
theDivText { $('div#a').text() }
}
}
Browser.drive {
to PageWithStringContent
assert theDivText == "a"
}
重要的是要认识到 «definition»
代码是针对页面实例进行评估的。这允许以下代码……
class PageWithContentReuse extends Page {
static content = {
theDiv { $("div#a") }
theDivText { theDiv.text() }
}
}
这也不局限于其他内容……
class PageWithContentUsingAField extends Page {
static content = {
theDiv { $('div', id: divId) }
}
def divId = "a"
}
或者……
class PageWithContentUsingAMethod extends Page {
static content = {
theDiv { $('div', id: divId()) }
}
def divId() {
"a"
}
}
5.3.1. 模板选项
模板定义可以采用不同的选项。语法是……
«name»(«options map») { «definition» }
例如…
theDiv(cache: false, required: false) { $("div", id: "a") }
以下是可用选项。
必需
默认值:true
required
选项控制定义返回的内容是否必须存在。这仅在定义返回 Navigator
对象(通过 $()
函数)或 null
时相关,如果定义返回其他任何内容,则忽略此选项。
如果 required
选项设置为 true
且返回的内容不存在,则会抛出 RequiredPageContentNotPresent
异常。
给定一个完全空的 HTML 文档,以下代码将通过……
class PageWithTemplatesUsingRequiredOption extends Page {
static content = {
requiredDiv { $("div", id: "b") }
notRequiredDiv(required: false) { $("div", id: "b") }
}
}
to PageWithTemplatesUsingRequiredOption
assert !notRequiredDiv
def thrown = false
try {
page.requiredDiv
} catch (RequiredPageContentNotPresent e) {
thrown = true
}
assert thrown
最小值
默认值:1
min
选项允许指定定义返回的 Navigator
对象应包含的最小元素数量。如果返回的元素数量低于指定值,则会抛出 ContentCountOutOfBoundsException
。
该值应为非负整数。此选项不能与times
选项一起使用。
给定以下 HTML……
<html>
<body>
<p>first paragraph</p>
<p>second paragraph</p>
<body>
</html>
访问以下内容定义……
atLeastThreeElementNavigator(min: 3) { $('p') }
将导致 ContentCountOutOfBoundsException
和以下异常消息
"Page content 'pages.PageWithTemplateUsingMinOption -> atLeastThreeElementNavigator: geb.navigator.DefaultNavigator' should return a navigator with at least 3 elements but has returned a navigator with 2 elements"
最大值
默认值:Integer.MAX_INT
max
选项允许指定定义返回的 Navigator
对象应包含的最大元素数量。如果返回的元素数量高于指定值,则会抛出 ContentCountOutOfBoundsException
。
该值应为非负整数。此选项不能与times
选项一起使用。
给定以下 HTML……
<html>
<body>
<p>first paragraph</p>
<p>second paragraph</p>
<p>third paragraph</p>
<p>fourth paragraph</p>
<body>
</html>
访问以下内容定义……
atMostThreeElementNavigator(max: 3) { $('p') }
将导致 ContentCountOutOfBoundsException
和以下异常消息
"Page content 'pages.PageWithTemplateUsingMaxOption -> atMostThreeElementNavigator: geb.navigator.DefaultNavigator' should return a navigator with at most 3 elements but has returned a navigator with 4 elements"
次数
默认值:null
一个辅助选项,允许在一个选项中同时指定min
选项和max
选项。如果返回的元素数量超出指定范围,则会抛出 ContentCountOutOfBoundsException
。
该值应为非负整数(当期望最小和最大元素数量相等时)或整数范围。此选项不能与 min
和 max
选项一起使用。
给定以下 HTML……
<html>
<body>
<p>first paragraph</p>
<p>second paragraph</p>
<p>third paragraph</p>
<p>fourth paragraph</p>
<body>
</html>
访问以下内容定义……
twoToThreeElementNavigator(times: 2..3) { $('p') }
将导致 ContentCountOutOfBoundsException
和以下异常消息
"Page content 'pages.PageWithTemplateUsingTimesOption -> twoToThreeElementNavigator: geb.navigator.DefaultNavigator' should return a navigator with at most 3 elements but has returned a navigator with 4 elements"
缓存
默认值:false
cache
选项控制每次请求内容时是否评估定义(内容将针对每组唯一的参数进行缓存)。
class PageWithTemplateUsingCacheOption extends Page {
static content = {
notCachedValue { value }
cachedValue(cache: true) { value }
}
def value = 1
}
to PageWithTemplateUsingCacheOption
assert notCachedValue == 1
assert cachedValue == 1
value = 2
assert notCachedValue == 2
assert cachedValue == 1
缓存是一种性能优化,默认情况下禁用。如果您发现某个特定的内容定义解析时间过长,您可能希望启用它。
到
默认值:null
to
选项允许定义如果点击内容,浏览器将被发送到哪个页面。
class PageWithTemplateUsingToOption extends Page {
static content = {
helpLink(to: HelpPage) { $("a", text: "Help") }
}
}
class HelpPage extends Page { }
to PageWithTemplateUsingToOption
helpLink.click()
assert page instanceof HelpPage
to
值将隐式用作内容 click()
方法的参数,有效地设置新页面类型并验证其“at”检查器。请参阅点击内容部分,了解这如何改变浏览器的页面对象。
此选项还支持可以传递给任何Browser.page()
方法变体的所有类型
-
一个页面实例
-
页面类列表
-
页面实例列表
使用列表变体时(此处以页面类显示)……
static content = {
loginButton(to: [LoginSuccessfulPage, LoginFailedPage]) { $("input.loginButton") }
}
然后,点击后,浏览器的页面将设置为列表中第一个“at”检查器通过的页面。这等效于 page(Class<? extends Page>[])
和 page(Page[])
浏览器方法,这些方法在更改页面部分中进行了说明。
在使用 to
选项的任何变体时,传入的所有页面类和页面实例的类都必须定义“at”检查器,否则将抛出 UndefinedAtCheckerException
。
等待
默认值:false
允许值
任何其他值都将被解释为 false
。
wait
选项允许 Geb 等待一段时间以使内容出现在页面上,而不是在请求内容时如果内容不存在则抛出 RequiredPageContentNotPresent
异常。
class DynamicPageWithWaiting extends Page {
static content = {
dynamicallyAdded(wait: true) { $("p.dynamic") }
}
}
to DynamicPageWithWaiting
assert dynamicallyAdded.text() == "I'm here now"
这相当于
class DynamicPageWithoutWaiting extends Page {
static content = {
dynamicallyAdded { $("p.dynamic") }
}
}
to DynamicPageWithoutWaiting
assert waitFor { dynamicallyAdded }.text() == "I'm here now"
有关 waitFor()
方法语义的更多信息,请参阅等待部分,该方法在内部使用。与 waitFor()
一样,如果等待超时,则会抛出 WaitTimeoutException
。
当定义非元素内容(如字符串或数字)时,也可以使用 wait
。Geb 将等待直到内容定义返回一个符合 Groovy Truth 的值。
class DynamicPageWithNonNavigatorWaitingContent extends Page {
static content = {
status { $("p.status") }
success(wait: true) { status.text().contains("Success") }
}
}
to DynamicPageWithNonNavigatorWaitingContent
assert success
在这种情况下,我们固有地等待 status
内容出现在页面上并包含字符串“Success”。如果当我们请求 success
时 status
元素不存在,则会抛出的 RequiredPageContentNotPresent
异常将被吞噬,并且 Geb 将在重试间隔过期后再次尝试。
如果您将 wait
选项设置为 true
并将其与 required
选项设置为 false
一起使用,则可以修改内容的等待行为。给定一个内容定义
static content = {
dynamicallyAdded(wait: true, required: false) { $("p.dynamic") }
}
那么,如果在检索 dynamicallyAdded
时等待超时,将不会抛出 WaitTimeoutException
,并且将返回最后的闭包评估值。如果在闭包评估期间抛出异常,它将被包装在 UnknownWaitForEvaluationResult
实例中并返回。
等待内容块受“隐式断言”的约束。有关更多信息,请参阅隐式断言部分。
waitCondition
默认值:null
waitCondition
选项允许指定一个带有条件的闭包,该条件必须满足才能将模板返回的值视为在等待内容时可用。
作为 waitCondition
选项传入的闭包将在 waitFor()
循环中调用,并将内容定义返回的值作为唯一参数传递给它。如果启用了隐式断言(默认情况下是启用),则闭包中的每个语句都会被隐式断言。如果条件满足,闭包应返回一个真值。
考虑以下示例
static content = {
dynamicallyShown(waitCondition: { it.displayed }) { $("p.dynamic") }
}
访问名为……的内容
dynamicallyShown
……将导致 Geb 等待直到内容不仅在 DOM 中而且也显示。如果在等待超时之前条件未满足,则会抛出 WaitTimeoutException
。
当提供 waitCondition
选项时,等待量可以使用 wait
选项控制,但如果未指定,则默认为 true
,这意味着应用默认等待。
toWait
默认值:false
允许的值与wait
选项相同。
可以与to
选项一起使用,以指定点击内容时执行的页面更改操作是异步的。这本质上意味着页面转换的验证(“at checking”)应包含在 waitFor()
调用中。
class PageWithTemplateUsingToWaitOption extends Page {
static content = {
asyncPageLoadButton(to: AsyncPage, toWait: true) { $("button#load-content") } (1)
}
}
class AsyncPage extends Page {
static at = { $("#async-content") }
}
1 | 页面更改是异步的,例如,涉及 ajax 调用。 |
to PageWithTemplateUsingToWaitOption
asyncPageLoadButton.click()
assert page instanceof AsyncPage
有关 waitFor()
方法语义的更多信息,请参阅等待部分,该方法在内部使用。与 waitFor()
一样,如果等待超时,则会抛出 WaitTimeoutException
。
页面
默认值:null
page
选项允许定义如果内容描述了一个框架并在 withFrame()
调用中使用,浏览器将设置为哪个页面。
给定以下 HTML……
<html>
<body>
<iframe id="frame-id" src="frame.html"></iframe>
<body>
</html>
……以及 frame.html 的内容……
<html>
<body>
<span>frame text</span>
</body>
</html>
……以下代码将通过……
class PageWithFrame extends Page {
static content = {
myFrame(page: FrameDescribingPage) { $('#frame-id') }
}
}
class FrameDescribingPage extends Page {
static content = {
frameContentsText { $('span').text() }
}
}
to PageWithFrame
withFrame(myFrame) {
assert frameContentsText == 'frame text'
}
5.3.2. 别名
如果您希望在不同名称下使用相同的内容定义,则可以创建一个指定 aliases
参数的内容定义
class AliasingPage extends Page {
static content = {
someDiv { $("div#aliased") }
aliasedDiv(aliases: "someDiv")
}
}
to AliasingPage
assert someDiv.@id == aliasedDiv.@id
请记住,别名内容必须在别名内容之前定义,否则您将收到 InvalidPageContent
异常。
5.3.3. 访问内容名称
如果您需要在运行时访问使用 DSL 定义的内容名称,可以使用页面实例的 contentName
属性
class ContentNamesPage extends Page {
static content = {
footer { $("#footer") }
paragraphText { $("p").text() }
}
}
to ContentNamesPage
assert contentNames == ['footer', 'paragraphText'] as Set
5.4. “At” 验证
每个页面都可以定义一种方式来检查底层浏览器是否位于页面类实际表示的页面。这是通过 static
at
闭包完成的……
class PageWithAtChecker extends Page {
static at = { $("h1").text() == "Example" }
}
此闭包可以返回 false
值或抛出 AssertionError
(通过 assert
方法)。verifyAt()
方法调用将
“At 检查器”应保持简单——它们应仅验证预期的页面是否已渲染,而不处理任何页面特定逻辑。 它们只应允许检查浏览器是否例如在订单摘要页面,而不是在产品详细信息页面,甚至更糟糕的是,不在未找到页面。另一方面,它们不应验证与所检查页面关联的逻辑的任何内容,例如其结构或应始终满足的某些谓词。此类检查更适合测试而不是“at 检查器”。这是因为“at 检查器”会被多次评估,而且通常是隐式评估,例如在使用 一个好的经验法则是,保持您的页面“at 检查器”相当相似——它们都应该访问几乎相同的信息,例如页面标题或标题文本,并且只在它们期望的值上有所不同。 |
考虑到上面的例子,你可以这样使用它……
class PageLeadingToPageWithAtChecker extends Page {
static content = {
link { $("a#to-page-with-at-checker") }
}
}
to PageLeadingToPageWithAtChecker
link.click()
page PageWithAtChecker
verifyAt()
verifyAt()
方法由接受页面类或实例的 Browser.at()
方法使用。如果“at”检查器失败,它的行为与 verifyAt()
相同,如果检查器成功,则返回页面实例(这对于希望使用 Geb 编写强类型代码很有用)……
to PageLeadingToPageWithAtChecker
link.click()
at PageWithAtChecker
At 检查器受“隐式断言”的约束。有关更多信息,请参阅隐式断言部分。
如果您不希望在“at”检查失败时收到异常,则有在这种情况下返回 false
的方法:Page#verifyAtSafely()
和 Browser#isAt(Class<? extends Page>)
。
如前所述,当内容模板定义了多个页面的to
选项时,页面的 verifyAt()
方法用于确定使用哪个页面。在这种情况下,由“at”检查器抛出的任何 AssertionError
都会被抑制。
“at”检查器是针对页面实例进行评估的,并且可以访问定义的内容或任何其他变量或方法……
class PageWithAtCheckerUsingContent extends Page {
static at = { heading == "Example" }
static content = {
heading { $("h1").text() }
}
}
如果页面没有“at”检查器,则 verifyAt()
和 at()
方法将抛出 UndefinedAtCheckerException
。如果用作 to
内容模板选项的列表中的任何页面都没有定义“at”检查器,也会发生同样的情况。
在默认情况下,将“at”验证包装在 waitFor()
调用中有时会很有用——众所周知,有些驱动程序在某些情况下在 URL 更改后或在人们认为页面已完全加载之前,会返回控制权。可以使用atCheckWaiting
配置条目来要求这种行为。
意外页面
可以通过unexpectedPages
配置条目提供意外页面列表。
请注意,此功能不适用于 HTTP 响应代码,因为 WebDriver 不会公开这些代码,因此 Geb 无法访问它们。要使用此功能,您的应用程序必须呈现自定义错误页面,这些页面可以建模为页面类并由“at”检查器检测到。 |
如果配置了,则在对任何页面执行“at”检查时,将首先检查指定为 unexpectedPages
配置条目的列表中的类,如果遇到其中任何一个,则会抛出带有适当消息的 UnexpectedPageException
。
假设您的应用程序在找不到页面时呈现一个自定义错误页面,其中包含“抱歉,我们找不到该页面”之类的文本,您可以使用以下类对该页面进行建模
class PageNotFoundPage extends Page {
static at = { $("#error-message").text() == "Sorry but we could not find that page" }
}
然后将该页面注册到配置中
unexpectedPages = [PageNotFoundPage]
当检查浏览器是否在一个页面上,同时满足 PageNotFoundPage
的“at”检查器时,将引发 UnexpectedPageException
。
try {
at ExpectedPage
assert false //should not get here
} catch (UnexpectedPageException e) {
assert e.message.startsWith("An unexpected page ${PageNotFoundPage.name} was encountered")
}
无论何时执行“at”检查,即使是隐式执行,例如使用 to
内容模板选项或将一个或多个页面类传递给 Navigator.click()
方法时,都会检查意外页面。
可以显式检查浏览器是否在意外页面。如果 PageNotFoundPage
的“at”检查成功,以下代码将通过,而不会抛出 UnexpectedPageException
at PageNotFoundPage
当遇到意外页面时,还可以丰富抛出的 UnexpectedPageException
的消息。这可以通过为意外页面实现 geb.UnexpectedPage
来实现
class PageNotFoundPageWithCustomMessage extends Page implements UnexpectedPage {
static at = { $("#error-message").text() == "Sorry but we could not find that page" }
@Override
String getUnexpectedPageMessage() {
"Additional UnexpectedPageException message text"
}
}
try {
at ExpectedPage
assert false //should not get here
} catch (UnexpectedPageException e) {
assert e.message.contains("Additional UnexpectedPageException message text")
}
全局 |
5.5. 页面 URL
页面可以通过 static
url
属性定义 URL。
class PageWithUrl extends Page {
static url = "example"
}
当使用浏览器 to()
方法时,将使用该 URL。
to PageWithUrl
assert currentUrl.endsWith("example")
有关 URL 和斜杠的注意事项,请参阅基本 URL 部分。
5.5.1. URL 片段
页面还可以通过 static
fragment
属性定义 URL 片段标识符(URL 末尾 #
字符之后的部分)。
分配的值可以是 String
,它将原样使用,也可以是 Map
,它将被转换为 application/x-www-form-urlencoded
String
。后者在处理通过表单编码将状态存储在片段标识符中的单页应用程序时特别有用。
无需转义用于 URL 片段的任何字符串,因为所有必要的转义都由 Geb 执行。 |
考虑以下页面,它在此类单页应用程序场景中定义了一个 URL 片段
class PageWithFragment extends Page {
static fragment = [firstKey: "firstValue", secondKey: "secondValue"]
}
然后在使用浏览器 to()
方法时使用该片段。
to PageWithFragment
assert currentUrl.endsWith("#firstKey=firstValue&secondKey=secondValue")
您也可以使用动态片段——您可以在高级页面导航章节的URL 片段小节中了解如何实现。
5.5.2. 页面级别的 atCheckWaiting
配置
特定页面的“at”检查器可以配置为默认情况下隐式包装在 waitFor()
调用中。这可以通过 static
atCheckWaiting
属性设置。
class PageWithAtCheckWaiting extends Page {
static atCheckWaiting = true
}
atCheckWaiting
选项的可能值与内容模板定义的 wait
选项相同。
在页面级别配置的 atCheckWaiting
值优先于配置中指定的全局值。
5.6. 高级页面导航
页面类在使用浏览器 to()
方法时可以自定义它们如何生成 URL。
考虑以下示例……
class PageObjectsPage extends Page {
static url = "pages"
}
Browser.drive(baseUrl: "https://gebish.org/") {
to PageObjectsPage
assert currentUrl == "https://gebish.org/pages"
}
to()
方法也可以接受参数……
class ManualsPage extends Page {
static url = "manual"
}
Browser.drive(baseUrl: "https://gebish.org/") {
to ManualsPage, "0.9.3", "index.html"
assert currentUrl == "https://gebish.org/manual/0.9.3/index.html"
}
传递给 to()
方法的任何参数(在页面类之后)都会通过调用每个参数的 toString()
并用“/
”连接它们来转换为 URL 路径。
然而,这是可扩展的。您可以指定一组参数如何转换为要添加到页面 URL 的 URL 路径。这通过覆盖 convertToPath()
方法来完成。Page
的此方法的实现如下……
String convertToPath(Object[] args) {
args ? '/' + args*.toString().join('/') : ""
}
您可以覆盖此通用方法以控制所有调用的路径转换,或为特定类型签名提供重载版本。考虑以下内容……
class Manual {
String version
}
class ManualsPage extends Page {
static url = "manual"
String convertToPath(Manual manual) {
"/${manual.version}/index.html"
}
}
def someManualVersion = new Manual(version: "0.9.3")
Browser.drive(baseUrl: "https://gebish.org/") {
to ManualsPage, someManualVersion
assert currentUrl == "https://gebish.org/manual/0.9.3/index.html"
}
5.6.1. 命名参数
to()
方法可以使用任何类型的参数,除了命名参数(即 Map
)。命名参数总是被解释为查询参数,并且它们从不使用上述示例中的类发送……
def someManualVersion = new Manual(version: "0.9.3")
Browser.drive(baseUrl: "https://gebish.org/") {
to ManualsPage, someManualVersion, flag: true
assert currentUrl == "https://gebish.org/manual/0.9.3/index.html?flag=true"
}
5.6.2. URL 片段
当调用 to()
时,UrlFragment
的实例可以作为页面类或实例之后的参数传递,以动态控制 URL 的片段标识符部分。UrlFragment
类带有两个静态工厂方法:一个用于从显式 String
创建片段,另一个用于从 Map
创建片段,然后将其进行表单编码。
以下显示了一个使用上述示例中的类进行操作的示例……
Browser.drive(baseUrl: "https://gebish.org/") {
to ManualsPage, UrlFragment.of("advanced-page-navigation"), "0.9.3", "index.html"
assert currentUrl == "https://gebish.org/manual/0.9.3/index.html#advanced-page-navigation"
}
如果您正在使用参数化页面并且希望片段动态确定,例如根据页面属性,那么您可以覆盖 getPageFragment()
方法
class ParameterizedManualsPage extends Page {
String version
String section
@Override
String convertToPath(Object[] args) {
"manual/$version/index.html"
}
@Override
UrlFragment getPageFragment() {
UrlFragment.of(section)
}
}
Browser.drive(baseUrl: "https://gebish.org/") {
to new ParameterizedManualsPage(version: "0.9.3", section: "advanced-page-navigation")
assert currentUrl == "https://gebish.org/manual/0.9.3/index.html#advanced-page-navigation"
}
5.7. 参数化页面
class BooksPage extends Page {
static content = {
book { bookTitle -> $("a", text: bookTitle) }
}
}
class BookPage extends Page {
static at = { forBook == bookTitle }
static content = {
bookTitle { $("h1").text() }
}
String forBook
}
Browser.drive {
to BooksPage
book("The Book of Geb").click()
at(new BookPage(forBook: "The Book of Geb"))
}
手动实例化的页面在使用前必须进行初始化。初始化是上述 |
5.8. 继承
页面可以按继承层次结构排列。内容定义合并……
class BasePage extends Page {
static content = {
heading { $("h1") }
}
}
class SpecializedPage extends BasePage {
static content = {
footer { $("div.footer") }
}
}
Browser.drive {
to SpecializedPage
assert heading.text() == "Specialized page"
assert footer.text() == "This is the footer"
}
如果子类定义了一个与超类中定义的内容模板同名的内容模板,则子类版本将替换超类中的版本。
5.9. 生命周期钩子
页面类可以选择性地实现当页面设置为浏览器的当前页面时以及当它被另一个页面替换时调用的方法。这可用于在页面之间传输状态。
5.9.1. onLoad(Page previousPage)
当页面成为浏览器的新页面对象时,onLoad()
方法将使用上一个页面对象实例调用。
class FirstPage extends Page {
}
class SecondPage extends Page {
String previousPageName
void onLoad(Page previousPage) {
previousPageName = previousPage.class.simpleName
}
}
Browser.drive {
to FirstPage
to SecondPage
assert page.previousPageName == "FirstPage"
}
5.9.2. onUnload(Page newPage)
当页面作为浏览器的页面对象被替换时,onUnload()
方法将与下一个页面对象实例一起调用。
class FirstPage extends Page {
String newPageName
void onUnload(Page newPage) {
newPageName = newPage.class.simpleName
}
}
class SecondPage extends Page {
}
Browser.drive {
def firstPage = to FirstPage
to SecondPage
assert firstPage.newPageName == "SecondPage"
}
5.9.3. 监听页面事件
可以注册一个监听器,每次在切换和检查页面时发生某些事件时都会收到通知。有关可监听事件的最佳参考是PageEventListener
接口的文档。如果您只希望监听页面事件的子集,那么 PageEventListenerSupport
可能很有用,因为它提供了 PageEventListener
所有方法的默认空实现。
使用页面事件监听器的一个用例是,在每次“at”检查失败时写入报告,从而增强报告功能。
以下示例展示了如何将页面事件监听器注册为配置脚本的一部分,该脚本在“at”检查失败时简单地打印页面标题和当前 URL……
import geb.Browser
import geb.Page
import geb.PageEventListenerSupport
pageEventListener = new PageEventListenerSupport() {
void onAtCheckFailure(Browser browser, Page page) {
println "At check failed for page titled '${browser.title}' at url ${browser.currentUrl}"
}
}
5.10. 处理框架
框架似乎是过去的事情,但如果您正在使用 Geb 访问或测试一些遗留应用程序,您可能仍然需要处理它们。值得庆幸的是,Geb 通过 Browser
、Page
和 Module
实例上可用的 withFrame()
方法,使处理它们变得更加方便。
5.10.1. 在框架上下文中执行代码
withFrame()
方法有多种变体,但对于所有变体,最后一个闭包参数都在由第一个参数指定的框架上下文中执行。闭包参数返回的值从方法返回,并且执行后页面恢复到调用前的状态。各种 withFrame()
方法如下:
-
withFrame(String, Closure)
-String
参数包含框架元素的名称或 id -
withFrame(int, Closure)
-int
参数包含框架元素的索引,也就是说,如果一个页面有三个框架,第一个框架的索引是0
,第二个是1
,第三个是2
-
withFrame(Navigator, Closure)
-Navigator
参数应包含一个框架元素 -
withFrame(SimplePageContent, Closure)
-SimplePageContent
,这是内容模板返回的类型,应包含一个框架元素
给定以下 HTML……
<html>
<body>
<iframe name="header" src="frame.html"></iframe>
<iframe id="footer" src="frame.html"></iframe>
<iframe id="inline" src="frame.html"></iframe>
<span>main</span>
<body>
</html>
……frame.html 的代码……
<html>
<body>
<span>frame text</span>
</body>
</html>
……和一个页面类……
class PageWithFrames extends Page {
static content = {
footerFrame { $('#footer') }
}
}
……那么这段代码将会通过……
to PageWithFrames
withFrame('header') { assert $('span').text() == 'frame text' }
withFrame('footer') { assert $('span').text() == 'frame text' }
withFrame(0) { assert $('span').text() == 'frame text' }
withFrame($('#footer')) { assert $('span').text() == 'frame text' }
withFrame(footerFrame) { assert $('span').text() == 'frame text' }
assert $('span').text() == 'main'
如果无法为 withFrame()
调用的给定第一个参数找到框架,则会抛出 NoSuchFrameException
。
5.10.2. 同时切换页面和框架
所有上述 withFrame()
变体还接受一个可选的第二个参数(页面类或页面实例),它允许为作为最后一个参数传递的闭包的执行切换页面。如果使用的页面指定了“at”检查器,它将在将上下文切换到框架后进行验证。
给定前一个示例中的 HTML 和页面类,以下是使用页面类的示例用法
class PageDescribingFrame extends Page {
static content = {
text { $("span").text() }
}
}
to PageWithFrames
withFrame('header', PageDescribingFrame) {
assert page instanceof PageDescribingFrame
assert text == "frame text"
}
assert page instanceof PageWithFrames
以下是使用页面实例的示例用法
class ParameterizedPageDescribingFrame extends Page {
static at = { text == expectedFrameText }
static content = {
text { $("span").text() }
}
String expectedFrameText
}
to PageWithFrames
withFrame('header', new ParameterizedPageDescribingFrame(expectedFrameText: "frame text")) {
assert page instanceof ParameterizedPageDescribingFrame
}
assert page instanceof PageWithFrames
还可以指定要切换到描述框架的页面内容的页面。
6. 模块
模块是可重用的内容定义,可以在多个页面中重复使用。它们对于建模在多个页面中使用的 UI 小部件,甚至在页面中定义更复杂的 UI 元素很有用。
它们的定义方式与页面类似,但扩展了 Module
……
class FormModule extends Module {
static content = {
button { $("input", type: "button") }
}
}
页面可以使用以下语法“包含”模块……
class ModulePage extends Page {
static content = {
form { module FormModule }
}
}
module
方法返回一个模块类的实例,然后可以按以下方式使用……
Browser.drive {
to ModulePage
form.button.click()
}
模块也可以参数化……
class ParameterizedModule extends Module {
static content = {
button {
$("form", id: formId).find("input", type: "button")
}
}
String formId
}
参数传递给模块的构造函数……
class ParameterizedModulePage extends Page {
static content = {
form { id -> module(new ParameterizedModule(formId: id)) }
}
}
Browser.drive {
to ParameterizedModulePage
form("personal-data").button.click()
}
模块也可以包含其他模块……
class OuterModule extends Module {
static content = {
form { module FormModule }
}
}
class OuterModulePage extends Page {
static content = {
outerModule { module OuterModule }
}
}
Browser.drive {
to OuterModulePage
outerModule.form.button.click()
}
6.1. 基础和上下文
模块可以本地化到它们所使用的页面中的特定部分,或者它们可以在其定义中指定一个绝对上下文。模块的基础/上下文可以通过两种方式定义。
模块可以基于 Navigator
实例……
class FormModule extends Module {
static content = {
button { $("input", type: "button") }
}
}
class PageDefiningModuleWithBase extends Page {
static content = {
form { $("form").module(FormModule) }
}
}
Browser.drive {
to PageDefiningModuleWithBase
form.button.click()
}
它也可以在内容定义之外完成……
Browser.drive {
go "/"
$("form").module(FormModule).button.click()
}
我们可以使用上述语法在包含模块时定义 Navigator
上下文。这意味着模块中发生的所有 Navigator
(例如 $()
)方法调用都将针对给定的上下文(在此示例中为 form
元素)。
然而,模块类也可以定义自己的基类……
class FormModuleWithBase extends FormModule {
static base = { $("form") }
}
class PageUsingModuleWithBase extends Page {
static content = {
form { module FormModuleWithBase }
}
}
Browser.drive {
to PageUsingModuleWithBase
form.button.click()
}
基于 Navigator
的模块和在模块中定义的基础可以结合使用。考虑以下 HTML……
<html>
<div class="a">
<form>
<input name="thing" value="a"/>
</form>
</div>
<div class="b">
<form>
<input name="thing" value="b"/>
</form>
</div>
</html>
以及以下内容定义……
class ThingModule extends Module {
static base = { $("form") }
static content = {
thingValue { thing().value() }
}
}
class ThingsPage extends Page {
static content = {
formA { $("div.a").module(ThingModule) }
formB { $("div.b").module(ThingModule) }
}
}
然后它们可以按以下方式使用……
Browser.drive {
to ThingsPage
assert formA.thingValue == "a"
assert formB.thingValue == "b"
}
如果模块声明了一个基准,它总是相对于初始化语句中使用的 Navigator
计算。如果初始化语句不使用 Navigator
,则模块的基准是相对于文档根目录计算的。
6.2. Module
是 Navigator
模块总是与一个基础导航器关联(如果您根本不为模块指定基础,那么它将被分配文档的根元素作为基础),因此自然会将其视为导航器。记住 Module
实现了 Navigator
并考虑以下 HTML……
<html>
<form method="post" action="login">
<input name="login" type="text"></input>
<input name="password" type="password"></input>
<input type="submit" value="Login"></input>
</from>
</html>
以及这些内容定义……
class LoginFormModule extends Module {
static base = { $("form") }
}
class LoginPage extends Page {
static content = {
form { module LoginFormModule }
}
}
以下代码将通过……
Browser.drive {
to LoginPage
assert form.@method == "post"
assert form.displayed
}
也可以在模块实现中使用 Navigator
方法……
class LoginFormModule extends Module {
String getAction() {
getAttribute("action")
}
}
Browser.drive {
to LoginPage
assert form.action == "login"
}
6.3. 在不同页面间重用模块
如前所述,模块可用于对跨多个页面重复使用的页面片段进行建模。例如,应用程序中许多不同类型的页面可能会显示有关用户购物车的信息。您可以使用模块来处理这个问题……
class CartInfoModule extends Module {
static content = {
section { $("div.cart-info") }
itemCount { section.find("span.item-count").text().toInteger() }
totalCost { section.find("span.total-cost").text().toBigDecimal() }
}
}
class HomePage extends Page {
static content = {
cartInfo { module CartInfoModule }
}
}
class OtherPage extends Page {
static content = {
cartInfo { module CartInfoModule }
}
}
模块在这方面表现良好。
6.4. 使用模块处理重复内容
除了在不同页面上重复的内容(如上面提到的购物车),页面本身也可以有重复的内容。在结账页面上,购物车的內容可以总结为产品名称、数量和每个产品的价格。对于这种类型的页面,可以使用 Navigator
的 moduleList()
方法收集模块列表。
考虑以下关于我们购物车内容的 HTML
<html>
<table>
<tr>
<th>Product</th><th>Quantity</th><th>Price</th>
</tr>
<tr>
<td>The Book Of Geb</td><td>1</td><td>5.99</td>
</tr>
<tr>
<td>Geb Single-User License</td><td>1</td><td>99.99</td>
</tr>
<tr>
<td>Geb Multi-User License</td><td>1</td><td>199.99</td>
</tr>
</table>
</html>
我们可以这样建模表格的一行
class CartRow extends Module {
static content = {
cell { $("td", it) }
productName { cell(0).text() }
quantity { cell(1).text().toInteger() }
price { cell(2).text().toBigDecimal() }
}
}
并在我们的页面中定义一个 CartRows 列表
class CheckoutPage extends Page {
static content = {
cartItems {
$("table tr").tail().moduleList(CartRow) // tailing to skip the header row
}
}
}
因为 cartItems
的返回值是 CartRow 实例的列表,所以我们可以使用任何常用的集合方法
assert cartItems.every { it.price > 0.0 }
我们还可以使用下标运算符以及索引或索引范围来访问购物车项目
assert cartItems[0].productName == "The Book Of Geb"
assert cartItems[1..2]*.productName == ["Geb Single-User License", "Geb Multi-User License"]
请记住,您可以使用参数化模块实例来创建重复内容的模块列表
class ParameterizedCartRow extends Module {
static content = {
cell { $("td", it) }
productName { cell(nameIndex).text() }
quantity { cell(quantityIndex).text().toInteger() }
price { cell(priceIndex).text().toBigDecimal() }
}
def nameIndex
def quantityIndex
def priceIndex
}
class CheckoutPageWithParametrizedCart extends Page {
static content = {
cartItems {
$("table tr").tail().moduleList {
new ParameterizedCartRow(nameIndex: 0, quantityIndex: 1, priceIndex: 2)
}
}
}
}
您可能想知道为什么允许使用参数化模块实例的 |
6.5. 内容 DSL
用于模块的内容 DSL 与用于页面的内容 DSL 完全相同,因此可以使用所有相同的选项和技术。
6.7. 表单控件模块
如果您以强类型方式使用 Geb,您可能会考虑使用提供的建模表单控件的模块,而不是直接使用 Navigator
API 操作它们。这将导致更长的内容定义,但使用它们会更容易,因为您不必记住 value()
调用对于不同类型的控件的含义。例如,在操作复选框时就是这种情况,通过 Navigator
API 与它们交互时,通过将布尔值传递给 value()
来实现选中和取消选中。
所有这些模块(除了 |
6.7.1. FormElement
FormElement
是所有建模表单控件的模块(除了RadioButtons
)的基类,并提供用于检查控件是否禁用或只读的快捷属性方法。您通常会在特定控件类型的模块类上调用这些方法,很少直接使用此模块。
给定 HTML……
<html>
<body>
<input disabled="disabled" name="disabled"/>
<input readonly="readonly" name="readonly"/>
</body>
</html>
以下是使用提供的快捷属性方法的示例……
assert $(name: "disabled").module(FormElement).disabled
assert !$(name: "disabled").module(FormElement).enabled
assert $(name: "readonly").module(FormElement).readOnly
assert !$(name: "readonly").module(FormElement).editable
6.7.2. Checkbox
Checkbox
模块提供了用于选中和取消选中复选框的实用方法,以及用于检索其状态的属性方法。
给定 HTML……
<html>
<body>
<input type="checkbox" name="flag"/>
</body>
</html>
它可以这样使用……
def checkbox = $(name: "flag").module(Checkbox)
assert !checkbox.checked
assert checkbox.unchecked
checkbox.check()
assert checkbox.checked
checkbox.uncheck()
assert checkbox.unchecked
6.7.3. Select
Select
模块提供了用于选择选项以及检索所选选项的值和单选选择元素的文本的属性方法。
给定 HTML……
<html>
<body>
<select name="artist">
<option value="1">Ima Robot</option>
<option value="2">Edward Sharpe and the Magnetic Zeros</option>
<option value="3">Alexander</option>
</select>
</body>
</html>
它可以这样使用……
def select = $(name: "artist").module(Select)
select.selected = "2"
assert select.selected == "2"
assert select.selectedText == "Edward Sharpe and the Magnetic Zeros"
select.selected = "Alexander"
assert select.selected == "3"
assert select.selectedText == "Alexander"
6.7.4. MultipleSelect
MultipleSelect
模块提供了用于选择选项以及检索所选选项值和多选选择元素文本的属性方法。这些方法接受并返回字符串列表。
给定 HTML……
<html>
<body>
<select name="genres" multiple>
<option value="1">Alt folk</option>
<option value="2">Chiptunes</option>
<option value="3">Electroclash</option>
<option value="4">G-Funk</option>
<option value="5">Hair metal</option>
</select>
</body>
</html>
它可以这样使用……
def multipleSelect = $(name: "genres").module(MultipleSelect)
multipleSelect.selected = ["2", "3"]
assert multipleSelect.selected == ["2", "3"]
assert multipleSelect.selectedText == ["Chiptunes", "Electroclash"]
multipleSelect.selected = ["G-Funk", "Hair metal"]
assert multipleSelect.selected == ["4", "5"]
assert multipleSelect.selectedText == ["G-Funk", "Hair metal"]
6.7.5. TextInput
TextInput
模块提供了用于设置和检索输入文本元素文本的属性方法。
给定 HTML……
<html>
<body>
<input type="text" name="language"/>
</body>
</html>
它可以这样使用……
def input = $(name: "language").module(TextInput)
input.text = "Groovy"
assert input.text == "Groovy"
6.7.6. Textarea
Textarea
模块提供了用于设置和检索文本区域元素文本的属性方法。
给定 HTML……
<html>
<body>
<textarea name="language"/>
</body>
</html>
它可以这样使用……
def textarea = $(name: "language").module(Textarea)
textarea.text = "Groovy"
assert textarea.text == "Groovy"
6.7.7. FileInput
FileInput
模块为文件输入元素的文件位置提供了一个设置器。该方法接受一个 File
实例。
给定 HTML……
<html>
<body>
<input type="file" name="csv"/>
</body>
</html>
它可以这样使用……
def csvFile = new File("data.csv")
def input = $(name: "csv").module(FileInput)
input.file = csvFile
6.7.8. RadioButtons
RadioButtons
模块提供了用于选中单选按钮以及检索选中按钮的值和与其关联的标签文本的属性方法。
给定 HTML……
<html>
<body>
<label for="site-current">Search this site</label>
<input type="radio" id="site-current" name="site" value="current">
<label for="site-google">Search Google
<input type="radio" id="site-google" name="site" value="google">
</label>
</body>
</html>
它可以这样使用……
def radios = $(name: "site").module(RadioButtons)
radios.checked = "current"
assert radios.checked == "current"
assert radios.checkedLabel == "Search this site"
radios.checked = "Search Google"
assert radios.checked == "google"
assert radios.checkedLabel == "Search Google"
6.7.9. SearchInput
SearchInput
模块提供了用于设置和检索搜索输入元素文本的属性方法。
给定 HTML……
<html>
<body>
<input type="search" name="language"/>
</body>
</html>
它可以这样使用……
def input = $(name: "language").module(SearchInput)
input.text = "Groovy"
assert input.text == "Groovy"
6.7.10. DateInput
DateInput
模块提供了用于设置和检索日期输入元素日期的属性方法。
给定 HTML……
<html>
<body>
<input type="date" name="release"/>
</body>
</html>
它可以这样使用……
def input = $(name: "release").module(DateInput)
input.date = "2017-11-25"
assert input.date == LocalDate.of(2017, 11, 25)
input.date = LocalDate.of(2017, 11, 26)
assert input.date == LocalDate.parse("2017-11-26")
6.7.11. DateTimeLocalInput
DateTimeLocalInput
模块提供了用于设置和检索 datetime-local 输入元素的日期和时间的属性方法。
给定 HTML……
<html>
<body>
<input type="datetime-local" name="next-meeting"/>
</body>
</html>
它可以这样使用……
def input = $(name: "next-meeting").module(DateTimeLocalInput)
input.dateTime = "2018-12-09T20:16"
assert input.dateTime == LocalDateTime.of(2018, 12, 9, 20, 16)
input.dateTime = LocalDateTime.of(2018, 12, 31, 0, 0)
assert input.dateTime == LocalDateTime.parse("2018-12-31T00:00")
6.7.12. TimeInput
TimeInput
模块提供了用于设置和检索时间输入元素时间的属性方法。
给定 HTML……
<html>
<body>
<input type="time" name="start" min="09:00:00" max="17:00:00" step="300" />
</body>
</html>
它可以与 java.time.LocalTime
对象一起使用……
def input = $(name: "start").module(TimeInput)
input.time = LocalTime.of(14, 5)
assert input.time == LocalTime.of(14, 5)
……或与字符串一起使用……
input.time = "15:15"
assert input.time == LocalTime.of(15, 15)
6.7.13. MonthInput
MonthInput
模块提供了用于设置和检索月份输入元素月份的属性方法。
给定 HTML……
<html>
<body>
<input type="month" name="employment-start"/>
</body>
</html>
它可以与 java.time.YearMonth
对象一起使用……
def input = $(name: "employment-start").module(MonthInput)
input.month = YearMonth.of(2018, 12)
assert input.month == YearMonth.of(2018, 12)
……或与字符串一起使用……
input.month = "2019-01"
assert input.month == YearMonth.of(2019, 1)
6.7.14. WeekInput
WeekInput
模块提供了用于设置和检索星期输入元素星期的属性方法。
给定 HTML……
<html>
<body>
<input type="week" name="delivery-week" min="2018-W01" max="2019-W01" step="1" />
</body>
</html>
它可以与 org.threeten.extra.YearWeek
对象一起使用……
def input = $(name: "delivery-week").module(WeekInput)
input.week = YearWeek.of(2018, 5)
assert input.week == YearWeek.of(2018, 5)
……或与字符串一起使用……
input.week = "2018-W52"
assert input.week == YearWeek.of(2018, 52)
6.7.15. EmailInput
EmailInput
模块提供了用于设置和检索电子邮件输入元素文本的属性方法。
给定 HTML……
<html>
<body>
<input type="email" name="address"/>
</body>
</html>
它可以这样使用……
def input = $(name: "address").module(EmailInput)
input.text = "joe@example.com"
assert input.text == "joe@example.com"
6.7.16. TelInput
TelInput
模块提供了用于设置和检索电话输入元素文本的属性方法。
给定 HTML……
<html>
<body>
<input type="tel" name="number"/>
</body>
</html>
它可以这样使用……
def input = $(name: "number").module(TelInput)
input.text = "(541) 754-3010"
assert input.text == "(541) 754-3010"
6.7.17. NumberInput
NumberInput
模块提供了用于设置和检索数字输入元素当前数字值的属性方法。它还提供了检索其 min
、max
和 step
属性值的方法。
给定 HTML……
<html>
<body>
<input type="number" name="amount" min="-2.5" max="2.5" step="0.5"/>
</body>
</html>
它可以这样使用……
def input = $(name: "amount").module(NumberInput)
input.number = 1.5
assert input.number == 1.5
assert input.min == -2.5
assert input.max == 2.5
assert input.step == 0.5
6.7.18. RangeInput
RangeInput
模块提供了用于设置和检索数字输入元素当前数字值的属性方法。它还提供了检索其 min
、max
和 step
属性值的方法。
给定 HTML……
<html>
<body>
<input type="range" name="volume" min="0" max="10" step="0.1"/>
</body>
</html>
它可以这样使用……
def input = $(name: "volume").module(RangeInput)
input.number = 3.5
assert input.number == 3.5
assert input.min == 0
assert input.max == 10
assert input.step == 0.1
6.7.19. UrlInput
UrlInput
模块提供了用于设置和检索 URL 输入元素文本的属性方法。
给定 HTML……
<html>
<body>
<input type="url" name="homepage"/>
</body>
</html>
它可以这样使用……
def input = $(name: "homepage").module(UrlInput)
input.text = "http://gebish.org"
assert input.text == "http://gebish.org"
6.7.20. PasswordInput
PasswordInput
模块提供了用于设置和检索密码输入元素文本的属性方法。
给定 HTML……
<html>
<body>
<input type="password" name="secret"/>
</body>
</html>
它可以这样使用……
def input = $(name: "secret").module(PasswordInput)
input.text = "s3cr3t"
assert input.text == "s3cr3t"
6.7.21. ColorInput
ColorInput
模块提供了用于设置和检索颜色输入元素颜色的属性方法。
给定 HTML……
<html>
<body>
<input type="color" name="favorite"/>
</body>
</html>
它可以与 org.openqa.selenium.support.Color
对象一起使用……
def input = $(name: "favorite").module(ColorInput)
input.color = new Color(0, 255, 0, 1)
assert input.color == new Color(0, 255, 0, 1)
assert input.value() == "#00ff00"
……或与十六进制字符串一起使用……
input.color = "#ff0000"
assert input.value() == "#ff0000"
assert input.color == new Color(255, 0, 0, 1)
6.8. 解封装从 content
DSL 返回的模块
为了更好的错误报告,当前实现将 content
块中声明的任何模块包装到 geb.content.TemplateDerivedPageContent
实例中。
给定如下定义的页面
class ModuleUnwrappingPage extends Page {
static content = {
theModule { module(UnwrappedModule) }
}
}
和自定义模块
class UnwrappedModule extends Module {
static content = {
theContent { $(".the-content") }
}
}
模块分配给其声明类型的变量将失败并抛出 GroovyCastException
Browser.drive {
to ModuleUnwrappingPage
UnwrappedModule foo = theModule (1)
}
1 | 抛出 GroovyCastException |
调用一个将模块作为参数并使用其声明类型的方法将失败并抛出 MissingMethodException
String getContentText(UnwrappedModule module) {
module.theContent.text()
}
Browser.drive {
to ModuleUnwrappingPage
getContentText(theModule) (1)
}
1 | 抛出 MissingMethodException |
由于您可能希望或需要对模块使用强类型,因此有一种方法可以实现。模块可以使用 Groovy as
运算符强制转换为其声明的类型
Browser.drive {
to ModuleUnwrappingPage
UnwrappedModule unwrapped = theModule as UnwrappedModule
getContentText(theModule as UnwrappedModule)
}
请记住,将模块强制转换为其声明的类型意味着模块被解包。这样做会丢失该模块的方便错误消息。 |
取舍是什么?对任何内容元素(包括模块)调用 toString()
会返回有意义的路径,例如
modules.ModuleUnwrappingPage -> theModule: modules.UnwrappedModule -> theContent: geb.navigator.DefaultNavigator
此类路径可以在错误消息中看到,这正是您要为未包装模块放弃的。
7. 配置
Geb 提供了一种配置机制,允许您以灵活的方式控制 Geb 的各个方面。其核心是 Configuration
对象,Browser
和其他对象在运行时查询该对象。
影响配置的通用机制有三种:系统属性、配置脚本和构建适配器。
7.1. 机制
7.1.1. 配置文件
Geb 尝试从默认包(换句话说,在类路径上的目录根目录中)加载一个名为 GebConfig.groovy
的 ConfigSlurper
脚本。如果找不到,Geb 将尝试从默认包加载一个名为 GebConfig
的 ConfigSlurper
类——这在您从 IDE 运行使用 Geb 的测试时很有用,因为您无需将 GebConfig.groovy
指定为资源,Geb 将简单地回退到脚本的编译版本。如果脚本和类都找不到,Geb 将继续使用所有默认值。
首先,使用执行线程的上下文类加载器查找脚本,如果找不到,则使用加载 Geb 的类加载器查找。这涵盖了 99% 的场景,无需任何干预即可完美运行。但是,如果您确实需要配置上下文类加载器来加载配置脚本,您必须确保它与加载 Geb 的类加载器相同或为其子级。如果这两个类加载器都找不到脚本,则将重复该过程,但这次将搜索类——首先使用执行线程的上下文类加载器,然后使用加载 Geb 的类加载器。
环境敏感性
Groovy ConfigSlurper
机制内置了对环境敏感配置的支持,Geb 利用这一点,使用 geb.env
系统属性来确定要使用的环境。有效使用此机制是根据指定的 Geb“环境”配置不同的驱动程序(具体细节将在下面详细说明)。
您如何设置环境系统属性将取决于您使用的构建系统。例如,在使用 Gradle 时,您可以通过在运行测试的测试任务的配置中指定 Geb 环境来控制它……
test {
systemProperty 'geb.env', 'windows'
}
其他构建环境将允许您以不同的方式执行此操作。
7.1.2. 系统属性
某些配置选项可以通过系统属性指定。通常,由系统属性指定的配置选项将覆盖配置脚本中设置的值。请参阅下面的配置选项,了解哪些选项可以通过系统属性控制。
7.1.3. 构建适配器
构建适配器机制的存在是为了允许 Geb 与逻辑上指定配置选项的开发/构建环境集成。
此机制通过系统属性 geb.build.adapter
加载类名(完全限定)来工作,该类名必须实现 BuildAdapter
接口。目前,构建适配器只能影响要使用的基本 URL 和报告目录的位置。
如果 geb.build.adapter
系统属性未明确设置,则默认为 `SystemPropertiesBuildAdapter。正如您可能推断的那样,此默认实现使用系统属性来指定值,因此在大多数情况下都可用。有关它查找的特定系统属性的详细信息,请参阅链接的 API 文档。
虽然默认构建适配器使用系统属性,但它不应被视为与系统属性配置相同,因为配置脚本中的值优先于构建适配器,而系统属性则不然。 |
7.2. 配置选项
7.2.1. 驱动实现
要使用的驱动程序由配置键 driver
或系统属性 geb.driver
指定。
工厂闭包
在配置脚本中,它可以是一个闭包,当不带参数调用时,返回一个 WebDriver
实例……
import org.openqa.selenium.firefox.FirefoxDriver
driver = { new FirefoxDriver() }
这是首选机制,因为它允许对驱动程序创建和配置进行最大程度的控制。
您可以使用 ConfigSlurper
机制的环境敏感性来为每个环境配置不同的驱动程序……
import org.openqa.selenium.htmlunit.HtmlUnitDriver
import org.openqa.selenium.firefox.FirefoxOptions
import org.openqa.selenium.remote.RemoteWebDriver
// default is to use htmlunit
driver = { new HtmlUnitDriver() }
environments {
// when system property 'geb.env' is set to 'remote' use a remote Firefox driver
remote {
driver = {
def remoteWebDriverServerUrl = new URL("http://example.com/webdriverserver")
new RemoteWebDriver(remoteWebDriverServerUrl, new FirefoxOptions())
}
}
}
WebDriver 能够在远程主机上驱动浏览器,这正是我们上面使用的。有关更多信息,请参阅 WebDriver 关于 |
驱动类名
要使用的驱动程序类的名称(它将不带参数构造)可以指定为配置脚本中键 driver
的字符串,或通过 geb.driver
系统属性(该类必须实现 WebDriver
接口)。
driver = "org.openqa.selenium.firefox.FirefoxDriver"
或者可以是以下简称之一:ie
、htmlunit
、firefox
、chrome
或 edge
。这些将被隐式扩展为它们的完全限定类名……
driver = "firefox"
下表列出了可以使用的简称
简称 | 驱动程序 |
---|---|
|
|
|
|
|
|
|
|
|
如果没有指定显式驱动程序,Geb 将按上表中列出的顺序在类路径中查找以下驱动程序。如果找不到这些类中的任何一个,将抛出 UnableToLoadAnyDriversException
。
7.2.2. 导航器工厂
可以通过配置指定您自己的 NavigatorFactory
实现。如果您想扩展 Navigator
类以提供您自己的行为扩展,这将非常有用。
与其注入您自己的 NavigatorFactory
,不如注入一个自定义的 InnerNavigatorFactory
,这是一个更简单的接口。为此,您可以为配置键 innerNavigatorFactory
指定一个闭包……
import geb.Browser
import org.openqa.selenium.WebElement
innerNavigatorFactory = { Browser browser, Iterable<WebElement> elements ->
new MyCustomNavigator(browser, elements)
}
这是一个相当高级的用例。如果您需要这样做,请查看源代码,或者如果需要帮助,请通过邮件列表联系我们。
7.2.3. 驱动器缓存
Geb 缓存驱动程序并将其用于 JVM 生命周期(即隐式驱动程序生命周期)的能力可以通过将 cacheDriver
配置选项设置为 false
来禁用。但是,如果这样做,您将负责在适当的时间退出每个创建的驱动程序。
默认的缓存行为是全局缓存 JVM 中的驱动程序。如果您在多个线程中使用 Geb,这可能不是您想要的,因为 Geb Browser
对象和 WebDriver 核心都不是线程安全的。为了解决这个问题,您可以指示 Geb 将驱动程序实例按线程缓存,方法是将配置选项 cacheDriverPerThread
设置为 true
。
此外,默认情况下,Geb 将注册一个关闭钩子,以便在 JVM 退出时退出所有缓存的浏览器。您可以通过将配置属性 quitCachedDriverOnShutdown
设置为 false
来禁用此功能。
7.2.4. 浏览器重置时退出驱动器
如果出于某种原因,您不希望缓存驱动程序,而是在每次测试后退出并在下一次测试前重新创建,那么 Geb 支持这种驱动程序实例的管理。要在每次测试后退出驱动程序,您可以将 quitDriverOnBrowserReset
配置属性设置为 true
。
quitDriverOnBrowserReset = true
如果驱动程序缓存被禁用,则 |
7.2.5. 基础 URL
要使用的基本 URL 可以通过设置 baseUrl
配置属性(带有 String
)值或通过构建适配器(其默认实现查看 geb.build.baseUrl
系统属性)来指定。配置脚本中设置的任何值都将优先于构建适配器提供的值。
7.2.6. 模板选项默认值
某些内容 DSL 模板选项的默认值是可配置的
templateOptions {
cache = true
wait = true
toWait = true
waitCondition = { it.displayed }
required = false
min = 0
max = 1
}
7.2.7. 等待
默认值
默认值可以通过以下方式指定
waiting {
timeout = 10
retryInterval = 0.5
}
两个值都是可选的,以秒为单位。如果未指定,timeout
的值为 5
,retryInterval
的值为 0.1
。
预设
预设可以通过以下方式指定
waiting {
presets {
slow {
timeout = 20
retryInterval = 1
}
quick {
timeout = 1
}
}
}
这里我们定义了两个预设,slow
和 quick
。请注意,quick
预设没有指定 retryInterval
值;任何缺失的值都将替换为默认值(即,quick
预设的默认 retryInterval
值为 0.1
)。
失败原因
当等待因条件抛出异常而失败时,无论是断言失败还是任何其他异常,该异常都将设置为 Geb 抛出的 WaitTimeoutException
的原因。这通常提供了相当好的诊断结果,说明出了什么问题。不幸的是,某些运行时,即 Maven Surefire Plugin,不会打印完整的异常堆栈跟踪并从其中排除原因。为了在这种情况下更容易诊断,可以配置 Geb 将原因的字符串表示形式包含在 WaitTimeoutException
消息中
waiting {
includeCauseInMessage = true
}
7.2.8. 在 “at” 检查器中等待
At 检查器可以配置为隐式地用 waitFor()
调用包装。这可以通过以下设置
atCheckWaiting = true
atCheckWaiting
属性的可能值与内容模板定义的 wait
选项的值一致。
此全局设置也可以在每个页面类的基础上覆盖。
7.2.9. 对隐式 “at” 检查要求 “at” 检查器
当显式“at”检查页面,即将其传递给 Browser
的 at()
方法,但该方法未定义“at”检查器时,会抛出 UndefinedAtCheckerException
。默认情况下,当执行隐式“at 检查”时,例如使用 Browser 的 to()
方法时,情况并非如此。可以通过将 requirePageAtCheckers
配置属性设置为“truthy”值来更改此行为,以便在执行隐式“at 检查”且页面未定义“at”检查器时也抛出 UndefinedAtCheckerException
requirePageAtCheckers = true
7.2.10. 等待基础导航器
有时 Firefox 驱动程序在尝试查找页面的根 HTML 元素时会超时。这表现为类似于以下内容的错误:
org.openqa.selenium.NoSuchElementException: Unable to locate element: {"method":"tag name","selector":"html"} Command duration or timeout: 576 milliseconds For documentation on this error, please visit: http://seleniumhq.org/exceptions/no_such_element.html
您可以通过配置等待超时来防止此错误发生,以便在驱动程序定位根 HTML 元素时使用,使用
baseNavigatorWaiting = true
baseNavigatorWaiting
选项的可能值与内容模板定义的 wait
选项一致。
7.2.11. 意外页面
unexpectedPages
配置属性允许指定意外的 Page
类列表,这些类将在执行“at”检查时进行检查。假设已经定义了 PageNotFoundPage
和 InternalServerErrorPage
,您可以使用以下内容将它们配置为意外页面
unexpectedPages = [PageNotFoundPage, InternalServerErrorPage]
有关意外页面的更多信息,请参阅此部分。
7.2.13. withNewWindow()
选项的默认值
某些withNewWindow()
调用选项的默认值是可配置的
withNewWindow {
close = false
wait = true
}
7.2.14. 报告器
报告器是负责快照浏览器状态的对象(有关详细信息,请参阅报告章节)。所有报告器都是 Reporter
接口的实现。如果没有明确定义报告器,将从 ScreenshotReporter
(拍摄 PNG 截图)和 PageSourceReporter
(将当前 DOM 状态转储为 HTML)创建一个复合报告器。这是一个合理的默认值,但如果您希望使用自定义报告器,可以将其分配给 reporter
配置键。
reporter = new CustomReporter()
7.2.15. 报告目录
报告目录配置用于控制浏览器应将报告写入何处(有关详细信息,请参阅报告章节)。
在配置脚本中,您可以通过 reportsDir
键设置报告目录的路径……
reportsDir = "target/geb-reports"
该值被解释为路径,如果不是绝对路径,则相对于 JVM 的工作目录。 |
报告目录也可以由构建适配器指定(默认实现会查看geb.build.reportsDir
系统属性)。配置脚本中设置的任何值都将优先于构建适配器提供的值。
还可以将reportsDir
配置项设置为文件。
reportsDir = new File("target/geb-reports")
默认情况下,此值未设置。浏览器的report()
方法需要此配置项的值,因此如果您使用报告功能,则必须设置报告目录。
7.2.16. 仅报告测试失败
默认情况下,Geb会在每个失败的测试方法结束时生成一份报告。如果希望即使没有失败也为每个测试生成报告,则可以将reportOnTestFailureOnly
设置设为false
。
reportOnTestFailureOnly = false
7.2.17. 报告监听器
可以指定一个监听器,当生成报告时会收到通知。有关详细信息,请参阅监听报告部分。
7.2.18. 导航器事件监听器
可以指定一个监听器,当发生某些导航器事件时会收到通知。有关详细信息,请参阅监听导航器事件部分。
7.2.19. 页面事件监听器
可以指定一个监听器,当发生某些页面事件时会收到通知。有关详细信息,请参阅监听页面事件部分。
7.2.20. 自动清除 Cookies
某些集成会自动清除当前域的驱动程序Cookie,这在使用隐式驱动程序时通常是必需的。此配置标志默认为true
,可以通过将配置中的autoClearCookies
值设置为false
来禁用。
autoClearCookies = false
7.2.21. 自动清除 Web 存储
某些集成会自动清除驱动程序的Web存储(即本地存储和会话存储),这在使用隐式驱动程序时通常是必需的。此配置标志默认为false
,可以通过将配置中的autoClearWebStorage
值设置为true
来启用。
autoClearWebStorage = true
7.3. 运行时覆盖
Configuration对象还为它公开的所有配置属性提供了设置器,允许您在特定情况下根据需要运行时覆盖配置属性。
例如,您可能有一个Spock规范需要禁用autoClearCookies
属性。您可以通过以下方式仅为此规范禁用它……
import geb.spock.GebReportingSpec
class FunctionalSpec extends GebReportingSpec {
def setupSpec() {
browser.config.autoClearCookies = false
}
def cleanup() {
browser.config.autoClearCookies = true
}
}
请记住,自Geb 6.0起,所有测试都共享一个 |
8. 隐式断言
自Geb 0.7.0起,Geb的某些部分利用“隐式断言”。此功能的唯一目标是提供更具信息性的错误消息。简单地说,这意味着对于给定的代码块,所有表达式都会自动转换为断言。因此,以下代码
1 == 1
变为……
assert 1 == 1
如果您使用过Spock Framework,那么您将非常熟悉Spock的 |
在Geb中,等待表达式和at表达式会自动使用隐式断言。考虑以下页面对象……
class ImplicitAssertionsExamplePage extends Page {
static at = { title == "Implicit Assertions!" }
def waitForHeading() {
waitFor { $("h1") }
}
}
这会自动变为……
class ImplicitAssertionsExamplePage extends Page {
static at = { assert title == "Implicit Assertions!" }
def waitForHeading() {
waitFor { assert $("h1") }
}
}
因此,当表达式因Groovy的power assertions而失败时,Geb能够提供更好的错误消息。
Geb使用一种特殊形式的assert
,它返回表达式的值,而常规的assert
返回null
。
这意味着给定……
static content = {
headingText(wait: true) { $("h1").text() }
}
在此访问headingText
将等待存在一个h1
并且它有一些文本(因为空字符串在Groovy中是false
),然后将返回该文本。这意味着即使使用隐式断言,该值仍然返回并且可用。
8.1. At 验证
让我们来看看“at checker”的情况。
如果您不熟悉Geb的“at checking”,请阅读此部分。 |
考虑以下小的Geb脚本……
to ImplicitAssertionsExamplePage
at checking的工作方式是验证页面的“at check”是否返回一个真值。如果返回真值,则at()
方法返回true
。否则,at()
方法将返回false
。但是,由于隐式断言,“at check”永远不会返回false
。相反,at checker将抛出AssertionError
。因为页面的“at check”被转换为断言,您将在堆栈跟踪中看到以下内容
Assertion failed: title == "Implicit Assertions!" | | | false 'Something else'
如您所见,这比at()
方法简单地返回false
更具信息性。
带有附加断言的 At 验证
除了接受单个参数的常规at()
方法外,还有接受额外闭包参数的at()
方法。这些方法主要为了更好的IDE支持而引入,但为了有用,它们利用了隐式断言。因为作为最后一个参数传递给at()
的闭包的每个语句都被隐式断言,所以它们可以在Spock规范的then:
和expect:
块中以富有表现力的方式使用,并在隐式断言失败时提供更好的错误消息。您可能熟悉Spock的Specification.with(Object, Closure<?>)
方法,它具有非常相似的特性和目的。
所以给定以下代码……
at(ImplicitAssertionsExamplePage) {
headingText.empty
}
……当闭包中的条件不满足时,您可能会看到类似的错误……
Assertion failed:
headingText.empty
| |
| false
'This is a heading'
……当闭包中的条件不满足时。
8.2. 等待
隐式断言的另一个应用是等待。
如果您不熟悉Geb的“等待”支持,请阅读此部分。 |
考虑以下Geb脚本
waitFor { title == "Page Title" }
waitFor
方法验证给定子句是否在特定时间范围内返回一个真值。由于隐式断言,当此失败时,您将在堆栈跟踪中看到以下内容
Assertion failed: title == "Page Title" | | | false 'Something else'
失败的断言作为geb.waiting.WaitTimeoutException
的原因被携带,并提供了一条信息性消息,说明等待失败的原因。
等待内容
相同的隐式断言语义适用于正在等待的内容定义。
如果您不熟悉Geb的“等待内容”支持,请阅读此部分。 |
任何声明了wait
参数的内容定义,其每个表达式都会像waitFor()
方法调用一样添加隐式断言。此外,任何声明了waitCondition
参数的内容定义,其作为该参数传递的闭包的每个表达式也会添加隐式断言。
等待时重新加载页面
隐式断言的语义同样适用于refreshWaitFor()
方法的使用
如果您不熟悉Geb的“等待时重新加载页面”支持,请阅读此部分。 |
任何对refreshWaitFor()
方法的调用,其传递的块中的每个表达式都会像waitFor()
方法调用一样添加隐式断言。
选择性禁用隐式断言
有时,我们不希望将隐式断言应用于传递给waitFor()
或refreshWaitFor()
的闭包中的所有表达式。一个例子可能是调用方法,如果它们抛出异常则应该使条件失败,但如果它们返回一个假值则不应该使条件失败。要禁用特定waitFor()
或refreshWaitFor()
调用中的隐式断言,只需将false
作为implicitAssertions
命名参数传递即可
waitFor(implicitAssertions: false) {
falseReturningMethod()
true
}
8.3. 工作原理
“隐式断言”功能是作为Groovy编译时转换实现的,它实际上将代码块中的所有表达式转换为断言。
此转换被打包为一个单独的JAR,名为geb-implicit-assertions
。此JAR需要位于您的Geb测试/页面/模块(以及您希望使用隐式断言的任何其他代码)的编译类路径上,此功能才能正常工作。
如果您通过依赖管理系统获取Geb,通常不需要担心,因为它会自动发生。Geb通过Apache Maven格式(即通过POM文件)的Maven中央仓库分发。主Geb模块geb-core
依赖geb-implicit-assertions
模块作为compile
依赖项。
如果您的依赖管理系统继承了传递性编译依赖项(即也将第一类编译依赖项的编译依赖项设为第一类编译依赖项),那么您将自动拥有geb-implicit-assertions
模块作为编译依赖项,一切都会正常工作(Maven、Gradle和Ivy的大多数配置都是如此)。如果您的依赖管理系统不这样做,或者如果您手动管理geb-core
依赖项,请务必将geb-implicit-assertions
依赖项作为编译依赖项包含在内。
9. Javascript、AJAX 和动态页面
本节讨论如何处理测试和/或自动化现代Web应用程序的一些挑战。
9.1. “js” 对象
浏览器实例公开了一个“js
”对象,该对象除了WebDriver提供的支持外,还支持JavaScript操作。理解WebDriver如何处理JavaScript非常重要,它是通过驱动程序对JavascriptExecutor.executeScript()
方法的实现来完成的。
在继续阅读之前,强烈建议阅读 |
您可以使用驱动程序实例通过浏览器像使用普通WebDriver一样执行JavaScript……
assert browser.driver.executeScript("return arguments[0];", 1) == 1
这有点冗长,正如您所期望的,Geb利用Groovy的动态性来简化操作。
|
9.1.1. 访问变量
浏览器内部的任何全局JavaScript变量都可以作为js
对象的属性读取。
给定以下页面……
<html>
<head>
<script type="text/javascript">
var aVariable = 1;
</script>
</head>
</html>
我们可以使用以下方式访问JavaScript变量“aVariable
”……
Browser.drive {
go "/"
assert js.aVariable == 1
}
或者如果我们想把它映射到页面内容……
Browser.drive {
to JsVariablePage
assert aVar == 1
}
我们甚至可以访问嵌套变量……
assert js."document.title" == "Book of Geb"
9.1.2. 调用方法
任何全局JavaScript函数都可以作为js
对象上的方法调用。
给定以下页面……
<html>
<head>
<script type="text/javascript">
function addThem(a,b) {
return a + b;
}
</script>
</head>
</html>
我们可以使用以下方式调用addThem()
函数……
Browser.drive {
go "/"
assert js.addThem(1, 2) == 3
}
这在页面和模块中也适用。
要调用嵌套方法,给定以下页面……
<html>
<head>
<script type="text/javascript">
functionContainer = {
addThem: function(a,b) {
return a + b;
}
}
</script>
</head>
</html>
我们使用与属性相同的语法……
Browser.drive {
Browser.drive {
go "/"
assert js."functionContainer.addThem"(1, 2) == 3
}
}
9.1.3. 执行任意代码
js
对象还有一个exec()
方法,可用于运行JavaScript片段。它与JavascriptExecutor.executeScript()
方法相同,只是它的参数顺序不同……
assert js.exec(1, 2, "return arguments[0] + arguments[1];") == 3
您可能想知道为什么顺序已更改(即参数位于脚本之前)。这使得编写多行JavaScript更方便……
js.exec 1, 2, """
someJsMethod(1, 2);
// lots of javascript
return true;
"""
9.2. 等待
Geb提供了一些方便的方法来等待某个条件为真。这对于测试使用AJAX、计时器或效果的页面非常有用。
waitFor
方法由WaitingSupport
混合提供,它委托给Wait
类(有关等待的精确语义,请参阅此类的waitFor()
方法的文档)。这些方法接受各种参数,这些参数决定根据{`}[Groovy Truth]等待给定闭包返回真对象的时间,以及再次调用闭包之间等待的时间。
waitFor {} (1)
waitFor(10) {} (2)
waitFor(10, 0.5) {} (3)
waitFor("quick") {} (4)
1 | 使用默认配置。 |
2 | 最多等待10秒,使用默认重试间隔。 |
3 | 等待长达10秒,每次重试之间等待半秒。有关如何更改默认值和定义预设,请参阅等待配置部分。 |
4 | 使用预设“quick”作为等待设置 |
也可以声明内容应隐式等待,请参阅内容定义的 |
9.2.1. 示例
这是一个示例,展示了使用waitFor()
来处理点击按钮后调用AJAX请求并在完成后创建新div
的情况的一种方式。
class DynamicPage extends Page {
static content = {
theButton { $("input", value: "Make Request") }
theResultDiv { $("div#result") }
}
def makeRequest() {
theButton.click()
waitFor { theResultDiv.present }
}
}
Browser.drive {
to DynamicPage
makeRequest()
assert theResultDiv.text() == "The Result"
}
回想一下,在Groovy中,return
关键字是可选的,因此在上面的示例中,$("div#result").present
语句充当闭包的返回值,并用作闭包是否通过的基础。这意味着您必须确保闭包内的最后一条语句返回一个根据{`}[Groovy Truth]为true
的值(如果您不熟悉Groovy Truth,请阅读该页面)。
因为浏览器将方法调用委托给页面对象,所以上述内容可以写成……
Browser.drive {
go "/"
$("input", value: "Make Request").click()
waitFor { $("div#result") }
assert $("div#result").text() == "The Result"
}
实际上,不使用显式 |
传递给waitFor()
方法(们)的闭包不需要是单语句。
waitFor {
def result = $("div#result")
result.text() == "The Result"
}
那样会工作得很好。
如果您希望在waitFor
闭包中以单独语句的形式测试多个条件,您只需将它们放在单独的行中。
waitFor {
def result = $("div")
result.@id == "result"
result.text() == "The Result"
}
9.2.2. 自定义消息
如果您希望为waitFor
调用超时时抛出的WaitTimeoutException
添加自定义消息,您可以通过为waitFor
调用提供消息参数来完成
waitFor(message: "My custom message") { $("div#result") }
9.2.3. 抑制 WaitTimeoutException
如果您不希望抛出WaitTimeoutException
,而希望在等待超时时返回上次评估结果,则可以将noException
参数设置为真值
waitFor(noException: true) { $("div#result") }
9.3. 等待时重新加载页面
除了通用的等待支持外,Geb还提供了一个便利方法,每次评估传递给它的块之前都会重新加载页面。
该方法在Page
类上可用,名为refreshWaitFor()
,除了页面重新加载行为外,它与waitFor()
方法相同。这意味着您可以向它传递与waitFor()
相同的参数,传递给它的块被隐式断言,并且它支持自定义消息。
以下示例展示了如何等待页面渲染的静态时间戳在测试开始后至少三百毫秒
class PageWithTimestamp extends Page {
static content = {
timestamp { OffsetDateTime.parse($().text()) }
}
}
def startTimestamp = OffsetDateTime.now()
to PageWithTimestamp
refreshWaitFor {
timestamp > startTimestamp.plus(300, MILLIS)
}
assert timestamp > startTimestamp.plus(300, MILLIS)
9.4. 控制网络条件
在驱动现代、单页、高度异步的Web应用程序时,时序问题频繁出现,是浏览器自动化的祸根,导致测试不稳定。正确等待异步事件完成变得至关重要。很多时候,如果异步事件发生相对较快,则异步性很难发现,只有在较慢的执行或网络环境中运行时才会暴露出来。
本节介绍了一个依赖于Chromium特定自定义WebDriver命令的功能。调用下一段中提到的方法仅在驱动基于Chromium的浏览器(Chromium、Chrome或Edge)时才有效,对于其他浏览器将抛出异常。 |
幸运的是,Chrome驱动程序支持通过其自定义setNetworkConditions
命令控制网络条件,该命令允许控制浏览器的网络条件。Geb在Browser
类上提供了setNetworkLatency()
和resetNetworkLatency()
方法,允许为正在驱动的浏览器引入网络延迟。添加几百毫秒的延迟通常会暴露出与网络相关的时序问题。在测试开发期间暂时添加网络延迟有助于消除时序问题,并且在调试可能发生在CI服务器等环境中但在开发人员机器等环境中不发生的此类问题时非常有用。
以下是setNetworkLatency()
方法的使用方式
def networkLatency = Duration.ofMillis(500)
browser.networkLatency = networkLatency
浏览器中发生的异步事件有多种类型,AJAX调用(与网络相关)只是其中一个例子。动画是Web应用程序中异步性的另一个例子,上述方法对动画或任何其他异步事件引入的时序问题没有影响。 |
9.5. 警报和确认对话框
WebDriver 目前不处理 alert() 和 confirm() 对话框。然而,我们可以通过一些 JavaScript 魔法来模拟它,正如WebDriver 关于此问题的讨论。Geb 为您实现了基于此解决方案的变通方法。请注意,此功能依赖于对浏览器 window
DOM 对象的更改,因此可能无法在所有平台上的所有浏览器上工作。当 WebDriver 添加对该功能的支持时,以下方法的底层实现将更改为使用更健壮的实现。Geb 通过混入到 Page
和 Module
中的 AlertAndConfirmSupport
类添加了此功能。
Geb 方法阻止浏览器实际显示对话框,这是一件好事。这可以防止浏览器在显示对话框时阻塞并导致测试无限期挂起。
意外的 |
9.5.1. alert()
有三种方法处理alert()
对话框
def withAlert(Closure actions)
def withAlert(Map params, Closure actions)
void withNoAlert(Closure actions)
第一种方法withAlert()
用于验证会产生alert对话框的操作。此方法返回alert消息。
给定以下 HTML……
<input type="button" name="showAlert" onclick="alert('Bang!');" />
withAlert()
方法的使用方式如下……
assert withAlert { $("input", name: "showAlert").click() } == "Bang!"
如果给定“actions”闭包未引发警报对话框,则将抛出AssertionError
。
withAlert()
方法也接受一个等待选项。如果您的“actions”闭包中的代码以异步方式引发对话框,并且可以像那样使用,则此选项很有用
assert withAlert(wait: true) { $("input", name: "showAlert").click() } == "Bang!"
wait
选项的可能值与内容定义的wait
选项一致。
第二种方法withNoAlert()
用于验证不会产生alert()
对话框的操作。如果给定“actions”闭包引发了警报对话框,则将抛出AssertionError
。
给定以下 HTML……
<input type="button" name="dontShowAlert" />
withNoAlert()
方法的使用方式如下……
withNoAlert { $("input", name: "dontShowAlert").click() }
当您做某事可能引发警报时,使用 |
此实现方式的一个副作用是,我们无法明确处理导致浏览器实际页面更改的操作(例如,单击传递给withAlert()
/withNoAlert()
的闭包中的链接)。我们可以检测到浏览器页面发生了更改,但我们无法知道在页面更改之前alert()
是否被调用。如果检测到页面更改,withAlert()
方法将返回字面值true
(而它通常会返回警报消息),而withNoAlert()
将成功。
9.5.2. confirm()
有五种方法处理confirm()
对话框
def withConfirm(boolean ok, Closure actions)
def withConfirm(Closure actions)
def withConfirm(Map params, Closure actions)
def withConfirm(Map params, boolean ok, Closure actions)
void withNoConfirm(Closure actions)
第一种方法withConfirm()
(及其默认值为“ok”的关联方法)用于验证将产生确认对话框的操作。此方法返回确认消息。ok
参数控制应单击“确定”还是“取消”按钮。
给定以下 HTML……
<input type="button" name="showConfirm" onclick="confirm('Do you like Geb?');" />
withConfirm()
方法的使用方式如下……
assert withConfirm(true) { $("input", name: "showConfirm").click() } == "Do you like Geb?"
如果给定“actions”闭包未引发确认对话框,则将抛出AssertionError
。
withConfirm()
方法也接受一个等待选项,就像withAlert()
方法一样。请参阅withAlert()
的描述以了解可能的值和用法。
另一种方法withNoConfirm()
用于验证不会产生确认对话框的操作。如果给定“actions”闭包引发了确认对话框,则将抛出AssertionError
。
给定以下 HTML……
<input type="button" name="dontShowConfirm" />
withNoConfirm()
方法的使用方式如下……
withNoConfirm { $("input", name: "dontShowConfirm").click() }
在执行可能引发确认的操作时,使用 |
此实现方式的一个副作用是,我们无法明确处理导致浏览器实际页面更改的操作(例如,单击传递给withConfirm()
/withNoConfirm()
的闭包中的链接)。我们可以检测到浏览器页面发生了更改,但我们无法知道在页面更改之前confirm()
是否被调用。如果检测到页面更改,withConfirm()
方法将返回字面值true
(而它通常会返回警报消息),而withNoConfirm()
将成功。
9.5.3. prompt()
Geb 不提供对 prompt()
的任何支持,因为它不常用且通常不鼓励使用。
9.6. jQuery 集成
Geb 对 jQuery 有特殊支持。导航器对象有一个特殊的适配器,可以简化对底层 DOM 元素调用 jQuery 方法。这最好通过示例来解释。
jQuery集成仅在您正在处理的页面包含jQuery时才有效,Geb不会为您在页面中安装它。支持的jQuery最低版本是1.4。 |
考虑以下页面
<html>
<head>
<script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript">
$(function() {
$("#a").mouseover(function() {
$("#b").show();
});
});
</script>
</head>
<body>
<div id="a"></div>
<div id="b" style="display:none;"><a href="http://www.gebish.org">Geb!</a></div>
</body>
</html>
我们想点击Geb链接,但由于它被隐藏而无法点击(WebDriver不允许与隐藏元素交互)。包含该链接的div(div“a”)仅在鼠标移动到div“a”上方时显示。
jQuery库提供了触发浏览器事件的便捷方法。我们可以用它来模拟鼠标移到div“a”上方。
在纯jQuery JavaScript中,我们会这样做……
jQuery("div#a").mouseover();
我们可以通过Geb轻松调用它……
js.exec 'jQuery("div#a").mouseover()'
那样可以,但可能不方便,因为它会复制Geb页面中的内容定义。Geb的jQuery集成允许您将Geb中定义的内容与jQuery一起使用。以下是我们如何在Geb中对元素调用mouseover
jQuery函数的方法……
$("div#a").jquery.mouseover()
需要明确的是,这是 Groovy 代码(而不是 JavaScript 代码)。它可以与页面一起使用……
class JQueryPage extends Page {
static content = {
divA { $("#a") }
divB { $("#b") }
}
}
to JQueryPage
divA.jquery.mouseover()
assert divB.displayed
导航器的jquery
属性在概念上等同于导航器所有匹配页面元素的jQuery对象。
这些方法也可以接受参数……
$("#a").jquery.trigger('mouseover')
这里允许使用与WebDriver的JavascriptExecutor.executeScript()
方法允许的类型相同的受限类型集。
在jquery
属性上调用的方法的返回值取决于相应的jQuery方法的返回值。jQuery对象将被转换为表示相同元素集的导航器,其他值(如对象、字符串和数字)按照WebDriver的JavascriptExecutor.executeScript()
方法返回。
为什么?
开发此功能是为了更容易触发与鼠标相关的事件。某些应用程序对鼠标事件非常敏感,在自动化环境中触发这些事件是一个挑战。jQuery提供了一个很好的API来模拟这些事件,这是一个很好的解决方案。另一种方法是使用interact()方法
。
10. 直接下载
Geb 具有一个 API,可用于从执行 Geb 脚本或测试的应用程序发出直接 HTTP 请求。这有助于进行细粒度请求和将 PDF、CSV、图像等内容下载到脚本或测试中,然后对其进行处理。
直接下载API通过使用java.net.HttpURLConnection
直接从执行Geb的应用程序连接到URL,绕过WebDriver。
直接下载API由DownloadSupport
类提供,该类混入到页面和模块中(这意味着您可以直接从任何需要的地方调用这些实例方法,例如驱动块、测试/规范中、页面对象上的方法、模块上的方法)。查阅DownloadSupport
API参考,了解可用的各种download*()
方法。
10.1. 下载示例
例如,假设您正在使用Geb来测试一个生成PDF文档的web应用程序。WebDriver API只能处理HTML文档。您希望点击PDF下载链接并对下载的PDF进行一些测试。直接下载API就是为了满足这种需求。
class LoginPage extends Page {
static content = {
loginButton(to: PageWithPdfLink) { $("input", name: "login") }
}
void login(String user, String pass) {
username = user
password = pass
loginButton.click()
}
}
class PageWithPdfLink extends Page {
static content = {
pdfLink { $("a#pdf-download-link") }
}
}
Browser.drive {
to LoginPage
login("me", "secret")
def pdfBytes = downloadBytes(pdfLink.@href)
}
这很简单,但请考虑幕后发生的事情。我们的应用程序要求我们登录,这意味着某种会话状态。Geb正在使用HttpURLConnection
来获取内容,在此之前,来自真实浏览器的cookie被传输到连接,使其可以重用相同的会话。PDF下载链接的href也可能是相对的,Geb通过将传递给下载函数的链接解析为浏览器当前页面URL来处理此问题。
10.2. 细粒度请求
直接下载API也可用于进行细粒度请求,这对于测试边缘情况或异常行为非常有用。
所有download*()
方法都带有一个可选的闭包,该闭包可以配置用于发出请求的HttpURLConnection
(在设置Cookie
头之后)。
例如,我们可以测试当我们在Accept
头中发送application/json
时会发生什么。
Browser.drive {
go "/"
def jsonBytes = downloadBytes { HttpURLConnection connection ->
connection.setRequestProperty("Accept", "application/json")
}
}
在进行上述操作之前,值得考虑通过Geb(一个浏览器自动化工具)进行此类测试是否是正确的做法。您可能会发现直接使用 |
10.3. 处理不受信任的证书
当遇到使用不受信任(例如自签名)SSL证书的Web应用程序时,尝试使用Geb的下载API时很可能会遇到异常。通过覆盖请求的行为,您可以解决此类问题。使用以下代码将允许对使用给定密钥库中证书的服务器执行请求
import geb.download.helper.SelfSignedCertificateHelper
def text = downloadText { HttpURLConnection connection ->
if (connection instanceof HttpsURLConnection) {
def keystore = getClass().getResource('/keystore.jks')
def helper = new SelfSignedCertificateHelper(keystore, 'password')
helper.acceptCertificatesFor(connection as HttpsURLConnection)
}
}
10.4. 默认配置
在配置中,可以通过提供一个闭包作为defaultDownloadConfig
属性来指定HttpURLConnection
对象的默认行为。
以下示例配置所有使用直接下载支持执行的请求都携带User-Agent
头。
defaultDownloadConfig = { HttpURLConnection connection ->
connection.setRequestProperty("User-Agent", "Geb")
}
此配置闭包将首先运行,因此在此处设置的任何内容都可以通过上面所示的细粒度请求配置进行覆盖。
10.5. 代理配置
如前所述,直接下载API使用java.net.HttpURLConnection
执行http请求。这意味着它可以与java.net.HttpURLConnection
完全相同的方式配置为使用代理,即通过设置http.proxyHost
和http.proxyPort
系统属性
10.6. 错误
在下载操作期间发生的任何I/O类型错误(例如HTTP 500响应)都将导致抛出DownloadException
,该异常封装了原始异常并提供了对用于发出请求的HttpURLConnection
的访问。
11. 脚本和绑定
Geb支持通过Browser.drive()
方法以及使用geb.binding.BindingUpdater
类来在脚本环境中使用,该类填充和更新可用于脚本的groovy.lang.Binding
。这与Cucumber-JVM使用的机制相同。
11.1. 设置
要使用绑定支持,您只需使用Binding
和Browser
创建一个BindingUpdater
对象……
import geb.Browser
import geb.binding.BindingUpdater
def binding = new Binding()
def browser = new Browser()
def updater = new BindingUpdater(binding, browser)
updater.initialize() (1)
def script = getClass().getResource(resourcePath).text
def result = new GroovyShell(binding).evaluate(script) (2)
updater.remove() (3)
1 | 填充并开始更新浏览器。 |
2 | 从类路径加载的资源中运行脚本。 |
3 | 从绑定中移除Geb位并停止更新它。 |
11.2. 绑定环境
11.2.1. 浏览器方法和属性
BindingUpdater
会在绑定中安装大多数Browser
公共方法的快捷方式。
以下是一个示例脚本,如果BindingUpdater
在其绑定上初始化,它将正常工作……
go "some/page"
at(SomePage)
waitFor { $("p#status").text() == "ready" }
js.someJavaScriptFunction()
downloadText($("a.textFile").@href)
在受管绑定中,通常可以在Browser.drive()
方法中调用的所有方法/属性都可用。这包括$()
方法。
提供以下方法
-
$()
-
go()
-
to()
-
via()
-
at()
-
waitFor()
-
withAlert()
-
withNoAlert()
-
withConfirm()
-
withNoConfirm()
-
download()
-
downloadStream()
-
downloadText()
-
downloadBytes()
-
downloadContent()
-
report()
-
reportGroup()
-
cleanReportGroupDir()
JavaScript接口属性js
也可用。Browser
对象本身以browser
属性的形式可用。
11.2.2. 当前页面
绑定更新程序还会将绑定的page
属性更新为浏览器的当前页面……
import geb.Page
class InitialPage extends Page {
static content = {
button(to: NextPage) { $("input.do-stuff") }
}
}
class NextPage extends Page {
}
to InitialPage
assert page instanceof InitialPage
page.button.click()
assert page instanceof NextPage
12. 报告
Geb 包含一个简单的报告机制,可用于在任何时间点捕获浏览器的状态快照。
报告器是Reporter
接口的实现。Geb附带了三个实际编写报告的实现:PageSourceReporter
、ScreenshotReporter
和FramesSourceReporter
,以及两个辅助实现:CompositeReporter
和MultiWindowReporter
。
有三个与报告相关的配置项:报告器实现、报告目录以及是否仅报告测试失败。如果没有显式定义报告器,将从ScreenshotReporter
(拍摄PNG截图)和PageSourceReporter
(将当前DOM状态转储为HTML)创建一个复合报告器。
您可以通过调用浏览器对象上的report(String label)
方法来生成报告。
Browser.drive {
go "http://google.com"
report "google home page"
}
如果调用 |
假设我们将reportsDir
配置为“reports/geb
”,运行此脚本后,我们将在该目录中找到两个文件
-
google home page.html
- 页面源的HTML转储 -
google home page.png
- 浏览器的屏幕截图,为PNG文件(如果驱动程序实现支持此功能)
为避免文件名中保留字符的问题,Geb 会将报告名称中任何不是字母数字、空格或连字符的字符替换为下划线。 |
12.1. 报告框架内容
如果您希望配置报告也写入包含页面帧源内容的报告,您可以按以下方式操作
import geb.report.*
reporter = new CompositeReporter(new PageSourceReporter(), new ScreenshotReporter(), new FramesSourceReporter())
12.2. 报告多个窗口
默认情况下,报告仅针对在生成报告时设置为当前浏览器窗口的窗口。也可以通过配置报告使用MultiWindowReporter
来为每个打开的窗口生成报告。
import geb.report.*
reporter = new MultiWindowReporter(new CompositeReporter(new PageSourceReporter(), new ScreenshotReporter()))
12.3. 报告组
配置机制允许您指定基本reportsDir
,这是报告默认写入的位置。也可以将报告组更改为该目录内的相对路径。
Browser.drive {
reportGroup "google"
go "http://google.com"
report "home page"
reportGroup "geb"
go "http://gebish.org"
report "home page"
}
我们现在已经在reportsDir
内部创建了以下文件……
-
google/home page.html
-
google/home page.png
-
gebish/home page.html
-
gebish/home page.png
浏览器将根据需要为报告组创建目录。默认情况下,报告组未设置,这意味着报告会写入reportsDir
的根目录。要在设置报告组后返回此状态,只需调用reportGroup(null)
即可。
测试集成通常会为您管理报告组,将其设置为测试类的名称。 |
12.4. 监听报告
可以在报告器上注册一个监听器,当生成报告时会收到通知。添加此功能是为了在生成报告时向标准输出写入内容,这是Jenkins JUnit Attachments Plugin将任意文件与测试执行关联起来的方式。报告监听器类型为ReportingListener
,可以作为配置的一部分指定……
import geb.report.*
reportingListener = new ReportingListener() {
void onReport(Reporter reporter, ReportState reportState, List<File> reportFiles) {
reportFiles.each {
println "[[ATTACHMENT|$it.absolutePath]]"
}
}
}
12.5. 清理
Geb不会自动为您清理报告目录。但是,它提供了一个您可以调用来执行此操作的方法。
Browser.drive {
cleanReportGroupDir()
go "http://google.com"
report "home page"
}
cleanReportGroupDir()
方法将删除当时设置的报告组目录。如果无法执行此操作,它将抛出异常。
Spock、JUnit和TestNG测试集成会自动为您清理报告目录,请参阅测试章节中关于这些集成的部分。 |
13. 测试
Geb通过与流行的测试框架(如Spock、JUnit、TestNg和Cucumber-JVM)集成,为功能Web测试提供了一流的支持。
13.1. Spock, JUnit & TestNG
Spock、JUnit和TestNG集成的工作方式基本相同。它们提供超类,用于设置一个Browser
实例,所有方法调用和属性访问/引用都通过Groovy的methodMissing
和propertyMissing
机制解析。这些超类只是GebTestManager
之上的一个薄层,并利用通过DynamicallyDispatchesToBrowser
注解注册的AST转换来实现对Browser
实例的动态方法和属性分派。如果提供的超类使用不便,或者您希望使用一个未开箱即用提供的测试框架,那么强烈建议在使用自定义集成时将GebTestManager
与DynamicallyDispatchesToBrowser
一起使用。在实现自定义集成时,查看提供内置测试框架支持的超类的代码是一个很好的起点。
回想一下,浏览器实例还会将它无法处理的任何方法调用或属性访问/引用转发到其当前页面对象,这有助于消除测试中的许多噪音。 |
考虑以下Spock规范……
import geb.spock.GebSpec
class FunctionalSpec extends GebSpec {
def "go to login"() {
when:
go "/login"
then:
title == "Login Screen"
}
}
这等价于……
import geb.spock.GebSpec
class FunctionalSpec extends GebSpec {
def "verbosely go to login"() {
when:
browser.go "/login"
then:
browser.page.title == "Login Screen"
}
}
13.1.1. 配置
浏览器实例由测试集成通过使用GebTestManager
创建。配置机制允许您控制驱动程序实现和基本URL等方面。
13.1.2. 报告
Spock、JUnit和TestNG集成还附带一个超类(每个集成模块的类名如下所示),如果测试失败,该超类会自动生成标签为“failure”的报告。它们还将报告组设置为测试类的名称(将“.”替换为“/”)。
report(String label)
浏览器方法被替换为专用版本。此方法与浏览器方法的工作方式相同,但会将计数器和当前测试方法名称作为前缀添加到给定标签。
import geb.spock.GebReportingSpec
import geb.spock.SpockGebTestManagerBuilder
import geb.test.GebTestManager
class LoginSpec extends GebReportingSpec {
def "login"() {
when:
go "/login"
username = "me"
report "login screen" (1)
login().click()
then:
title == "Logged in!"
}
}
1 | 生成登录界面的报告。 |
假设配置的reportsDir
为reports/geb
,以及默认的报告器(即ScreenshotReporter
和PageSourceReporter
),我们将找到以下文件
-
reports/geb/my/tests/LoginSpec/001-001-login-login screen.html
-
reports/geb/my/tests/LoginSpec/001-001-login-login screen.png
如果标题断言失败,则还会生成以下文件
-
reports/geb/my/tests/LoginSpec/001-002-login-failure.html
-
reports/geb/my/tests/LoginSpec/001-002-login-failure.png
报告文件名格式为
«test method number»-«report number in test method»-«test method name»-«label».«extension»
报告是一个非常有用的功能,可以帮助您更容易地诊断测试失败。在可能的情况下,首选使用自动报告基类。
13.1.3. Cookie 管理
GebTestManager
以及Spock、JUnit和TestNG内置集成将在每个测试方法结束时自动清除浏览器当前域的cookie。除非GebTestManager.resetBrowserAfterEachTestPredicate
评估为false
(例如@Stepwise
Spock规范),否则此操作会在GebTestManager.afterTest()
被调用时发生——在这种情况下,cookie清除会在GebTestManager.afterTestClass()
中发生(这意味着逐步规范中的所有特性方法共享相同的浏览器状态)。
这种Cookie的自动清除可以通过配置禁用。
如果您需要在多个域中清除cookie,您需要手动跟踪URL并调用clearCookies(String… additionalUrls)
。
13.1.4. Web 存储管理
GebTestManager
以及Spock、JUnit和TestNG内置集成可以配置为在每个测试方法结束时自动清除浏览器当前域的Web存储(即本地存储和会话存储)。除非GebTestManager.resetBrowserAfterEachTestPredicate
评估为false
(例如@Stepwise
Spock规范),否则此操作会在GebTestManager.afterTest()
被调用时发生——在这种情况下,Web存储清除会在GebTestManager.afterTestClass()
中发生(这意味着逐步规范中的所有特性方法共享相同的浏览器状态)。
13.1.5. 测试中途重启浏览器
如果您想在测试中途重启浏览器,您必须注意Geb的测试支持中存在两层驱动程序实例缓存。首先,作为GebTestManager
实例中字段存储的延迟初始化的Browser
实例,该实例由为各种测试框架提供支持的基类持有,它持有WebDriver
实例的引用——因此,您需要从基类或直接在GebTestManager
实例上调用resetBrowser()
方法来清除该字段。其次,Geb默认缓存WebDriver
实例,如隐式驱动程序生命周期部分所述。要清除缓存并退出浏览器,您需要调用CachingDriverFactory.clearCacheAndQuitDriver()
。
因此,您可以在测试中使用以下代码来重启浏览器
testManager.resetBrowser()
CachingDriverFactory.clearCacheAndQuitDriver()
13.2. Cucumber (Cucumber-JVM)
可以同时
-
编写自己的Cucumber-JVM步骤来操作Geb
-
使用一个预构建步骤库来驱动Geb完成许多常见任务
13.2.1. 编写自己的步骤
使用 Geb 的绑定管理功能在 before/after 钩子中绑定浏览器,通常在名为 env.groovy
的文件中
def bindingUpdater
Before() { scenario ->
bindingUpdater = new BindingUpdater(binding, new Browser())
bindingUpdater.initialize()
}
After() { scenario ->
bindingUpdater.remove()
}
然后,正常的Geb命令和对象在您的Cucumber步骤中可用
import static cucumber.api.groovy.EN.*
Given(~/I am on the DuckDuckGo search page/) { ->
to DuckDuckGoHomePage
waitFor { at(DuckDuckGoHomePage) }
}
When(~/I search for "(.*)"/) { String query ->
page.search.value(query)
page.searchButton.click()
}
Then(~/I can see some results/) { ->
assert at(DuckDuckGoResultsPage)
}
Then(~/the first link should be "(.*)"/) { String text ->
waitFor { page.results }
assert page.resultLink(0).text()?.contains(text)
}
13.2.2. 使用预构建步骤
geb-cucumber项目有一组预构建的cucumber步骤,用于驱动Geb。因此,例如,一个具有类似上述步骤的功能将如下所示
When I go to the duck duck go home page And I enter "cucumber-jvm github" into the search field And I click the search button Then the results table 1st row link matches /cucumber\/cucumber-jvm · GitHub.*/
更多示例请参阅geb-cucumber。
geb-cucumber 也会自动进行Geb绑定,因此如果它被识别,您就不需要像上面那样自己进行绑定。
14. 云浏览器测试
当您希望在多个浏览器和操作系统上执行Web测试时,为每个目标环境维护机器可能会非常复杂。有几家公司提供“远程Web浏览器即服务”,使得进行此类矩阵测试变得容易,而无需自己维护多个浏览器安装。Geb提供了与其中三个服务(SauceLabs、BrowserStack和LambdaTest)的轻松集成。这种集成包括两部分:在GebConfig.groovy
中创建驱动程序的辅助和Gradle插件。
14.1. 创建驱动程序
对于SauceLabs、BrowserStack和LambdaTest,都提供了一个特殊的驱动工厂,该工厂在给定浏览器规范以及用户名和访问密钥的情况下,创建配置为使用云中浏览器的RemoteWebDriver
实例。下面包含GebConfig.groovy
中典型用法的示例。如果设置了适当的系统属性,它们将配置Geb在SauceLabs/BrowserStack/LambdaTest中运行,否则将使用配置的任何驱动程序。这对于在本地浏览器中运行代码进行开发非常有用。理论上您可以使用任何系统属性来传递浏览器规范,但geb.saucelabs.browser
/geb.browserstack.browser
/geb.lambdatest.browser
也由Geb Gradle插件使用,因此最好坚持使用这些属性名称。
传递给create()
方法的第一个参数是“浏览器规范”,它应该是一个Java属性文件格式的所需浏览器功能列表
browserName=«browser name as per values of fields in org.openqa.selenium.remote.BrowserType» platformName=«platform as per enum item names in org.openqa.selenium.Platform» browserVersion=«version»
假设您在GebConfig.groovy
中使用以下代码片段通过SauceLabs在Linux上使用Firefox 19执行您的代码,您需要将geb.saucelabs.browser
系统属性设置为
browserName=firefox platformName=LINUX browserVersion=19
并将其设置为在Vista上使用IE 9执行
browserName=internet explorer platformName=VISTA browserVersion=19=9
某些浏览器(如Chrome)会自动更新到最新版本;对于这些浏览器,您无需指定版本,因为只有一个版本,您可以使用类似
browserName=chrome platformName=MAC
作为“浏览器规范”。有关可用浏览器、版本和操作系统的完整列表,请参阅您的云提供商的文档
请注意,Geb Gradle 插件可以为您设置 geb.saucelabs.browser
/geb.browserstack.browser
/geb.lambdatest.browser
系统属性,使用上述格式。
浏览器规范之后是用于识别您与云提供商帐户的用户名和访问密钥。示例使用两个环境变量来访问此信息。这通常是在开放CI服务(如drone.io或Travis CI)中将秘密传递给构建的最简单方法,如果您的代码是公开的,但您可以根据需要使用其他机制。
您可以选择通过向create()
方法提供一个Map作为最后一个参数来传递额外的配置设置。自Selenium 4以来,您需要为任何不符合W3C标准的功能使用供应商前缀。例如,要在SauceLabs中指定要使用的Selenium版本,您将使用sauce:options.seleniumVersion
作为功能键,其中sauce:options
是供应商前缀,seleniumVersion
是供应商特定的功能键。可用的配置选项在您的云提供商的文档中进行了描述
最后,还有一个create()
方法的重载版本,它不接受字符串规范,允许您简单地使用map指定所有所需功能。如果您只想使用工厂,但不需要构建级别的参数化,此方法可能很有用。
14.1.1. SauceLabsDriverFactory
以下是GebConfig.groovy
中利用SauceLabsDriverFactory
配置将使用SauceLabs云中提供的浏览器的驱动程序的示例。
def sauceLabsBrowser = System.getProperty("geb.saucelabs.browser")
if (sauceLabsBrowser) {
driver = {
def username = System.getenv("GEB_SAUCE_LABS_USER")
assert username
def accessKey = System.getenv("GEB_SAUCE_LABS_ACCESS_PASSWORD")
assert accessKey
new SauceLabsDriverFactory().create(sauceLabsBrowser, username, accessKey)
}
}
默认情况下,SauceLabsDriverFactory
创建连接到SauceLabs美国数据中心的RemoteWebDriver
实例。如果您希望使用不同的数据中心,只需将给定数据中心的主机名传递给构造函数。以下示例显示了使用欧盟数据中心的主机名
new SauceLabsDriverFactory("ondemand.eu-central-1.saucelabs.com")
14.1.2. BrowserStackDriverFactory
以下是GebConfig.groovy
中利用BrowserStackDriverFactory
配置将使用BrowserStack云中提供的浏览器的驱动程序的示例。
def browserStackBrowser = System.getProperty("geb.browserstack.browser")
if (browserStackBrowser) {
driver = {
def username = System.getenv("GEB_BROWSERSTACK_USERNAME")
assert username
def accessKey = System.getenv("GEB_BROWSERSTACK_AUTHKEY")
assert accessKey
new BrowserStackDriverFactory().create(browserStackBrowser, username, accessKey)
}
}
如果使用localIdentifier
支持
def browserStackBrowser = System.getProperty("geb.browserstack.browser")
if (browserStackBrowser) {
driver = {
def username = System.getenv("GEB_BROWSERSTACK_USERNAME")
assert username
def accessKey = System.getenv("GEB_BROWSERSTACK_AUTHKEY")
assert accessKey
def localId = System.getenv("GEB_BROWSERSTACK_LOCALID")
assert localId
new BrowserStackDriverFactory().create(browserStackBrowser, username, accessKey, localId)
}
}
14.1.3. LambdaTestDriverFactory
以下是GebConfig.groovy
中利用LambdaTestDriverFactory
配置将使用LambdaTest云中提供的浏览器的驱动程序的示例。
def lambdaTestBrowser = System.getProperty("geb.lambdatest.browser")
if (lambdaTestBrowser) {
driver = {
def username = System.getenv("GEB_LAMBDATEST_USERNAME")
assert username
def accessKey = System.getenv("GEB_LAMBDATEST_AUTHKEY")
assert accessKey
new LambdaTestDriverFactory().create(lambdaTestBrowser, username, accessKey)
}
}
如果使用TunnelIdentifier
支持
def lambdaTestBrowser = System.getProperty("geb.lambdatest.browser")
if (lambdaTestBrowser) {
driver = {
def username = System.getenv("GEB_LAMBDATEST_USERNAME")
assert username
def accessKey = System.getenv("GEB_LAMBDATEST_AUTHKEY")
assert accessKey
def tunnelName = System.getenv("GEB_LAMBDATEST_TUNNEL_NAME")
assert tunnelName
new LambdaTestDriverFactory().create(lambdaTestBrowser, username, accessKey, tunnelName)
}
}
14.2. Gradle 插件
对于SauceLabs、BrowserStack和LambdaTest,Geb提供了一个Gradle插件,它简化了声明所需的账户和浏览器,以及配置隧道以允许云提供商访问本地应用程序。这些插件允许轻松创建多个Test
任务,这些任务将设置适当的geb.PROVIDER.browser
属性(其中PROVIDER是saucelabs
、browserstack
或lambdatest
)。然后,该属性的值可以在配置文件中传递给SauceLabsDriverFactory
/BrowserStackDriverFactory
/LambdaTestDriverFactory
作为“浏览器规范”。下面包含典型用法的示例。
14.2.1. geb-saucelabs 插件
以下是使用geb-saucelabs Gradle插件的示例。
import geb.gradle.saucelabs.SauceAccount
plugins {
id "org.gebish.saucelabs" version "7.0" (1)
}
dependencies { (2)
sauceConnect "com.saucelabs:ci-sauce:1.153"
}
sauceLabs {
browsers { (3)
firefox_linux_19
chrome_mac
delegate."internet explorer_vista_9"
nexus4 { (4)
capabilities(
browserName: "android",
platformName: "Linux",
browserVersion: "4.4",
"sauce:options.deviceName": "LG Nexus 4"
)
}
}
task { (5)
maxHeapSize = "512m"
}
additionalTask("quick") { (6)
useJUnit {
includeCategories "com.example.Quick"
}
}
account { (7)
username = System.getenv(SauceAccount.USER_ENV_VAR)
accessKey = System.getenv(SauceAccount.ACCESS_KEY_ENV_VAR)
}
connect { (8)
port = 4444 (9)
additionalOptions = ['--proxy', 'proxy.example.com:8080'] (10)
}
}
1 | 将插件应用于构建。 |
2 | 将SauceConnect的版本声明为sauceConnect 配置的一部分。这将由在运行生成的测试任务之前打开SauceConnect隧道的任务使用,这意味着云中的浏览器将具有指向运行构建的机器的localhost。 |
3 | 声明测试应使用简写语法在3个不同的浏览器中运行;这将生成以下Test 任务:firefoxLinux19Test 、chromeMacTest 和internet explorerVista9Test 。 |
4 | 如果简写语法不允许您表达所有必需的功能,则明确指定所需的浏览器功能;该示例将生成一个名为nexus4Test 的Test 任务。 |
5 | 配置所有生成的测试任务;对于每个任务,闭包都将在委托设置为正在配置的测试任务的情况下运行。 |
6 | 为每个浏览器添加一个额外的测试任务,字符串作为第一个参数添加到任务名称前;作为最后一个参数传递的闭包将在委托设置为正在添加的测试任务的情况下运行。 |
7 | 传入SauceConnect的凭据。 |
8 | 如果需要,另外配置SauceConnect。 |
9 | 覆盖SauceConnect使用的端口,默认为4445。 |
10 | 将额外的命令行选项传递给SauceConnect。 |
您可以使用 |
禁用 SauceConnect
该插件默认管理SauceConnect实例的生命周期,这允许将SauceLabs中配置的浏览器指向可通过localhost访问但无法从Internet访问的URL。
如果您只将浏览器指向可以通过互联网访问的URL,并且希望摆脱打开隧道相关的开销,您可能希望禁用SauceConnect的使用。可以通过以下方式完成
sauceLabs {
useTunnel = false
}
14.2.2. geb-browserstack 插件
以下是使用geb-browserstack Gradle插件的示例。
import geb.gradle.browserstack.BrowserStackAccount
plugins {
id "org.gebish.browserstack" version "7.0" (1)
}
browserStack {
application 'https://:8080' (2)
browsers { (3)
firefox_mac_19
chrome_mac
delegate."internet explorer_windows_9"
nexus4 { (4)
capabilities browserName: "android", platformName: "ANDROID", "bstack:options.device": "Google Nexus 4"
}
}
task { (5)
maxHeapSize = "512m"
}
additionalTask("quick") { (6)
useJUnit {
includeCategories "com.example.Quick"
}
}
account { (7)
username = System.getenv(BrowserStackAccount.USER_ENV_VAR)
accessKey = System.getenv(BrowserStackAccount.ACCESS_KEY_ENV_VAR)
}
local {
force = true (8)
tunnelReadyMessage = 'You can now access your local server(s) in our remote browser' (9)
}
}
1 | 将插件应用于构建。 |
2 | 指定BrowserStack隧道应可访问的URL。可以指定多个应用程序。如果未指定任何应用程序,则隧道将不受特定URL的限制。 |
3 | 声明测试应使用简写语法在3个不同的浏览器中运行;这将生成以下Test 任务:firefoxLinux19Test 、chromeMacTest 和internet explorerVista9Test 。 |
4 | 如果简写语法不允许您表达所有必需的功能,则明确指定所需的浏览器功能;该示例将生成一个名为nexus4Test 的Test 任务。 |
5 | 配置所有生成的测试任务;对于每个任务,闭包都将在委托设置为正在配置的测试任务的情况下运行。 |
6 | 为每个浏览器添加一个额外的测试任务,字符串作为第一个参数添加到任务名称前;作为最后一个参数传递的闭包将在委托设置为正在添加的测试任务的情况下运行。 |
7 | 传递BrowserStack的凭据。 |
8 | 配置BrowserStack隧道以通过本地机器路由所有流量;此配置属性控制-forcelocal 标志,默认值为false 。 |
9 | 设置在BrowserStack隧道进程输出中搜索的自定义消息,然后才认为其成功启动——如果进程输出已更改且找不到默认消息,则此功能很有用。 |
也可以指定代理的位置和凭据,以便与BrowserStack隧道一起使用
browserStack {
local {
proxyHost = '127.0.0.1'
proxyPort = '8080'
proxyUser = 'user'
proxyPass = 'secret'
}
}
以及隧道ID和任何其他必要的命令行选项
browserStack {
local {
id = 'Custom id'
additionalOptions = ['--log-file', '/tmp/browser-stack-local.log']
}
}
您可以使用 |
禁用 BrowserStack 隧道
该插件默认管理隧道的生命周期,这允许将BrowserStack中配置的浏览器指向可以通过localhost访问但无法从Internet访问的URL。
如果您只将浏览器指向互联网可访问的URL,并且希望摆脱打开隧道带来的开销,您可能需要禁用它的使用。可以通过以下方式完成
browserStack {
useTunnel = false
}
14.2.3. geb-lambdatest 插件
以下是使用geb-lambdatest Gradle插件的示例。
import geb.gradle.lambdatest.LambdaTestAccount
plugins {
id "org.gebish.lambdatest" version "7.0" (1)
}
lambdaTest {
application 'https://:8080' (2)
browsers { (3)
chrome_windows_70 {
capabilities platformName: "Windows 10" (4)
}
}
task { (5)
maxHeapSize = "512m"
}
additionalTask("quick") { (6)
useJUnit {
includeCategories "com.example.Quick"
}
}
account { (7)
username = System.getenv(LambdaTestAccount.USER_ENV_VAR)
accessKey = System.getenv(LambdaTestAccount.ACCESS_KEY_ENV_VAR)
}
tunnelOps {
additionalOptions = ['--proxyhost', 'proxy.example.com'] (8)
tunnelReadyMessage = 'Secure connection established, you may start your tests now' (9)
}
}
1 | 将插件应用于构建。 |
2 | 指定 LambdaTest 隧道应能够访问的 URL。可以指定多个应用程序。如果未指定任何应用程序,隧道将不受特定 URL 的限制。 |
3 | 测试将在Windows 10上的chrome中作为示例运行。 |
4 | 如果简写语法无法表达所有所需功能,则明确指定所需的浏览器功能。 |
5 | 配置所有生成的测试任务;对于每个任务,闭包都将在委托设置为正在配置的测试任务的情况下运行。 |
6 | 为每个浏览器添加一个额外的测试任务,字符串作为第一个参数添加到任务名称前;作为最后一个参数传递的闭包将在委托设置为正在添加的测试任务的情况下运行。 |
7 | 传递 LambdaTest 的凭据。 |
8 | 将额外的命令行选项传递给LambdaTestTunnel。 |
9 | 设置在LambdaTest隧道进程输出中搜索的自定义消息,然后才认为它已成功启动——如果进程的输出已更改且找不到默认消息,则此功能很有用。 |
您可以使用 |
禁用 LambdaTest 隧道
该插件默认管理隧道的生命周期,这允许将LambdaTest中配置的浏览器指向可通过localhost访问但无法从Internet访问的URL。
如果您只将浏览器指向互联网可访问的URL,并且希望摆脱打开隧道带来的开销,您可能需要禁用它的使用。可以通过以下方式完成
lambdaTest {
useTunnel = false
}
构建系统和框架集成
这种Geb集成通常专注于管理基本URL和报告目录,因为构建系统倾向于能够提供此配置(通过构建适配器机制)。
Gradle
将Geb与Gradle一起使用只需引入适当的依赖项,并在需要时在构建脚本中配置基本URL和报告目录。
以下是使用Geb进行测试的有效Gradle build.gradle
文件。
apply plugin: "groovy"
repositories {
mavenCentral()
}
ext {
gebVersion = "7.0"
seleniumVersion = "4.2.2"
}
dependencies {
// If using Spock, need to depend on geb-spock
testCompile "org.gebish:geb-spock:${gebVersion}"
testCompile "org.spockframework:spock-core:2.3-groovy-4.0"
// If using JUnit, need to depend on geb-junit (4 or 5)
testCompile "org.gebish:geb-junit4:${gebVersion}"
testCompile "junit:junit-dep:4.8.2"
// Need a driver implementation
testCompile "org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}"
testRuntime "org.seleniumhq.selenium:selenium-support:${seleniumVersion}"
}
test {
systemProperties "geb.build.reportsDir": "$reportsDir/geb"
}
有一个Gradle示例项目可用。
Maven
使用Geb与Maven,只需引入适当的依赖项,并在构建脚本中配置基本URL和报告目录(如果需要)。
以下是用于测试(与Spock一起)的有效pom.xml
文件。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.gebish.example</groupId>
<artifactId>geb-maven-example</artifactId>
<packaging>jar</packaging>
<version>1</version>
<name>Geb Maven Example</name>
<url>http://www.gebish.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>2.3-groovy-4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.gebish</groupId>
<artifactId>geb-spock</artifactId>
<version>7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-firefox-driver</artifactId>
<version>4.2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-support</artifactId>
<version>4.2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<includes>
<include>*Spec.*</include>
</includes>
<systemPropertyVariables>
<geb.build.baseUrl>http://google.com/ncr</geb.build.baseUrl>
<geb.build.reportsDir>target/test-reports/geb</geb.build.reportsDir>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.gmaven</groupId>
<artifactId>gmaven-plugin</artifactId>
<version>1.3</version>
<configuration>
<providerSelection>1.7</providerSelection>
</configuration>
<executions>
<execution>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
有一个Maven示例项目可用。
IDE 支持
Geb在IDE中不需要任何特殊插件或配置。然而,本章将讨论一些注意事项。
执行
如果IDE支持执行Groovy脚本,则可以在IDE中执行Geb脚本。所有支持Groovy的IDE通常都支持此功能。此配置通常只有两个关注点:将Geb类放在类路径上,以及GebConfig.groovy
文件。
如果IDE支持Groovy脚本以及您与Geb一起使用的测试框架,则可以在IDE中执行Geb测试。如果您使用JUnit或Spock(基于JUnit),这很简单,因为所有现代Java IDE都支持JUnit。就IDE而言,Geb测试只是一个JUnit测试,不需要特殊支持。与执行脚本一样,IDE必须将Geb类放在测试执行的类路径上,并且GebConfig.groovy
文件必须可访问(通常将此文件放在测试源树的根目录就足够了)。
在这两种情况下,创建此类 IDE 配置最简单的方法是使用支持 IDE 集成的构建工具(例如 Gradle 或 Maven)。这将负责类路径设置和其他问题。
编写辅助(自动完成和导航)
本节讨论 IDE 可以提供哪些类型的编写辅助以及可以实现更好编写支持的使用模式。
动态性和简洁性 vs 工具支持
Geb 大力采用 Groovy 提供的动态类型,以实现简洁性以提高可读性。这立即减少了 IDE 在编写 Geb 代码时可以提供的编写辅助量。这是一种有意的妥协。功能/验收测试的主要成本在于测试套件的维护时间。Geb 通过多种方式对此进行优化,其中之一是侧重于意图揭示代码(通过简洁性实现)。
话虽如此,如果编写支持是您关心的问题,请继续阅读以了解如何放弃简洁性以改善编写支持的详细信息。
强类型
为了获得更好的编写支持,您必须在测试和页面对象中包含类型。此外,您必须显式访问浏览器和页面对象,而不是依赖动态调度。
以下是惯用(无类型)Geb 代码的示例。
to HomePage
loginPageLink.click()
at LoginPage
username = "user1"
password = "password1"
loginButton.click()
at SecurePage
使用类型编写的相同代码将如下所示
HomePage homePage = browser.to HomePage
homePage.loginPageLink.click()
LoginPage loginPage = browser.at LoginPage
SecurePage securePage = loginPage.login("user1", "password1")
其中页面对象是
class HomePage extends Page {
Navigator getLoginPageLink() {
$("#loginLink")
}
}
class LoginPage extends Page {
static at = { title == "Login Page" }
Navigator getLoginButton() {
$("input", type: "submit")
}
SecurePage login(String username, String password) {
$(name: "username").value username
$(name: "password").value password
loginButton.click()
browser.at SecurePage
}
}
class SecurePage extends Page {
static at = { title == "Secure Page" }
}
总结
-
显式使用
browser
对象(由测试适配器提供) -
使用
to()
和at()
方法返回的页面实例,而不是通过浏览器调用 -
使用
Page
类上的方法,而不是content {}
块和动态属性 -
如果您需要使用
required:
和wait:
等内容定义选项,那么您仍然可以像往常一样在Page
和Module
类中的方法中引用使用 DSL 定义的内容元素,例如
IntelliJ IDEA 包含对 Geb 引入的某些构造的支持,这使得上述某些内容变得不必要。如果您是 IntelliJ IDEA 的用户,请务必熟悉有关该支持内容的章节。 |
static content = {
async(wait: true) { $("#async") }
}
String asyncText() {
async.text() (1)
}
1 | 在这里等待异步定义返回非空导航器… |
使用这种“类型化”样式不是全有或全无的命题。类型选项存在于一个连续体上,可以有选择地使用,在额外“噪音”的成本值得实现更好 IDE 支持的情况下。例如,可以轻松地混合使用 content {}
DSL 和方法。关键的实现者是捕获 to()
和 at()
方法的结果,以访问页面对象实例。
作为 at 检查的一部分执行断言
通过使用变量来跟踪当前页面实例以获得改进的编写支持的替代方法是使用将闭包作为最后一个参数的 at()
方法。该闭包中的每个语句都隐式断言,其委托设置为作为第一个参数传递的页面。由于在方法签名中的闭包参数上使用了 @DelegatesTo
注解,支持该注解的 IDE 理解调用已委托给给定的页面实例,并且可以提供代码补全。
上一节的示例可以改写为以下方式
HomePage homePage = browser.to HomePage
homePage.loginPageLink.click()
browser.at(LoginPage) {
login("user1", "password1")
}
at SecurePage
IntelliJ IDEA 支持
IntelliJ IDEA(从版本 12 开始)对编写 Geb 代码有特殊支持。这内置于 Groovy 支持中;无需额外安装。
该支持提供
-
理解测试类中隐式浏览器方法(例如
to()
,at()
)(例如extends GebSpec
) -
理解通过内容 DSL 定义的内容(仅在
Page
和Module
类中) -
在
at {}
和content {}
块中完成
这有效地启用了更多的编写支持,但显式类型信息更少。作为一个示例,让我们重新审视关于强类型化的一节中的示例,看看如果使用 Intellij IDEA,如何在保持完整自动完成支持的同时简化它
def homePage = to HomePage
homePage.loginPageLink.click()
def loginPage = at LoginPage
def securePage = loginPage.login("user1", "password1")
其中页面对象是
class HomePage extends Page {
static content = {
loginPageLink { $("#loginLink") }
}
}
class LoginPage extends Page {
static at = { title == "Login Page" }
static content = {
loginButton { $("input", type: "submit") }
}
SecurePage login(String username, String password) {
$(name: "username").value username
$(name: "password").value password
loginButton.click()
browser.at SecurePage
}
}
class SecurePage extends Page {
static at = { title == "Secure Page" }
}
Geb 开发团队要感谢 JetBrains 的好心人,将这种对 Geb 的明确支持添加到 IDEA 中。
关于项目
Geb 主页可在http://www.gebish.org找到。
支持与开发
Geb 的支持在 geb-user@googlegroups.com 邮件列表中提供,可在此处订阅。
关于 Geb 的想法和新功能可以在 geb-dev@googlegroups.com 邮件列表中讨论,可在此处订阅。
鸣谢
贡献者
-
亚历山大·佐洛托夫 - TestNG 集成
-
克里斯托弗·诺伊罗斯 - 各种修复和补丁
-
安东尼·琼斯 - 各种修复和补丁,文档改进
-
扬-亨德里克·彼得斯 - 文档改进
-
托马斯·林 - 文档改进
-
杰森·卡胡恩 - 文本匹配器错误修复
-
托马斯·卡尔科辛斯基 - 文档改进
-
里奇·道格拉斯·埃文斯 - 文档改进
-
伊恩·达坎 - 文档改进
-
科林·哈灵顿 - 文档改进
-
鲍勃·赫尔曼 - 文档改进
-
乔治·T·沃尔特斯二世 -
withWindow()
的页面选项支持 -
克雷格·阿特金森 - 文档改进
-
安迪·邓肯 - 遇到意外页面时快速失败
-
约翰·恩格尔曼 - Grails 集成改进
-
迈克尔·莱加特 - Grails 集成改进
-
格雷姆·罗歇 - Grails 集成改进
-
肯·盖斯 - 文档改进
-
凯利·罗宾逊 - SauceLabs 的附加配置参数
-
托德·格斯帕切尔 - 文档改进,清理 settings.gradle
-
大卫·M·卡尔 - BrowserStack 集成
-
汤姆·邓斯坦 - Cucumber 集成和相关文档
-
布莱恩·科特克 - 文档改进
-
大卫·W·米勒 - 文档改进
-
廖爱琳 - 文档改进
-
瓦伦·梅农 - Selenium By 选择器支持及相关文档,支持导航到页面实例以及类
-
安德斯·D·约翰逊 - 文档改进
-
大中弘行 - 文档改进
-
埃里克·普拉格特 - 手册初步迁移到 AsciiDoctor
-
维贾伊·博莱帕利 - 各种修复
-
皮埃尔·希尔特 -
hasNot()
过滤 -
高桥洋太郎 - 文档改进
-
约亨·伯格 - 尝试设置不存在的选择选项时更好的错误报告
-
马坦·卡茨 - 支持为 BrowserStack 隧道设置
forcelocal
标志 -
维克托·帕玛尔 - 添加配置选项,用于在
WaitTimeoutException
的消息中包含原因字符串表示 -
贝拉尔迪诺·拉·托雷 - 修复
waitFor()
方法的错误委托 -
马库斯·施利希廷 - 文档修复
-
安德烈·希特林 - 报告文件名中字符替换的改进
-
利奥·费多罗夫 - 支持 Spock 集成的
reportOnTestFailureOnly
配置选项 -
克里斯·伯恩汉姆 - 能够指定与 BrowserStack 隧道一起使用的代理位置和凭据
-
阿西姆·班萨尔 - 文档改进
-
托马什·普日比什 - 将模块解包为其声明类型
-
布莱恩·斯图尔特 - 文档改进
-
雅各布·奥·米克尔森 - 各种修复和改进
-
帕特里克·拉德特克 - 文档改进
-
伦纳德·布吕宁斯 - 修复了在预期页面检查失败后才检查意外页面的问题,添加了新的复合文本匹配器
-
李·巴茨 - 尝试在单选选择元素上选择 null 时改进错误消息
-
里奇·伦格 - 忽略作为 css 选择器一部分传递给
Navigator.filter()
的标签名大小写 -
赫苏斯·L·D·穆里尔 - 文档修复
-
约亨·沙兰达 - 文档
-
迈克尔·库茨 - 添加了
NumberInput
、RangeInput
、UrlInput
、PasswordInput
、ColorInput
、DateTimeLocalInput
、TimeInput
、MonthInput
和WeekInput
-
亚历山大·克里希 - 文档修复
-
哈雷·法格特 - 文档修复
-
阿尔皮特·古普塔 - 与 LambdaTest 集成
-
乔纳森·莱舒 - 配置项目上 Gradle Wrapper 验证 GitHub Action 的执行
-
何塞·路易斯·罗德里格斯·阿隆索 - 网站改进
-
斯蒂芬·克拉森 - 文档改进
-
普热米斯瓦夫·比耶利茨基 - 从构建中移除弃用项
-
亚瑟·森吉列夫 - 依赖项更新
-
比约恩·考特勒 - 各种改进
-
阿列克谢·阿肯蒂耶夫 - 修复
OnFailureReporter
中跳过和中止测试的处理
历史
此页面列出了 Geb 版本之间的高级更改。
4.0
改进
-
引入
GebTestManager
以减少测试框架集成之间的代码重复,并使用户更容易为其他框架添加集成。 [#614] -
当使用嵌套的 withFrame() 调用时,改进帧上下文管理。 [#612]
重大更改
-
为受益于
GebTestManager
的引入,提供各种测试框架支持的超类已被以不向后兼容的方式重写。 [#614] -
Geb 不再依赖
groovy-all
artifact,而是依赖org.codehaus.groovy
组的groovy
、groovy-templates
和groovy-macro
artifact。 [#618] -
更新到 Groovy 2.5.13。 [#617]
-
移除了已弃用的
geb.PageChangeListener
,取而代之的是geb.PageEventListener
。 [#593] -
更新到 Gradle 6.7 并构建云浏览器 Gradle 插件。 [#622]
-
在 LambdaTest 集成的 Gradle 插件中,将隧道 ID 重命名为隧道名称。 [#606]
3.0
重大更改
-
geb.navigator.EmptyNavigator
类已被移除。 [#557] -
geb.navigator.factory.InnerNavigatorFactory
中的方法签名已更改。 [#557] -
多个方法已从
geb.navigator.Locator
移至geb.navigator.BasicLocator
。 [#557] -
geb.navigator.NonEmptyNavigator
已重命名为geb.navigator.DefaultNavigator
。 [#557] -
JUnit 3 支持已停用。 [#532]
-
更新到 Groovy 2.5.6。 [#534]
-
Groovy 2.3 的支持已移除。 [#560]
-
现在默认情况下,报告仅在测试失败时生成,而不是在每次测试后生成。 [#527]
-
当使用 geb-browserstack Gradle 插件时,BrowserStackLocal 的代理设置、隧道标识符和强制所有流量通过本地机器现在在不同的块中配置。 [#573]
-
更新到 Spock 1.3,放弃对 Spock 1.0 的支持。 [#581]
2.2
新功能
-
使在重新加载页面时等待某些内容变得更方便。 [#499]
-
添加了
waitCondition
内容模板选项。 [#342] -
添加了禁用 BrowserStack 和 SauceLabs 的 Gradle 插件中使用隧道的功能。 [#384]
-
在
Browser
类中添加了pause()
方法,作为调试时设置断点的替代方案。 [#247] -
添加了在运行时访问使用 DSL 定义的内容名称的功能。 [#369]
-
添加了配置内容 DSL 模板选项默认值的功能。 [#369]
-
添加了配置传递给
withWindow()
和withNewWindow()
的选项默认值的功能。 [#406] -
为
TemplateDerivedPageContent
和PageContentContainer
添加了源信息。 [#446] -
添加了改进的 Web 存储支持,包括测试集成中的管理。 [#472]
1.0
重大更改
-
geb.testng.GebTest
和geb.testng.GebReportingTest
已移除,它们在 0.13.0 中已弃用。 -
Navigator
的isDisabled()
、isEnabled()
、isReadOnly()
和isEditable()
方法已移除,它们在 0.12.0 中已弃用。 -
内容 DSL 中宽松类型的
module()
和moduleList()
方法已移除,它们在 0.12.0 中已弃用。
0.13.0
改进
-
报告文件名中的非 ASCII 单词字符不再被替换。 [#399]
-
将 TestNG 支持更改为基于特性。 [#412]
-
添加
Navigator.moduleList()
方法作为内容 DSL 中已弃用的moduleList()
方法的替代方案。 [#402] -
添加了使用 Geb 与 Selendroid 及其他基于 Selenium 的框架测试非 Web 应用程序的支持。 [#320]
-
改进
Browser.clearCookies()
的文档,说明清除的具体内容,并添加了一个辅助方法,用于跨多个域删除 cookie。 [#159] -
不要依赖 UndefinedAtCheckerException 进行流程控制。 [#368]
-
文档说明
Navigator.text()
仅在元素可见时才返回元素的文本。 [#403] -
使
interact()
的实现更少动态。 [#190] -
改进
interact()
的文档。 [#207] -
设置文本输入值时,不要不必要地多次请求标签名和类型属性。 [#417]
-
提高内容元素字符串表示的有用性。 [#274]
弃用
-
geb.testng.GebTest
和geb.testng.GebReportingTest
已弃用,取而代之的是geb.testng.GebTestTrait
和geb.testng.GebReportingTestTrait
。
重大更改
-
Geb 现在使用 Groovy 2.4.5 和 Spock 1.0-groovy-2.4 构建。
-
以下
Navigator
方法在多元素Navigator
上调用时会抛出SingleElementNavigatorOnlyMethodException
:hasClass(java.lang.String)
、is(java.lang.String)
、isDisplayed()
、isDisabled()
、isEnabled()
、isReadOnly()
、isEditable()
、tag()
、text()
、getAttribute(java.lang.String)
、attr(java.lang.String)
、classes()
、value()
、click()
、getHeight()
、getWidth()
、getX()
、getY()
、css(java.lang.String)
、isFocused()
。 [#284]
0.12.0
新功能
-
支持导航到页面实例以及类。 [#310]
-
支持将页面实例用作窗口切换方法的
page
选项值。 [#352] -
支持将页面实例与帧切换方法一起使用。 [#354]
-
支持将页面实例与
Navigator.click()
方法一起使用。 [#355] -
支持将页面实例和页面实例列表用作内容模板的
page
选项值。 [#356] -
新的
Navigator.module(Class<? extends Module>)
和Navigable.module(Class<? extends Module>)
。 [#312] -
新的
Navigable.module(Module)
和Navigable.module(Module)
。 [#311] -
支持在模块中使用
interact {}
块。 [#364] -
支持页面级别的
atCheckWaiting
配置。 [#287] -
Navigator
元素现在也可以使用hasNot()
方法进行过滤。 [#370] -
已为实现
Navigator
的类添加了equals()
和hashCode()
方法的自定义实现。 [#374] -
支持为 BrowserStack 隧道设置
forcelocal
标志。 [#385] -
添加配置选项,用于在
WaitTimeoutException
的消息中包含原因字符串表示。 [#386]
修复
-
改进了当导航器不包含表单元素时从 Navigator.isDisabled() 和 Navigator.isReadOnly() 抛出的消息。 [#345]
-
当禁用隐式断言时,Browser.verifyAtIfPresent() 在 at checker 返回 false 时应该失败。 [#357]
-
当意外页面配置不是包含扩展
Page
的类的集合时,提供更好的错误报告。 [#270] -
当创建报告时且驱动程序的截图方法返回 null 时,不要失败。 [#292]
-
可以定义内容的类不应该从
propertyMissing()
抛出自定义异常。 [#367] -
现在,传递给
withFrame()
方法的页面的“at 检查器”已验证。 [#358]
重大更改
-
Page.toString()
现在返回完整的页面类名而不是其简单名称。 -
当在页面或模块上找不到给定名称的内容时,会抛出
MissingPropertyException
而不是UnresolvablePropertyException
。 -
Geb 现在使用 Groovy 2.3.10 和 Spock 1.0-groovy-2.3 构建。
弃用
-
内容 DSL 中可用的
module(Class<? extends Module>, Navigator base)
已弃用,取而代之的是Navigator.module(Class<? extends Module>)
,并将在 Geb 的未来版本中移除。 -
内容 DSL 中可用的
module(Class<? extends Module>, Map args)
已弃用,取而代之的是Navigable.module(Module)
,并将在 Geb 的未来版本中移除。 -
内容 DSL 中可用的
module(Class<? extends Module>, Navigator base, Map args)
已弃用,取而代之的是Navigator.module(Module)
,并将在 Geb 的未来版本中移除。 -
内容 DSL 中可用的所有
moduleList()
方法变体已弃用,取而代之的是使用Navigator.module()
方法以及collect()
调用,并将在 Geb 的未来版本中移除,请参阅关于使用模块重复内容的章节中的示例 [#362] -
Navigator
的isDisabled()
、isEnabled()
、isReadOnly()
和isEditable()
方法已弃用,并将在 Geb 的未来版本中移除。这些方法现在在新的FormElement
模块类上可用。
项目相关更改
-
用户邮件列表已移至 Google Groups。
-
《Geb之书》已迁移到 Asciidoctor,示例已变为可执行。 [#350]
0.10.0
新功能
修复
-
允许从模块的内容块访问模块属性。 [#245]
-
支持为返回大写标签名的 WebDriver 实现设置元素。 [#318]
-
使用本机二进制文件运行 BrowserStack 隧道。 [#326]
-
更新 BrowserStack 支持以使用隧道版本 3.1 中引入的命令行参数。 [#332]
-
修复使用 groovy 脚本支持的配置时发生的 PermGen 内存泄漏。 [#335]
-
如果 at 检查等待已启用且超时,则
Browser.isAt()
不会失败。 [#337] -
文档示例中传递给
aliases
内容选项的值应为 String [#338] -
在 Navigator 上添加了具有所有
find()
签名的方法$()
。 [#321] -
geb-saucelabs
插件现在使用 SauceConnect 的本机版本。 [#341] -
不要修改传递给 “Navigator.find(Map<String, Object>, String)” 的谓词映射。 [#339]
-
使用 JUnit 和 Geb 实现的功能测试在 Grails 2.3+ 中运行两次。 [#314]
-
在手册中提及可以从何处下载快照工件。 [#305]
-
文档说明
withNewWindow()
和withWindow()
将页面切换回原始页面。 [#279] -
修复文档中用于操作复选框的选择器。 [#268]
0.9.3
重大更改
-
移除了 easyb 支持。 [#277]
-
现在,当使用快捷方式获取基于控件名称的导航器且返回的导航器为空时,会抛出
MissingMethodException
。 [#239] -
使用 SauceLabs 集成时,
allSauceTests
任务已重命名为allSauceLabsTests
-
使用 SauceLabs 集成时,
geb.sauce.browser
系统属性已重命名为geb.saucelabs.browser
-
Module
现在实现Navigator
而不是Navigable
,因此可以在其上调用Navigator
的方法,而无需先调用$()
来获取模块的基础Navigator
。
0.9.2
重大更改
-
如果
isDisabled()
在EmptyNavigator
或包含除按钮、输入、选项、选择或文本区域以外的任何内容的Navigator
上调用,则现在会抛出UnsupportedOperationException
。 -
如果
isReadOnly()
在EmptyNavigator
或包含除输入或文本区域以外的任何内容的Navigator
上调用,则现在会抛出UnsupportedOperationException
。
0.9.1
重大更改
-
如果页面对象未定义 at 检查器,显式调用带有页面对象的
at()
将抛出UndefinedAtCheckerException
,而不是静默通过。 -
将没有 at 检查器的页面传递给
click(List<Class<? extends Page>>)
或作为to
模板选项中的页面之一将抛出UndefinedAtCheckerException
。
修复
-
containsWord()
和iContainsWord()
现在在匹配包含空格的文本时返回预期结果 [#254] -
已将
has(Map<String, Object> predicates, String selector)
和has(Map<String, Object> predicates)
添加到 Navigator,以与find()
和filter()
保持一致 [#256] -
当页面验证在
click()
调用后失败时,也捕获 WaitTimeoutException [#255] -
已将
not(Map<String, Object> predicates, String selector)
和not(Map<String, Object> predicates)
添加到 Navigator,以与find()
和filter()
保持一致 [#257] -
确保对于驱动程序实现无法获取当前 URL 而未事先将浏览器驱动到 URL 的不正确情况,不抛出
NullPointerException
[#291]
0.9.0
新功能
-
新的
via()
方法,其行为与to()
之前的行为相同 - 它在浏览器上设置页面,并且不验证该页面的 at checker [#249]。 -
现在可以通过指定自定义
NavigatorFactory
来提供自己的Navigator
实现,更多信息请参阅本手册章节 [#96]。 -
withFrame()
方法的新变体,允许一键切换到帧上下文并更改页面,并在调用后自动将其改回原始页面,请参阅手册中的[同时切换页面和帧][switch-frame-and-page] [#213]。 -
wait
、page
和close
选项可以传递给withNewWindow()
调用,更多信息请参阅本手册章节 [#167]。 -
改进了 UnresolvablePropertyException 的消息,以包含忘记导入类的提示 [#240]。
-
改进了
Browser.at()
和Browser.to()
的签名,使其返回被断言为位于/导航到的页面的确切类型。 -
可以注册
ReportingListener
对象以观察报告(参见:本手册章节)
修复
-
修复了即使超时前最后一次评估返回真值,waitFor 也会抛出 WaitTimeoutException 的问题 [#215]。
-
修复了当浏览器不在 HTML 页面(例如 XML 文件)上时,为报告截图的问题 [#126]。
-
返回
(wait: true, required: false)
内容的最后评估值,而不是在超时过期时始终返回 null 并抛出WaitTimeoutException
。 [#216]。 -
isAt()
的行为与at()
相同,如果其 at checker 成功,则会将浏览器的页面实例更新为给定的页面类型。 [#227]。 -
select
元素的处理已重构,效率更高 [#229]。 -
模块支持使用 @attributeName 符号访问基本属性的值 [#237]。
-
支持在模块基本定义中使用文本匹配器。 [#241]。
-
已更新文本区域的读取,以便返回文本字段的当前值,而不是初始值。 [#174]。
重大更改
-
to(Class<? extends Page>)
方法现在在一个方法调用中更改浏览器上的页面并验证该页面的 at checker [#1],[#249];如果需要旧行为,请使用via()
-
Navigator
上的getAttribute(String)
现在对于不存在的布尔属性返回null
。 -
Browser
上的at()
和to()
方法如果成功则返回页面实例,via()
方法总是返回页面实例 [#217]。 -
不带页面参数的
withFrame()
调用现在在调用后将浏览器页面更改回调用前的页面。 [#222]。 -
由于性能改进,在创建新的
Navigator
时不再删除重复元素;如果需要,可以使用Navigator
上的新unique()
方法来删除重复项 [#223]。 -
Browser
上的at(Page)
和isAt(Page)
方法已移除,因为它们与 API 的其余部分不一致。 [#242]。 -
Navigator 的
click(Class<? extends Page>)
和to:
内容选项现在在切换到新页面后验证页面,以与to(Class<? extends Page>)
的新行为保持一致。 [#250]。 -
在所有驱动程序中,读取未在导航器上设置的属性现在返回一个空字符串。 [#251]。
0.7.0
新功能
-
在
moduleList()
方法中添加了对索引和范围的支持 -
表单控件快捷方式现在也在页面和模块内容上工作
-
waitFor()
调用的自定义超时消息 -
导航器也可以由内容组成
-
传递给
waitFor()
调用的闭包表达式现在被转换,以便其中的每个语句都被断言 - 这提供了更好的waitFor()
超时报告。 -
页面类的
at
闭包属性现在被转换,以便其中的每个语句都被断言 - 这提供了更好的失败 at 检查报告 -
浏览器上新的
isAt()
方法,其行为与at()
以前的行为相同,即,如果 at 检查失败,则不抛出 AssertionError,而是返回false
-
withAlert()
和withConfirm()
现在接受wait
选项,并且可能的值与等待内容的值相同
重大更改
-
click()
现在指示浏览器仅点击导航器匹配的第一个元素 -
所有
click()
方法变体都返回接收者 -
当超时过期时,
required: false, wait: true
的内容定义返回null
并且不抛出WaitTimeoutException
-
传递给
waitFor()
调用的闭包表达式中不再允许赋值语句 -
如果 at 检查失败,
at()
现在会抛出 AssertionException,而不是返回 false
0.6.2
新功能
-
新的
interact()
函数用于鼠标和键盘操作,它委托给 WebDriver Actions 类 -
新的
moduleList()
函数用于重复内容 -
新的
withFrame()
方法用于处理帧 -
新的
withWindow()
和withNewWindow()
方法用于处理多个窗口 -
向浏览器添加了
getCurrentWindow()
和getAvailableWindows()
方法,它们委托给底层驱动程序实例 -
内容别名现在可以使用内容 DSL 中的
aliases
参数 -
如果找不到配置脚本,将使用配置类(如果存在)——这在您使用 IDE 中的 Geb 运行测试时非常有用
-
驱动程序现在在整个 JVM 中缓存,这在某些情况下避免了浏览器启动成本
-
添加了配置选项以禁用在 JVM 关闭时退出缓存的浏览器
重大更改
-
Page.convertToPath()
函数现在负责在需要时添加前缀斜杠(即它不会在Page.getPageUrl()
中隐式添加)[GEB-139]。 -
未选中的复选框现在将其值报告为
false
而不是 null
0.6.1
新功能
-
兼容至少 Selenium 2.9.0(Geb 0.6.0 版本不适用于 Selenium 2.5.0 及更高版本)
-
尝试将选择设置为不包含的值现在会引发异常
-
等待算法现在是基于时间的,而不是基于重试次数的,这对于不是即时执行的块更好
-
更好地支持使用已实例化的页面
重大更改
-
使用 Geb 中的
<select>
元素现在需要显式依赖额外的 WebDriver jar(请参阅安装部分以获取更多信息) -
Navigator
的classes()
方法现在返回一个List
(而不是Set
),并保证按字母顺序排序
0.6
新功能
-
selenium-common 现在是 Geb 的“provided”范围依赖项
-
单选按钮可以通过其标签文本和值属性进行选择。
-
选择选项可以通过其文本和值属性进行选择。
-
当未找到属性时,
Navigator.getAttribute
返回null
而不是空字符串。 -
Navigator
上的jquery
属性现在返回在其上调用的 jQuery 方法所返回的任何内容。 -
所有 waitFor 子句现在都将条件中引发的异常视为评估失败,而不是传播异常
-
内容可以定义为
wait: true
,以便 Geb 在请求时隐式等待它 -
现在为所有实现
TakesScreenshot
接口的驱动程序(几乎所有)在报告时截取屏幕截图 -
添加了
BindingUpdater
类,可以管理 Groovy 脚本绑定以供 Geb 使用 -
向浏览器添加了
quit()
和close()
方法,它们委托给底层驱动程序实例 -
geb.Browser.drive()
方法现在返回使用的Browser
实例 -
Navigator 的底层 WebElements 现在可以检索
-
添加了 $() 方法,该方法接受一个或多个 Navigator 或 WebElement 对象,并返回一个由这些对象组成的新 Navigator
-
添加了直接下载 API,可用于直接将内容(PDF、CSV 等)下载到 Geb 程序中(不通过浏览器)
-
引入了新的配置机制,用于更灵活和环境敏感的 Geb 配置(例如驱动程序实现、基本 URL)
-
默认等待超时和重试间隔现在可配置,并且现在还可以使用用户配置预设(例如快速、慢速)
-
添加了“构建适配器”机制,使构建系统更容易控制相关配置
-
JUnit 3 集成现在在自动生成的报告中包含测试方法名称
-
报告支持已重写,使其在测试之外使用起来更加友好
-
添加了 TestNG 支持(由 Alexander Zolotov 贡献)
-
为导航器对象和模块添加了
height
、width
、x
和y
属性
重大更改
-
将最低 Groovy 版本提高到 1.7
-
所有失败的 waitFor 子句现在都抛出
geb.waiting.WaitTimeoutException
而不是AssertionError
-
将 WebDriver 的最低版本要求升级到 2.0rc1
-
onLoad()
和onUnload()
页面方法的返回类型都已从def
更改为void
-
Grails 特定的测试子类已被移除。请改用直接等效项(例如
geb.spock.GebReportingSpec
而不是grails.plugin.geb.GebSpec
) -
Grails 插件不再依赖于测试集成模块,您需要手动依赖您想要的模块
-
测试子类中的
getBaseUrl()
方法已移除,请使用配置机制 -
没有值的输入现在将其值报告为空字符串而不是
null
-
未启用多选的选择元素不再将其值报告为 1 元素列表,而是报告为选定元素的值(如果没有选择,则返回
null
)
0.5.1
-
修复了编译不正确的规范和 geb grails 模块的问题
0.5
新功能
-
Navigator 对象现在实现了 Groovy 真值(空 == false,非空 == true)
-
引入了“js”短符号
-
添加了“easyb”支持(
geb-easyb
)和 Grails 支持 -
通过
geb.PageChangeListener
支持页面更改监听 -
添加了
waitFor()
方法,使处理动态页面更容易 -
支持
alert()
和confirm()
对话框 -
添加了 jQuery 集成
-
报告集成类(例如 GebReportingSpec)如果使用 FirefoxDriver,现在会保存屏幕截图
-
为导航器对象添加了
displayed
属性,用于确定可见性 -
添加了
find
作为$
的别名(例如find("div.section")
) -
Browser 对象现在实现了
page(List<Class>)
方法,该方法将页面设置为第一个其 at-checker 匹配页面的类型 -
接受一个或多个页面类的 click() 方法现在可在
Navigator
对象上使用 -
添加了页面生命周期方法
onLoad()
/onUnload()
重大更改
-
drive()
块中引发的异常不再用DriveException
包装 -
at(Class pageClass)
方法不再要求现有页面实例属于该类(如果给定类型匹配,页面将更新)
0.4
首次公开发布