WebdriverIO を使い始めるときのハマりどころ (geckodriver を添えて)
WebDriver で Firefox を操作するために Node.js 用の WebDriver バインディングである WebdriverIO を使ってみました。
使ってみると意外とハマりどころがあってちゃんと使い始めるまでに時間がかかったので、自分がはまったところを書き残しておきます。
自分の環境
- OS : Windows 10 Pro 1803 (Spring Creators Update)
- WebDriver リモートエンド
- geckodriver 0.20.1 (Firefox の Marionette に対するプロキシとして WebDriver の API を提供するやつ)
- 操作対象のユーザーエージェント (ブラウザ) : Firefox 60.0.1 (64 ビット)
- 注意点) Selenium Server を経由せず、geckodriver が提供する WebDriver の API を直接使用する。
- クライアント側
- Node.js 8.6.0
- npm 5.3.0
- TypeScript 2.8.3
- WebdriverIO 4.12.0
- @types/webdriverio 4.10.1
私は TypeScript で書いてるので、もともと JS で書かれてたサンプルコード以外は全て TypeScript のコードである。
ハマったところ
クライアントの生成 (接続先の指定)
例などを見ると、クライアントの生成は以下のように書かれているが、詳細なドキュメントが見当たらない。 (どこかにあるかもしれないが見つけられなかった。)
var webdriverio = require('webdriverio'); var options = { desiredCapabilities: { browserName: 'chrome' } }; var client = webdriverio.remote(options);
geckodriver を使う場合に、WebDriver のリモートエンドの URL を指定する方法がわからなかった。 TypeScript の型定義を見ると、オプションに baseUrl
があったので以下のような指定をしてみたが、
import * as wd from "webdriverio"; let wdClient = wd.remote({ baseUrl: "http://localhost:4444" });
残念ながら以下のようなエラーが返ってきた。
Error: POST /wd/hub/session did not match a known command
このエラー内容から察するに、どうやら Selenium Server への接続を想定しているようである。 そしてよくよく調査すると、baseUrl
に何を指定してもホスト 127.0.0.1 の 4444 ポートに対してリクエストを試みているようであった。 (baseUrl
の指定が効いていない。) 他のオプションも試してみて、結論としては host
、port
、path
といったオプションで、接続先を指定できた。
import * as wd from "webdriverio"; let wdClient = wd.remote({ host: "localhost", port: 4444, path: "/" });
非同期処理とエラー処理
サンプルコードを見ると、メソッドチェインで処理を繋げられるようである。
client .init() .url('https://duckduckgo.com/') .setValue('#search_form_input_homepage', 'WebdriverIO') .click('#search_button_homepage') .getTitle().then(function(title) { console.log('Title is: ' + title); // outputs: // "Title is: WebdriverIO (Software) at DuckDuckGo" }) .end();
この例を見てもどういう順序で実行されるのかわからないだろう。
普通に実行するとすべてのメソッドは非同期処理になる。 しかし、WDIO というテストランナー上では同期的に実行される。
Each command documentation usually comes with an example that demonstrates the usage of it using WebdriverIO’s testrunner running its commands synchronously. If you run WebdriverIO in standalone mode you still can use all commands but need to make sure that the execution order is handled properly by chaining the commands and resolving the promise chain.
WebdriverIO - API Docs
初見殺しもいいところである。 ちなみに @types/webdriverio の型定義としては、同じメソッドで非同期的なときの返り値の型と同期的なときの返り値の型の intersection type で定義されてたりする。 難しすぎる。
上のサンプルコード (+ インポート処理やクライアント準備) を、TypeScript の async/await を使ってエラー処理も含めていい感じに書き直すと以下のような感じになる。
import * as wd from "webdriverio"; (async () => { let client = wd.remote({ baseUrl: "http://localhost:4444", path: "/" }); let session = client.init(); try { await session.url('https://duckduckgo.com/'); await session.setValue('#search_form_input_homepage', 'WebdriverIO'); await session.click('#search_button_homepage'); let title = await session.getTitle(); console.log('Title is: ' + title); // outputs: // "Title is: WebdriverIO (Software) at DuckDuckGo" } finally { await session.end(); } })().catch(e => console.error(e));
一度理解してしまえばメソッドチェインで書けるのは便利ではあるが、最初の理解が難しかった。
余談だが、バージョン 3 でコア部分が Ajax/Promise ベースの Monad に書き換えられて、コマンドチェインや Promise の扱いがやりやすくなって今の形の API になったらしい。
Some big changes came along with v3. We’ve rewritten the whole core to an ajax/promise based monad. Instead of implementing a complex command scheduler or request queues we built the whole library on top of a monad construct. This allows us to chain commands as we are used to and keep stacktraces sane. In addition to that we wanted to have 1st level promise support. Therefore we used the Q library (there are already plans to move to native Promises) to integrate promises into the monad system. This works astoundingly well. Each command execution represents a promise. If you chain commands, the command waits until the previous command is resolved. On top of that, the optional modifier that you can pass to a monad makes the library incredibly flexible and extensible.
WebdriverIO - What's new in WebdriverIO?
セレクタについて
WebDriver では、要素の選択に使用できるセレクタに種類がある。
WebDriver12.1 Locator Strategies
An element location strategy is an enumerated attribute deciding what technique should be used to search for elements in the current browsing context. The following table of location strategies lists the keywords and states defined for this attribute:
State Keyword CSS selector "css selector" Link text selector "link text" Partial link text selector "partial link text" Tag name "tag name" XPath selector "xpath"
しかし、WebdriverIO ではその指定ができないようである。 どうやら Sizzle みたいな既存の一般的なセレクタライブラリに近くなるように内部的に使い分けしてくれてるっぽい。
The JsonWireProtocol provides several strategies to query an element. WebdriverIO simplifies these to make it more familiar with the common existing selector libraries like Sizzle.
WebdriverIO - Selectors
具体的にどういうセレクタを書けるのかは WebdriverIO の Selectors のドキュメントに書かれている。
id セレクタを使用しようとする問題
セレクタの話でいうと、geckodriver に対して await session.setValue('#search_form_input_homepage', 'WebdriverIO')
を実行すると以下のようなエラーが返ってくる。
Error: Unknown locator strategy id
WebDriver には id lacator strategy というものはなく、geckodriver には実装されていないのだが、WebdriverIO は id locator strategy を使おうとしてこのエラーが出るようになっているらしい。 回避方法としては 「*#foo」 みたいな感じで ID 以外の条件も含めると良い模様。
WebdriverIO の Issue にもなっているが、「We can't remove the id selector just yet as it is still supported my many other drivers (also on mobile)」 って言って閉じられてる。 CSS セレクタがサポートされてる環境なのなら普通に移行できると思うのだけど、CSS セレクタがサポートされてない環境があるってことなのだろうか……。 (わからん。)
おわり
というわけで私がハマった WebdriverIO の罠でした。 多分ここら辺にハマっておけば後はいい感じに使えるはず。
それでは、よき自動化人生を!