Node.js
selenium-webdriver

selenium-webdriver(node版)でclosest要素を取得する

何がしたいか。

Element.closest的なことをseleniumでやりたい時の対応

ul.books
  li
    .book
      .name
        a href="http://path/to/buy" 罪と罰
      .outline
        p ほげほげほげほげ
        p ほげほげほげ
        a href="http://path/to/more" もっと読む
  li
    .book
      .name
        a href="http://path/to/buy" ダヴィンチ・コード
      .outline
        p ほげほげほげほげ
        p ほげほげほげ
        a href="http://path/to/more" もっと読む
  li
    .book
      .name
        a href="http://path/to/buy" 白鯨
      .outline
        p ほげほげほげほげ
        p ほげほげほげ
        a href="http://path/to/more" もっと読む

みたいな要素があってダヴィンチ・コードの[もっと読む]をクリックしたい時とかに

let anchor = await driver.findElement(By.partialLinkText('ダヴィンチ・コード'));
await anchor.closest('.book').findElement(By.partialLinkText('もっと読む')).click();

みたいなことをしたいけどWebElement.prototype.closestなんてものはないのでできない

方法

selenium-webdriverのNode版のLocatorにはBy.jsとかいうのがある。
これを使うと、与えたscriptをexecuteScriptでブラウザ側で実行して、取得したDOMをWebElementとして扱える。
なので、executeScriptで与えるscript内でネイティブのElement.closestすればよい。

実装例

By.jsの第一引数はクライアント側で実行するscript, 第二引数以降はscriptに渡す引数となる。
第二引数でダヴィンチ・コードのリンクを渡して、それをscriptが拾ってclosestで要素をとればclosest要素がとれる。

let bookAnchor = await driver.findElement({partialLinkText: 'ダヴィンチ・コード'});
let book = await driver.findElement(
  By.js(
    el => el.closest('.book'),
    bookAnchor
  )
);
await book.findElement({partialLinkText: 'もっと読む'}).click();

もし実行対象のブラウザがElement.closestに対応してない場合はscript内でparentNodeをたどって対象のdomになるまで探せば良い。

do {
  if (el.matches && el.matches(selector)) {
    return el;
  }
} while (el = el.parentNode)
// なければ例外
throw new Error('closest target element is not found');

おまけ By.jsについて

あんまりドキュメントに詳しく使い方書かれてないけど実装みるとBy.jsはこんな感じの実装になってる

selenium-webdriver/lib/by.js
static js(script, var_args) {
  let args = Array.prototype.slice.call(arguments, 0);
  return function(driver) {
    return driver.executeScript.apply(driver, args);
  };
}

locator実行時に第一引数をdriverとして受け取って、executeScriptしてる。
ちなみにexecuteScriptWebDriver.prototypeにはあるけどWebElement.prototypeにはない。
findElement系では現在のcontextをlocatorに渡すのでBy.jsを与えるのはWebElement.prototype.findElementではなくてWebDriver.prototype.findElementでなければいけないのでその点は注意が必要。