最近のselenium-webdriverの話


こんにちは、[@nazomikan](https://twitter.com/nazomikan)です。
この記事は[LIFULL Advent Calender2017 その2](https://qiita.com/advent-calendar/2017/lifull2)の3日目の記事です。

昨年のAdvent Calenderで[selenium-webdriver(node)のapi翻訳記事](https://qiita.com/nazomikan/items/40b86dc5619bb1795aaa)を書きましたが、その当時対象としてた2系から現在はメジャーバージョンアップを挟んで色々と現状が変わってきてるのでその辺の話をします。


## ローカルモードでのテスト時にselenium standaloneサーバが不要に

v2.43のチェンジセットでFireFoxのネイティブサポートが追加されて、remoteでの実行時を除いてサポート対象の全てのブラウザがstandaloneサーバなしで実行できるようになりました。
(chrome/phantomjsのネイティブサポートは2.34)

※のちにphantomjsとoperaのサポートは切られる
※この当時サポート対象でなかった各ブラウザはそれぞれ以下のタイミングでサポートされる

* 2.52: edge
* 2.45: ie/opera+26/safari
* 3.1.1: firefox nightly/safari technical preview

```js:スタンドアロンサーバの立ち上げとbuilder.usingServerが不要に
driver = new webdriver.Builder()
  // .usingServer('http://localhost:4444/wd/hub')
  .withCapabilities(webdriver.Capabilities.chrome())
  .build();

```

## PromiseManagerの廃止の流れ

もとよりselenium-webdriverは非同期なAPIとして設計されており、全てのコマンドはPromiseで返却されます。
ただ、それをそのまま使うと簡単な作業ですら非常に難解に表現してしまうという問題がありました。

たとえば非常に単純なgoogle検索を実行するコードを見て見ましょう。

```js:google検索をするコード
let driver = new Builder().forBrowser('firefox').build();
driver.get('http://www.google.com/ncr')
    .then(_ => driver.findElement(By.name('q')))
    .then(q => q.sendKeys('webdriver'))
    .then(_ => driver.findElement(By.name('btnG')))
    .then(btnG => btnG.click())
    .then(_ => driver.wait(until.titleIs('webdriver - Google Search'), 1000))
    .then(_ => driver.quit(), e => {
      console.error(e);
      driver.quit();
    });
```

どうでしょう、読みやすいとは言えませんよね。
処理の流れは明示的ですが一個一個が何をしてるのか読み取りづらいものになっています。

これをもっとシンプルにするためにselenium-webdriverは当初よりPromiseManager(いわゆるcontrolFlow)というコマンドの実行を内部的にトラックする概念を導入していました。

これにより上記のコードは以下のように表現できます。

```js:PromiseManagerを利用した実装
let driver = new Builder().forBrowser('firefox').build();
driver.get('http://www.google.com/ncr');
driver.findElement(By.name('q')).sendKeys('webdriver');
driver.findElement(By.name('btnG')).click();
driver.wait(until.titleIs('webdriver - Google Search'), 1000);
driver.quit();
```

このPromiseManagerの導入により、Selenium-WebDriverの内部コードこそ複雑性を増したが、それを利用して書くコードは、より意味を理解しやすく、シンプルなものになりました。

しかし、JavaScript自身が進化してasyc/awaitが利用できるようになった今、このPromiseManagerがなかったとしてもPromiseの持つそのままの性質だけで以下のように書くことができます。

```js:async/awaitを利用した実装
async function doGoogleSearch() {
  let driver = new Builder().forBrowser('firefox').build();
  await driver.get('http://www.google.com/ncr');
  await driver.findElement(By.name('q')).sendKeys('webdriver');
  await driver.findElement(By.name('btnG')).click();
  await driver.wait(until.titleIs('webdriver - Google Search'), 1000);
  await driver.quit();
}

doGoogleSearch()
    .then(_ => console.log('SUCCESS!'), e => console.error('FAILURE: ' + e));
```

JavaScriptの標準の機能を利用するだけでもこれだけシンプルに記述できるようになった今、内部の複雑性と戦いながらPromiseManagerを残す必要があるのかという議論になり、PromiseManagerを廃止し、NativeなPromiseの実装に置き換えていく流れが起きました。

とはいえ急激な変化はユーザーを混乱させるため順を追った対応となりました。

### 廃止スケジュール

#### ver3.0

SELENIUM_PROMISE_MANAGERフラグが実装され、これを0にするとNative Promiseで実装されたSchedulerが利用されます。

#### 2017/10月頃 (Node8系)
async/awaitを実装したNodeのLTSのリリースに伴ってSELENIUM_PROMISE_MANAGERのデフォルト値を0にします。

#### 2018/10 (Node10系)

PromiseManagerの完全廃止。 ControlFlowクラスと関連するコードはすべて削除されます。

[該当issue](https://github.com/SeleniumHQ/selenium/issues/2969)

## Promise準拠

上記のながれでPromiseの実装をNativeに置き換えるため、段階的に標準準拠を行うことになりました。

v3.0.0の時点でpromise.Deferredがthenableオブジェクトではなくなり、この辺もdeprecatedになった

* Deferred#cancel()
* Deferred#catch()
* Deferred#finally()
* Deferred#isPending()
* Deferred#then()
* Promise#thenCatch()
* Promise#thenFinally()

v3.0.1でも準拠のための微調整が入りPromise.fullfilled/rejectedも非推奨となりresolve/rejectを使うことが推奨された

v3.1.0でpromise.fullfilled/rejected/deferがPromise利用フラグ(SELENIUM_PROMISE_MANAGER)を0にしてるときにnativeのPromiseを返却するようにしました。

[差分](https://github.com/SeleniumHQ/selenium/commit/195cf614db9736db63829e85ad5342b1aa7b62f2#diff-08a6a8d91f29fca8cebfc8671e4ffc3d)



## 追加されたAPI

### Until系

* until.urlIs(url)

そのなの通りurlが与えられたものになるまで待機する処理
内部的にはgetCurrentUrlの結果と与えられたものを比較する条件式

* until.urlContains(substrUrl)

与えられたurlの部分文字列を含むurlになるまで待機する処理
上記同様にgetCurrentUrlの結果をindexOfでマッチさせてる

* until.urlMatches(regex)

与えれた正規表現にマッチするurlになるまで待機する処理
上記同様にgetCurrentUrlの結果をRegex.testでマッチさせてる


## 削除されたAPI

* WebDriver#isElementPresent()
* WebElement#isElementPresent()

他の言語のdriverとの整合性を図るためisElementPresent系は削除して、従来からあるfindElementsでがんばれとのこと

```js:findElements
driver.findElements(By.id('foo'))
  .then(found => console.log('Element found? %s', !!found.length));
```

* WebElement#getInnerHtml()
* WebElement#getOuterHtml()

getInnerHtml/getOuterHtmlも他の言語との整合性を図るため廃止されました。
多少強引な話のように見えるけどexecuteScriptでがんばれとのこと

```js
let el = driver.findElement(By.id("foo"));

function getIInnerHtml(el) {
  return driver.executeScript("return arguments[0].innerHTML;", el);
}

function getOuterHtml(el) {
  return driver.executeScript("return arguments[0].outerHTML;", el);
}
```

* WebElement#getRawId()


## 廃止予定

WebDriver#timeouts
  * W3C WebDriver準拠のOptions#getTimeouts/setTimeoutsを使うことを推奨


## まとめ

最近の特に大きな流れはPromiseManagerの廃止(予定)でしょうか。
ESの発展に伴い色々なモジュールが新しい進化を遂げようとしているのを感じますね。

今回はこんな感じでした。
よいseleniumライフを。。。