WebDriverJS
WebDriver 的 JavaScript 语言绑定。本文包含以下内容:
- 介绍
- 快速上手
- 在 Node 中运行
- 在浏览器中运行
- 设计细节
- 管理异步 API
- 同服务端通讯
- /xdrpc
- 未来计划
介绍
WebDriver 的 JavaScript 绑定(WebDriverJS),可以使 JavaScript 开发人员避免上下文切换的开销,并且可以让他们使用和项目开发代码一样的语言来编写测试。WebDriverJS 既可以在服务端运行,例如 Node,也可以在浏览器中运行。
警告: WebDriverJS 要求开发者习惯异步编程。对于那些 JavaScript 新手来说可能会发现 WebDriverJS 有点难上手。
快速上手
在 Node 中运行
虽然 WebDriverJS 可以在 Node 中运行,但它至今还没有实现本地驱动的支持(也就是说,你的测试必须使用一个远程的 WebDriver 服务)。并且,你必须编译 Selenium 服务端,将其添加到 WebDriverJS 模块。进入 Selenium 客户端的根目录,执行:
$ ./go selenium-server-standalone //javascript/node:webdriver
当两个目标都被编译好以后,启动服务和 Node,开始编写测试代码:
$ java -jar build/java/server/src/org/openqa/grid/selenium/selenium-standalone.jar &
$ node
var webdriver = require('./build/javascript/node/webdriver');
var driver = new webdriver.Builder().
usingServer('http://localhost:4444/wd/hub').
withCapabilities({
'browserName': 'chrome',
'version': '',
'platform': 'ANY',
'javascriptEnabled': true
}).
build();
driver.get('http://www.google.com');
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver');
driver.findElement(webdriver.By.name('btnG')).click();
driver.getTitle().then(function(title) {
require('assert').equal('webdriver - Google Search', title);
});
driver.quit();
在浏览器中运行
除了 Node,WebDriverJS 也可以直接在浏览器中运行。编译比Node方式少很多依赖的浏览器模块,运行:
$ ./go //javascript/webdriver:webdriver
为了和可能不在同一个域下的 WebDriver 的服务端进行通信,客户端使用的是修改过的 JsonWireProtocol 和 cross-origin resource sharing:
<!DOCTYPE html>
<script src="webdriver.js"></script>
<script>
var client = new webdriver.http.CorsClient('http://localhost:4444/wd/hub');
var executor = new webdriver.http.Executor(client);
// 启动一个新浏览器,这个浏览器可以被这段脚本控制
var driver = webdriver.WebDriver.createSession(executor, {
'browserName': 'chrome',
'version': '',
'platform': 'ANY',
'javascriptEnabled': true
});
driver.get('http://www.google.com');
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver');
driver.findElement(webdriver.By.name('btnG')).click();
driver.getTitle().then(function(title) {
if (title !== 'webdriver - Google Search') {
throw new Error(
'Expected "webdriver - Google Search", but was "' + title + '"');
}
});
driver.quit();
</script>
控制宿主浏览器
启动一个浏览器运行 WebDriver 来测试另一个浏览器看起来比较冗余(相比在 Node 中运行而言)。但是,使用 WebDriverJS 在浏览器中运行自动化测试是浏览器真实在跑这些脚本的。这只要服务端的 url 和浏览器的 session id 是已知的就可以实现。这些值可能会直接传递给 builder,它们也可以通过从页面 url 的查询字符串中解析出来的 wdurl 和 wdsid 定义 。
<!-- Assuming HTML URL is /test.html?wdurl=http://localhost:4444/wd/hub&wdsid=foo1234 -->
<!DOCTYPE html>
<script src="webdriver.js"></script>
<input id="input" type="text"/>
<script>
// Attaches to the server and session controlling this browser.
var driver = new webdriver.Builder().build();
var input = driver.findElement(webdriver.By.tagName('input'));
input.sendKeys('foo bar baz').then(function() {
assertEquals('foo bar baz',
document.getElementById('input').value);
});
</script>
警告
在浏览器中使用 WebDriverJS 有几个需要注意的地方。首先,webdriver.Builder 类只能用于已存在的 session。为了获得一个新的 session,你必须像上面的例子那样手工创建。其次,有一些命令可能会影响运行 WebDriverJS 脚本的页面。
- webdriver.WebDriver#quit: quit 命令将终止整个浏览器进程,包括在运行 WebDriverJS 的窗口。除非你确定要这样做,否则不要使用这个命令。
- webdriver.WebDriver#get: WebDriver 的接口被设计为尽量接近用户的操作。这意味着无论 WebDriver 客户端当前聚焦在哪个帧,导航命令(如:driver.get(url))总是指向最高层的帧。在操作宿主浏览器时,WebDriverJS 脚本可以通过使用 .get 命令导航离开当前页面,而当前页面仍然获得焦点。 如果要自动操作一个宿主浏览器但仍想在页面间跳转,请把WebDriver客户端的焦点设在另一个窗口上(这和Selenium RC 的多窗口模式的概念非常相似):
<!DOCTYPE html>
<script src="webdriver.js"></script>
<script>
var testWindow = window.open('', 'slave');
var driver = new webdriver.Builder().build();
driver.switchTo().window('slave');
driver.get('http://www.google.com');
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver');
driver.findElement(webdriver.By.name('btnG')).click();
</script>
调试 Tests
你可以使用 WebDriver 的服务来调试在浏览器中使用 WebDriverJS 运行的测试。
$ ./go selenium-server-standalone
$ java -jar \
-Dwebdriver.server.session.timeout=0 \
build/java/server/src/org/openqa/grid/selenium/selenium-standalone.jar &
启动服务后,访问 WebDriver 的控制面板: http://localhost:4444/wd/hub。你可以使用这个控制面板查看,创建或者删除 sessions。选择一个要调试的 session 后,点击 “load script” 按钮。在弹出的对话框中,输入你的 WebDriverJS 测试的地址:服务端将在你的浏览器中打开这个页面,这个页面的 url 含有额外的参数用于 WebDriverJS 客户端和服务端的通讯。
支持的浏览器
- IE 8+
- Firefox 4+
- Chrome 12+
- Opera 12.0a+
- Android 4.0+
设计细节
管理异步 API
不同于其他那些提供了阻塞式 API 的语言绑定,WebDriverJS 完全是异步的。为了追踪每个命令的执行状态, WebDriverJS 对 “promise” 进行了扩展。promise 是一个这样的对象,它包含了在未来某一点可用的一个值。JavaScript 有几个 promise 的实现,WebDriverJS 的 promise 是基于 CommonJS 的 Promise/A 提议,它定义了 promise 是任意对象上的 then 函数属性。
/**
* Registers listeners for when this instance is resolved.
*
* @param {?function(*)} callback The function to call if this promise is
* successfully resolved. The function should expect a single argument: the
* promise's resolved value.
* @param {?function(*)=} opt_errback The function to call if this promise is
* rejected. The function should expect a single argument: the failure
* reason. While this argument is typically an {@code Error}, any type is
* permissible.
* @return {!Promise} A new promise which will be resolved
* with the result of the invoked callback.
*/
Promise.prototype.then = function(callback, opt_errback) {
};
通过使用 promises,你可以将一连串的异步操作连接起来,确保每个操作执行时,它之前的操作都已经完成:
var driver = new webdriver.Builder().build();
driver.get('http://www.google.com').then(function() {
return driver.findElement(webdriver.By.name('q')).then(function(searchBox){
return searchBox.sendKeys('webdriver').then(function() {
return driver.findElement(webdriver.By.name('btnG')).then(function(submitButton) {
return submitButton.click().then(function() {
return driver.getTitle().then(function(title) {
assertEquals('webdriver - Google Search', title);
});
});
});
});
});
});
不幸的是,上述范例非常冗长,难以辨别测试的意图。为了提供一套不降低测试可读性的干净利落的异步操作 API, WebDriverJS 引入了一个 promise “管理器” 来调度和执行所有的命令。
简言之,promise 管理器处理用户自定义任务的调度和执行。管理器保存了一个任务调度的列表,当列表中的某个任务执行完毕后,依次执行下一个任务。如果一个任务返回了一个 promise,管理器将把它当做一个回调注册,在这个 promise 完成后恢复其运行。WebDriver 将自动使用管理器,所以用户不需要使用链式调用。因此,之前的 google 搜索的例子可以简化成:
var driver = new webdriver.Builder().build();
driver.get('http://www.google.com');
var searchBox = driver.findElement(webdriver.By.name('q'));
searchBox.sendKeys('webdriver');
var submitButton = driver.findElement(webdriver.By.name('btnG'));
submitButton.click();
driver.getTitle().then(function(title) {
assertEquals('webdriver - Google Search', title);
});
On Frames and Callbacks
就内部而言,promise 管理器保存了一个调用栈。在管理器执行循环的每一圈,它将从最顶层帧的队列中取一个任务来执行。任何被包含在之前命令的回调中的命令将被排列在一个新帧中,以确保它们能在所有早先排列的任务之前运行。这样做的结果是,如果你的测试是 written-in line,所有的回调都使用函数字面量定义,命令将按照它们在屏幕上出现的垂直顺序来执行。例如,考虑以下 WebDriverJS 测试用例:
driver.get(MY_APP_URL);
driver.getTitle().then(function(title) {
if (title === 'Login page') {
driver.findElement(webdriver.By.id('user')).sendKeys('bugs');
driver.findElement(webdriver.By.id('pw')).sendKeys('bunny');
driver.findElement(webdriver.By.id('login')).click();
}
});
driver.findElement(webdriver.By.id('userPreferences')).click();
这个测试用例可以使用 WebDriver 的 Java API 重写如下:
driver.get(MY_APP_URL);
if ("Login Page".equals(driver.getTitle())) {
driver.findElement(By.id("user")).sendKeys("bugs");
driver.findElement(By.id("pw")).sendKeys("bunny");
driver.findElement(By.id("login")).click();
}
driver.findElement(By.id("userPreferences")).click();
错误处理
既然所有 WebDriverJS 的操作都是异步执行的,我们就不能使用 try-catch 语句。取而代之的是,你必须为所有命令的 promise 返回注册一个错误处理的函数。这个错误处理函数可以抛出一个错误,在这种情况下,它将被传递给链中的下一个错误处理,或者他将返回一个不同的值来抑制这个错误并切换回回调处理链。
如果错误处理器没有正确的处理被拒绝的 promise(不只是哪些来自于 WebDriver 命令的),则这个错误会传播至错误处理链的父级帧。如果一个错误没有被抑制而传播到了顶层帧,promise 管理器要么触发一个 uncaughtException 事件(如果有注册监听的话),或者将错误抛给全局错误处理器。在这两种情况下,promise 管理器都将抛弃所有队列中后续的命令。
// 注册一个事件监听未处理的错误
webdriver.promise.Application.
getInstance().
on('uncaughtException', function(e) {
console.error('There was an uncaught exception: ' + e.message);
});
driver.switchTo().window('foo').then(null, function(e) {
// 忽略 NoSuchWindow 错误,让其他类型的错误继续向上冒泡
if (e.code !== bot.ErrorCode.NO_SUCH_WINDOW) {
throw e;
}
});
// 如果上面的错误不被抑制的话,这句将永远不会执行
driver.getTitle();
同服务端通讯
当在服务端环境中运行时,客户端不受安全沙箱的约束,可以简单的发送 http 请求(例如:node 的 http.ClientRequest)。当在浏览器端运行时,WebDriverJS 客户端就会收到同源策略的约束。为了和可能不在同一个域下的服务端通讯,WebDriverJS 客户端使用的是修改过的 JsonWireProtocol 和 cross-origin resource sharing。
Cross-Origin Resource Sharing
如果一个浏览器支持 cross-origin resource sharing (CORS), WebDriverJS 将使用 cross-origin XMLHttpRequests (XDR) 发送命令给服务端。服务端要想支持 XDR,就需要响应 preflight 请求,并带有合适的 access-control 头。
Access-Control-Origin: *
Access-Control-Allow-Methods: DELETE,GET,HEAD,POST
Access-Control-Allow-Headers: Accept,Content-Type
在编写本文时,已有 Firefox 4+, Chrome 12+, Safari 4+, Mobile Safari 3.2+, Android 2.1+, Opera 12.0a, 和 IE8+ 支持 CORS。不幸的是,这些浏览器的实现并不一致,也不是完全都遵循 W3C 的规范。
- IE 的 XDomainRequest 对象,比其 XMLHttpRequest 对象的功能要弱。XDomainRequest 只能发送哪些标准的 form 表单可以发送的请求。这限制了 IE 只能发送 get 和 post 请求(wire 协议要求支持 delete 请求)。
- WebKit 的 CORS 实现禁止了跨域请求的重定向,即使 access-control 头被正确设置了也是如此。
- 如果返回一个服务端错误(4xx 或 5xx),IE 和 Opera 的实现将触发 XDomainRequest/XMLHttpRequest 对象的错误处理,但是拿不到服务端返回的信息。这使得它们无法处理以标准的 JSON 格式返回的错误信息。
为了弥补这些短处,当在浏览器中运行时,WebDriverJS 将使用修改过的 JsonWireProtocol 和通过 /xdrpc 路由所有的命令。
/xdrpc
POST /xdrpc
作为命令的代理,所有命令相关的内容必须被编码成 JSON 格式。命令的执行结果将在 HTTP 200 响应中作为一个标准的响应结果返回。客户端依赖于响应的转台吗以确认命令是否执行成功。
参数:
- method - {string} http 方法
- path - {string} 命令路径
- data - {Object} JSON 格式的命令参数
返回:
{*} 命令执行的结果。
举个例子,考虑以下 /xdrpc 命令:
POST /xdrpc HTTP/1.1
Accept: application/json
Content-Type: application/json
Content-Length: 94
{"method":"POST","path":"/session/123/element/0a/element","data":{"using":"id","value":"foo"}}
服务端将编码这个命令并重新分发:
POST /session/123/element/0a/element HTTP/1.1
Accept: application/json
Content-Type: application/json
Content-Length: 28
{"using":"id","value":"foo"}
不管是否成功,命令的执行结果都将作为一个标准的 JSON 返回:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 60
{"status":7,"value":{"message":"Unable to locate element."}}
未来计划
以下是一些预期要做的事情。但什么时候完成,在现在仍然未知。如果你有兴趣参与开发,请加入 [email protected]。当然,这是一个开源软件,你完全不需要等待我们。如果你有好主意,就马上开工吧:)
- 使用 AutomationAtoms 实现一个纯 JavaScript 的命令执行器。这将允许开发者使用 js 编写非常轻量的测试代码,并且可以运行在任何服浏览器中(当然,仍然会收到同源策略的限制)。
- 基于扩展实现一个 SafariDriver。
- 为 Node 提供本地浏览器支持,而不需要通过 WebDriver Server 运行。