NightmareでE2Eテストをするときに役立った独自アクションのメモ

アドベントカレンダー ソフトウェアテスト6日目担当の @ks888 です。

ソフトウェアテスト Advent Calendar 2016 - Qiita

最近はNightmareでE2Eテストを書くことが増えてきました。Nightmareを使うと、例えば以下のような感じでテストを書けます。

var Nightmare = require('nightmare');
var expect = require('chai').expect; // jshint ignore:line

describe('test yahoo search results', function() {
  it('should find the nightmare github link first', function(done) {
    var nightmare = Nightmare()
    nightmare
      .goto('http://yahoo.com')
      .type('form[action*="/search"] [name=p]', 'github nightmare')
      .click('form[action*="/search"] [type=submit]')
      .wait('#main')
      .evaluate(function () {
        return document.querySelector('#main .layoutMiddle a').href
      })
      .end()
      .then(function(link) {
        console.log(link);
        expect(link).to.equal('https://github.com/segmentio/nightmare');
        done();
      })
  });
});

コード中で出てくるclickとかwaitとかはNightmareではアクションと呼ばれています。2016年12月時点では以下のようなアクションが用意されています(各アクションの意味は公式のREADMEを参考にして下さい)。

engineVersions
title
url
visible
exists
click
mousedown
mouseover
type
insert
check
uncheck
select
back
forward
refresh
wait
evaluate
inject
viewport
useragent
scrollTo
screenshot
html
pdf
cookies.get
cookies.set
cookies.clear
cookies.clearAll
authentication

アクションは自作することもできて、いくつか追加しておくと、テストを書くのが更に捗ります。今回はメモがてら、これまでに作った独自アクションを残しておきます。

HTTPヘッダーを付与する(addHeaderアクション)

var Nightmare = require('nightmare');

// アクションを追加する
Nightmare.action('addHeader',
  function (name, options, parent, win, renderer, done) {
    parent.respondTo('addHeader', function (headerKey, headerValue, done) {
      win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
        details.requestHeaders[headerKey] = headerValue
        callback({cancel: false, requestHeaders: details.requestHeaders})
      })
      done()
    })
    done()
  }, function (headerKey, headerValue, done) {
    this.child.call('addHeader', headerKey, headerValue, done)
  })

// 利用例
var nightmare = Nightmare()
nightmare
  .addHeader('Key', 'Value')
  .goto('http://localhost:8080')
  .end()
  .then()

任意のHTTPヘッダーを付与するアクションです。公式でheader()というメソッドが既にあるのですが、header()で指定したヘッダーはgoto()で指定したページをロードするときしか使われず、JS等で動的にロードする場合には使われません。addHeader()はJS等で動的にロードする場合にも指定したヘッダーが付きます。

ちなみにaddHeader()を複数回呼んでも、最後の呼び出しで指定したHeaderしか付きません。ちょっといけてないですが、今のところ困ってないので。。

特定URLへのアクセスを遮断する(blockAccessアクション)

var Nightmare = require('nightmare');

// アクションを追加する
Nightmare.action('blockAccess',
  function (name, options, parent, win, renderer, done) {
    parent.respondTo('blockAccess', function (blockUrls, done) {
      win.webContents.session.webRequest.onBeforeRequest({ urls: blockUrls }, (details, callback) => {
        callback({cancel: true})
      })
      done()
    })
    done()
  }, function (blockUrls, done) {
    this.child.call('blockAccess', blockUrls, done)
  })


// 利用例
var nightmare = Nightmare()
nightmare
  .blockAccess(['http://localhost:8080/*'])
  .goto('http://localhost:8080')
  .end()
  .then()

指定したパターンにマッチしたURLへのアクセスを全てキャンセルします。 例えばGoogleAnalyticsサーバへのアクセスをキャンセルすると、E2EテストがGoogleAnalyticsのデータに影響を与えるのを防ぐことができます。

条件付きでクリックする(clickIfアクション)

var Nightmare = require('nightmare');

// アクションを追加する
Nightmare.action('clickIf', function (condFn, selector, done) {
  var clickIfFn
  eval(
    "clickIfFn = function() {" +
    "  if (" + condFn + "()) {" +
    "    document.activeElement.blur();" +
    "    var element = document.querySelector('" + selector + "');" +
    "    if (!element) {" +
    "      throw new Error('unable to find element by selector: " + selector + "');" +
    "    }" +
    "    var event = document.createEvent('MouseEvent');" +
    "    event.initEvent('click', true, true);" +
    "    element.dispatchEvent(event);" +
    "  }" +
    "}"
  );
  this.evaluate_now(clickIfFn, done)
})

// 利用例
var nightmare = Nightmare()
nightmare
  .goto('https://github.com/')
  .clickIf(function() { return document.querySelector('#user-links') === null }, '.site-header-actions > a')
  .end()
  .then()

第一引数で与えた関数がtrueを返した場合のみ、第二引数が指定する要素をクリックします。関数はElectron内で実行されます。evaluate()click()を組み合わせても同じことができますが、こちらの方がテストの可読性が高くなるかと思っています。

独自アクションを書く上で参考になる情報

自分でアクションを書く際には、以下のリンクが役立ちました。

参考になれば幸いです。新しいアクションを書いたら、また追記します。