# 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](https://code.google.com/p/selenium/wiki/JsonWireProtocol) 和 [cross-origin resource sharing](https://code.google.com/p/selenium/wiki/WebDriverJs#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](http://www.google.com/url?q=http%3A%2F%2Fwiki.commonjs.org%2Fwiki%2FPromises%2FA&sa=D&sntz=1&usg=AFQjCNGC0NMXO-81exam-S5HjTuOxaV_mw) 提議,它定義了 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."}}
## 未來計劃
以下是一些預期要做的事情。但什么時候完成,在現在仍然未知。如果你有興趣參與開發,請加入 selenium-developers@googlegroups.com。當然,這是一個開源軟件,你完全不需要等待我們。如果你有好主意,就馬上開工吧:)
- 使用 AutomationAtoms 實現一個純 JavaScript 的命令執行器。這將允許開發者使用 js 編寫非常輕量的測試代碼,并且可以運行在任何服瀏覽器中(當然,仍然會收到同源策略的限制)。
- 基于擴展實現一個 SafariDriver。
- 為 Node 提供本地瀏覽器支持,而不需要通過 WebDriver Server 運行。