ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

WebdriverIO を使い始めるときのハマりどころ (geckodriver を添えて)

WebDriverFirefox を操作するために Node.js 用の WebDriver バインディングである WebdriverIO を使ってみました。

webdriver.io

使ってみると意外とハマりどころがあってちゃんと使い始めるまでに時間がかかったので、自分がはまったところを書き残しておきます。

そもそも WebDriver とは?

という方は、昔私が書いた下記の記事をご覧ください。

自分の環境

私は 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 の指定が効いていない。) 他のオプションも試してみて、結論としては hostportpath といったオプションで、接続先を指定できた。

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 では、要素の選択に使用できるセレクタに種類がある。

12.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"
WebDriver

しかし、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 の罠でした。 多分ここら辺にハマっておけば後はいい感じに使えるはず。

それでは、よき自動化人生を!