概述

大多数情况下,您需要扩展 Nightwatch 命令以适应您自己的应用程序需求。为此,创建一个新文件夹(例如 nightwatch/commands)并在其中开始定义您自己的命令,每个命令都在其自己的文件中。

然后在 nightwatch.json 文件中指定该文件夹的路径,作为 custom_commands_path 属性。

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

命令名称是文件本身的名称。

定义自定义命令

您可以通过两种主要方式定义自定义命令

1) 类样式命令

这是编写自定义命令的推荐样式,也是 Nightwatch 自身大多数命令的编写方式。您的命令模块需要导出一个带有一个 command 实例方法的类构造函数,该方法代表命令函数。

所有 Nightwatch 命令都是异步的,这意味着自定义命令必须发出完成信号(在 command 方法中)。这可以通过两种方式实现

  1. 返回一个 Promise
  2. 发出 "complete" 事件(在这种情况下,该类需要继承自 Node 的 EventEmitter

返回一个 Promise 是推荐的方式。基于类的 command 方法在类的实例的上下文中(this 的值)执行。browser 对象可通过 this.api 获取。

以下示例执行与 .pause() 命令等效的操作。请注意有关完成的两种变体。

返回值

您还可以指定一个返回值,作为 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)。这些是直接 HTTP 映射到 Selenium JsonWireW3C Webdriver 协议端点,具体取决于当前使用哪个协议。

以下是一个示例

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 端点。这可能是扩展提供的 API 协议的一种便捷方式,因为它与其他协议操作使用相同的 HTTP 请求 接口。

当使用提供其他端点的服务时,这将特别有用,例如 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: 'https://127.0.0.1/test_url'
        },
        
        method: 'POST'
      });
    } catch (err) {
      console.error('An error occurred', err);
      returnValue = {
        status: -1,
        error: err.message
      }
    }
    
    return returnValue;
  }
}

2. 函数样式命令

这是一种更简单的命令定义形式,但它们也很有限。

命令模块需要导出一个 command 函数,该函数需要至少调用一个 Nightwatch api 方法(例如 .execute())。这是由于命令的异步队列系统的工作原理的限制。您也可以将所有内容包装在 .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;
};

上面的示例定义了一个命令(例如 resizePicture.js),它将图像文件加载为 data-URI 并调用一个名为 resizePicture(通过 .execute())的方法,该方法在应用程序中定义。

使用此命令,测试将类似于

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.node.org.cn');
    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');
  }
};