Intern Creating Custom Commands

intern

http://albertogasparin.it/articles/2015/04/extend-intern-leadfoot-commands/

// Intern - Creating custom commands:

StxCommand.js:

define([
  // path to the native leadfoot/Command
  // It's a node module (common js format), and this format is not directly 
  // compatible with AMD so we use dojo/node! plug-in to load it
  'intern/dojo/node!leadfoot/Command',

  // method we want to add to the command
  './newMethod'

], function(Command, newMethod) {
  'use strict';

  // inheritance as recommended according to the docs
  function StxCommand() {
    Command.apply(this, arguments);
  }

  StxCommand.prototype = Object.create(Command.prototype);
  StxCommand.prototype.constructor = StxCommand;

  // adding new method
  StxCommand.prototype.newMethod = newMethod;

  // return extended command from the module
  return StxCommand;

});

newMethod.js:

define([], function() {
  'use strict';

  // boilerplate for new methods
  return function(someParameter) { // method signature
    return new this.constructor(this, function() {
      this.parent
        // actual method code
        .then(function() {
          console.log(someParameter);
        });
    });
  };
});

Using our custom commands:

define([
  // base
  'intern/chai!assert',
  'require',
  'path/relative/to/this/file/StxCommand',

  // resources
  'path/relative/to/this/file/resources'

], function(
  // base
  assert, require, StxCommand,

  // resources
  resources

  ) {
    'use strict';
    // return the test function
    return function() {
      var stxRemote = new StxCommand(this.remote.session);
      return stxRemote.newMethod('done!'); 
    }
});

The above code does not look like what we typically see in a test suite.  
Perhaps, it looks more like code inside a test case.

Because StxCommand inherits from leadfoot/Command, it also contains an implicit 
promise.

In the above code, we create an instance of StxCommand, passing it the 
this.remote.session object.  The result, stxRemote object, is equivalent to 
the this.remote object.

For each custom command, we must follow a few best practices.  We can implement 
a simple timer and an error management block that will prevent the tests from 
hanging for too long.

Explicit promise is used to give us more freedom to leadfoot/Command usage.  
This is what we will return from the test function and it is what will act as 
a semaphore for the test suite.  When the promise is fulfilled, the test is 
marked as completed.  If the promised is resolved, the test is considered as 
successful.  If the promise is rejected, the test will fail.

The test suite does not know about errors thrown from the code.  If that 
happens, the test will simply hang until the timeout is reached.  The only way 
to communicate errors to the test suite is to reject the returned promise.  
When we use leadfoot/Command built-in promise, this is done automatically.  
When we do not use leadfoot/Command built-in promise, we must do it manually in 
the catch block.

define([], function() {
  'use strict';
  return function(someParameter) {
    return new this.constructor(this, function() {
      var promise = new Deferred();
      try {
        this.parent
          .getTimeout('implicit')
          .then(function(timeout) {
            if (! timeout) {
              timeout = 5000;
            }
            setTimeout(function() {
              promise.reject('methodName timed out.');
            }, timeout);
          })
          // actual method logic
          .then(function() {
            console.log('new method');
          })
          .then(function() {
            promise.resolve(true);
          })
          .catch(function(error) {
            promise.reject(error);
          })
      } catch (error) {
        promise.reject(error);
      }
      return promise;
    });
  }
});

In the above code, we use this.parent.getTimeout to checks if a timeout has been 
set but the native api.  If a timeout have not been set, we give it our own 
default value.  Regardless, of whether a timeout has been set, we use setTimeout 
to reject the promise if the promise was not resolved by the time the timeout 
expired.

Always return the explicit promise regardless of what happens.

In order to use the Command class, you first need to pass it a leadfoot/Session 
instance for it to use:

var command = new Command(session);

Once you have created the Command, you can then start chaining methods, and 
they will execute in order one after another:

command.get('http://example.com')
  .findByTagName('h1')
  .getVisibleText()
  .then(function (text) {
      assert.strictEqual(text, 'Example Domain');
  });

