カスタムコマンドの定義
概要
ほとんどの場合、Nightwatchコマンドをアプリケーションのニーズに合わせて拡張する必要があります。そのためには、新しいフォルダ(例:nightwatch/commands
)を作成し、その中に独自のコマンドをそれぞれのファイルで定義します。
次に、nightwatch.json
ファイルで、そのフォルダへのパスをcustom_commands_path
プロパティとして指定します。
{
"custom_commands_path" : "nightwatch/commands"
}
コマンド名はファイル名自体です。
カスタムコマンドを定義する
カスタムコマンドを定義する方法は主に2つあります。
1)クラス形式のコマンド
これはカスタムコマンドを記述する推奨スタイルであり、Nightwatch独自のコマンドのほとんどもこのように記述されています。コマンドモジュールは、コマンド関数を表すcommand
インスタンスメソッドを持つクラスコンストラクターをエクスポートする必要があります。
Nightwatchのすべてのコマンドは非同期であるため、カスタムコマンドは完了を(command
メソッドで)通知する必要があります。これは2つの方法で実現できます。
Promise
を返す- "complete"イベントを発行する(この場合、クラスはNodeの
EventEmitter
から継承する必要がある)
Promise
を返すのが推奨される方法です。クラスベースのcommand
メソッドは、クラスインスタンスのコンテキスト(this
の値)で実行されます。browser
オブジェクトはthis.api
を通じて利用できます。
以下の例は、.pause()
コマンドと同等の処理を実行します。完了に関する2つのバリエーションに注目してください。
戻り値
また、戻り値を指定することもできます。これは、Promiseが解決される引数として、または"complete"イベント呼び出しへの引数として指定します。
Promiseによる完了
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を返すため、値を返すだけで済みます。
別の例を次に示します。
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"イベントによる完了
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マッピングです。
次に例を示します。
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のように追加のエンドポイントを提供するサービスを使用する場合に役立ちます。
次に例を示します。
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()
呼び出しでラップすることもできます。execute
やperform
などのクライアントコマンドは、this
を介して利用できます。
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)を定義しています。
このコマンドを使用すると、テストは次のようになります。
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');
}
};