概要

ほとんどの場合、Nightwatchコマンドをアプリケーションのニーズに合わせて拡張する必要があります。そのためには、新しいフォルダ(例:nightwatch/commands)を作成し、その中に独自のコマンドをそれぞれのファイルで定義します。

次に、nightwatch.jsonファイルで、そのフォルダへのパスをcustom_commands_pathプロパティとして指定します。

nightwatch.json
{
  "custom_commands_path" : "nightwatch/commands"
}

コマンド名はファイル名自体です。

カスタムコマンドを定義する

カスタムコマンドを定義する方法は主に2つあります。

1)クラス形式のコマンド

これはカスタムコマンドを記述する推奨スタイルであり、Nightwatch独自のコマンドのほとんどもこのように記述されています。コマンドモジュールは、コマンド関数を表すcommandインスタンスメソッドを持つクラスコンストラクターをエクスポートする必要があります。

Nightwatchのすべてのコマンドは非同期であるため、カスタムコマンドは完了を(commandメソッドで)通知する必要があります。これは2つの方法で実現できます。

  1. Promiseを返す
  2. "complete"イベントを発行する(この場合、クラスはNodeのEventEmitterから継承する必要がある)

Promiseを返すのが推奨される方法です。クラスベースのcommandメソッドは、クラスインスタンスのコンテキスト(thisの値)で実行されます。browserオブジェクトはthis.apiを通じて利用できます。

以下の例は、.pause()コマンドと同等の処理を実行します。完了に関する2つのバリエーションに注目してください。

戻り値

また、戻り値を指定することもできます。これは、Promiseが解決される引数として、または"complete"イベント呼び出しへの引数として指定します。

Promiseによる完了
nightwatch/commands/customPause.js
module.exports = class CustomPause {
  command(ms, cb) {
    // If we don't pass the milliseconds, the client will
    // be suspended indefinitely
    if (!ms) {
      return;
    }
    
const returnValue = { value: 'something' };
return new Promise((resolve) => { setTimeout(() => { // if we have a callback, call it right before the complete event if (cb) { cb.call(this.api); }
resolve(returnValue); }, ms); }); } }

commandメソッドはasyncにすることもできます。この場合、asyncメソッドはすでにPromiseを返すため、値を返すだけで済みます。

別の例を次に示します。

nightwatch/commands/customCommand.js
module.exports = class CustomCommand {
  async command() {
    let returnValue;
    try {
      returnValue = await anotherAsyncFunction();
    } catch (err) {
      console.error('An error occurred', err);
      returnValue = {
        status: -1,
        error: err.message
      }
    }
    
return returnValue; } }
"complete"イベントによる完了
nightwatch/commands/customPause.js
const Events = require('events');

module.exports = class CustomPause extends Events { command(ms, cb) { // If we don't pass the milliseconds, the client will // be suspended indefinitely if (!ms) { return; }
const returnValue = { value: 'something' };
setTimeout(() => { // if we have a callback, call it right before the complete event if (cb) { cb.call(this.api); }
// This also works: this.complete(returnValue) this.emit('complete', returnValue); }, ms); } }

Nightwatchプロトコルアクションの使用

v1.4以降では、Nightwatchが独自の組み込みAPIで使用しているプロトコルアクション(this.transportActionsを介して)を直接使用することもできます。これらは、現在使用中のプロトコルに応じて、Selenium JsonWireまたはW3C Webdriverプロトコルエンドポイントへの直接HTTPマッピングです。

次に例を示します。

nightwatch/commands/customCommand.js
module.exports = class CustomCommand {
  async command() {
    let returnValue;
    
// list all the avaialble transport actions // console.log(this.transportActions);
try { returnValue = await this.transportActions.getCurrentUrl(); } catch (err) { console.error('An error occurred', err); returnValue = { status: -1, error: err.message } }
return returnValue; } }

Selenium/Webdriverエンドポイントを直接呼び出す

また、v1.4以降では、カスタムコマンドから(this.httpRequest(options)を介して)Selenium/Webdriverサーバーで利用可能なHTTPエンドポイントを直接呼び出すことができます。これは、他のプロトコルアクションと同じHTTPリクエストインターフェースを使用しているため、提供されたAPIプロトコルを拡張するのに便利な方法です。

特に、Appiumのように追加のエンドポイントを提供するサービスを使用する場合に役立ちます。

次に例を示します。

nightwatch/commands/customCommand.js
module.exports = class CustomCommand {
  async command() {
    let returnValue;
    
try { returnValue = await this.httpRequest({ // the pathname of the endpoint to call path: '/session/:sessionId/url',
// the current Selenium/Webdriver sessionId sessionId: this.api.sessionId,
// host and port are normally not necessary, since it is the current Webdriver hostname/port //host: '', //port: '',
// the body of the request data: { url: 'http://localhost/test_url' },
method: 'POST' }); } catch (err) { console.error('An error occurred', err); returnValue = { status: -1, error: err.message } }
return returnValue; } }

2)関数形式のコマンド

これは、コマンドを定義できるより単純な形式ですが、非常に制限もあります。

コマンドモジュールは、少なくとも1つのNightwatch APIメソッド(.execute()など)を呼び出す必要があるcommand関数をエクスポートする必要があります。これは、コマンドの非同期キューイングシステムの動作の制限によるものです。すべてを.perform()呼び出しでラップすることもできます。executeperformなどのクライアントコマンドは、thisを介して利用できます。

nightwatch/commands/customCommand.js
module.exports.command = function(file, callback) {
  var self = this;
  var imageData;
  var fs = require('fs');
  
try { var originalData = fs.readFileSync(file); var base64Image = new Buffer(originalData, 'binary').toString('base64'); imageData = 'data:image/jpeg;base64,' + base64Image; } catch (err) { console.log(err); throw "Unable to open file: " + file; }
this.execute(function(data) { // execute application specific code App.resizePicture(data); return true; }, [imageData], // arguments array to be passed function(result) { if (typeof callback === "function") { callback.call(self, result); } });
return this; };

上記の例では、data-URIとして画像ファイルをロードし、アプリケーション内で定義されたresizePictureというメソッドを(.execute()を介して)呼び出すコマンド(例:resizePicture.js)を定義しています。

このコマンドを使用すると、テストは次のようになります。

tests/sampleTest.js
module.exports = {
  "testing resize picture" : function (browser) {
    browser
      .url("http://app.host")
      .waitForElementVisible("body")
      .resizePicture("../../../var/www/pics/moon.jpg")
      .assert.element(".container .picture-large")
      .end();
  }
};

async/awaitの使用

関数形式のカスタムコマンド内でES6のasync/await構文を使用することもできます。別のカスタムコマンドの例を次に示します。

module.exports = {
  command: async function () {
    this.url('https://nightwatch.dokyumento.jp');
    this.waitForElementVisible('section#index-container');
    
const result = await this.elements('css selector', '#index-container ul.features li'); this.assert.strictEqual(result.value.length, 7, 'Feature elements number is correct'); } };