Because these operations are asynchronous, you need to use a then callback in 
order to retrieve the value from the last method. Command objects are Thenables, 
which means that they can be used with any Promises/A+ or ES6-conformant 
Promises implementation.  though there are some specific differences in the 
arguments and context that are provided to callbacks.

The then method is compatible with the Promise#then API, with two important 
differences:

1. The context (this) of the callback is set to the Command object, rather than 
   being undefined. This allows promise helpers to be created that can retrieve 
   the appropriate session and element contexts for execution.

2. A second non-standard setContext argument is passed to the callback. This 
   setContext function can be called at any time before the callback fulfills 
   its return value and expects either a single leadfoot/Element or an array of 
   Elements to be provided as its only argument. The provided element(s) will 
   be used as the context for subsequent element method invocations (click, 
   etc.). If the setContext method is not called, the element context from the 
   parent will be passed through unmodified.

Each call on a Command generates a new Command object, which means that certain 
operations can be parallelised:

command = command.get('http://example.com');
Promise.all([
  command.getPageTitle(),
  command.findByTagName('h1').getVisibleText()
]).then(function (results) {
  assert.strictEqual(results[0], results[1]);
});

In this example, the commands on line 3 and 4 both depend upon the get call c
ompleting successfully but are otherwise independent of each other and so 
execute here in parallel.

Command objects actually encapsulate two different types of interaction: session 
interactions, which operate against the entire browser session, and element 
interactions, which operate against specific elements taken from the currently 
loaded page. Things like navigating the browser, moving the mouse cursor, and 
executing scripts are session interactions; things like getting text displayed 
on the page, typing into form fields, and getting element attributes are element 
interactions.

Session interactions can be performed at any time, from any Command. On the 
other hand, to perform element interactions, you first need to retrieve one or 
more elements to interact with. This can be done using any of the find or 
findAll methods, by the getActiveElement method, or by returning elements from 
execute or executeAsync calls. The retrieved elements are stored internally as 
the element context of all chained Commands. When an element method is called on 
a chained Command with a single element context, the result will be returned 
as-is:

command = command.get('http://example.com')
  // finds one element -> single element context
  .findByTagName('h1')
  .getVisibleText()
  .then(function (text) {
    // `text` is the text from the element context
    assert.strictEqual(text, 'Example Domain');
  });

When an element method is called on a chained Command with a multiple element 
context, the result will be returned as an array:

command = command.get('http://example.com')
  // finds multiple elements -> multiple element context
  .findAllByTagName('p')
  .getVisibleText()
  .then(function (texts) {
    // `texts` is an array of text from each of the `p` elements
    assert.deepEqual(texts, [
      'This domain is established to be used for […]',
      'More information...'
    ]);
  });

The find and findAll methods are special and change their behaviour based on the 
current element filtering state of a given command. If a command has been 
filtered by element, the find and findAll commands will only find elements 
within the currently filtered set of elements. Otherwise, they will find 
elements throughout the page.

Some method names, like click, are identical for both Session and Element APIs; 
in this case, the element APIs are suffixed with the word Element in order to 
identify them uniquely.

Commands can be subclassed in order to add additional functionality without 
making direct modifications to the default Command prototype that might break 
other parts of the system:

function CustomCommand() {
  Command.apply(this, arguments);
}
CustomCommand.prototype = Object.create(Command.prototype);
CustomCommand.prototype.constructor = CustomCommand;
CustomCommand.prototype.login = function (username, password) {
  return new this.constructor(this, function () {
    return this.parent
      .findById('username')
        .click()
        .type(username)
        .end()
      .findById('password')
        .click()
        .type(password)
        .end()
      .findById('login')
        .click()
        .end();
  });
};

Note that returning this, or a command chain starting from this, from a 
callback or command initialiser will deadlock the Command, as it waits for 
itself to settle before settling.

Command is different from Element, so your last method in the queue needs to 
return a Command instance (if you use find() then add end() before returning) or 
your code will fail.
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License