Intern commands API, powered by Leadfoot, is powerful and easy to use. However, it’s missing some handful methods. Let’s see how to extend it with custom commands.

The key part is the JS wrapper setup. You need to require the leadfoot/Command module and then extend its prototype with additional methods. Your custom command could be a simple shorthand for a series of other commands, or it can be something more complex.

custom_commands.js
define([
  'intern/chai!assert',
  'require',
  'intern/dojo/node!leadfoot/Command',
], function (assert, require, Command) {

Command.prototype.findAndClickByCssSelector = function (selector) {
  return new this.constructor(this, function () {
    return this.parent
      // example of commands queue
      .findByCssSelector(selector)
        .moveMouseTo()
        .click()
      .end();
  });
};

This is not the ideal solution because we are extending the original Command prototype. However, Intern does not allow you to extend its custom command object, so pay attention to not override Leadfoot/Intern methods. Finally, keep in mind that 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.

Implementing window scrollTo command

Implementation example of window.scrollTo command that animates nicely, like users do.

Command.prototype.scrollWindowToPosition = function (endX, endY, duration) {
  return new this.constructor(this, function () {
    return this.parent
      .setExecuteAsyncTimeout(5000)
      .executeAsync(function (endX, endY, duration, done) {
        var startX = window.pageXOffset,
            startY = window.pageYOffset;

        var maxDelta = Math.max( Math.abs(endX - startX), Math.abs(endY - startY)),
            totalDuration = duration || ~~(Math.pow( maxDelta, 0.54) * 15),
            now = Date.now();

        (function animation () {
          var time = Math.min(1, ((Date.now() - now) / totalDuration)),
              easedTime = (--time) * time * time + 1;
          window.scrollTo(~~(easedTime * (endX - startX)) + startX, ~~(easedTime * (endY - startY)) + startY);
          if(time) { requestAnimationFrame(animation); }
          else { done(null); }
        })();

      }, [endX, endY, duration]);
  });
};

Implementing window scrollIntoView command

Implementation example of window.scrollIntoView command that animates nicely, like users do.

Command.prototype.scrollWindowToCssSelector = function (selector, duration) {
  return new this.constructor(this, function () {
    return this.parent
      .setExecuteAsyncTimeout(5000)
      .executeAsync(function (selector, duration, done) {
        var html = document.documentElement,
            elem = document.querySelector(selector),
            eDim = elem.getBoundingClientRect(),
            startX = window.pageXOffset,
            startY = window.pageYOffset,
            endX = eDim.left + startX + (eDim.left > 0 ? eDim.width - html.clientWidth : 0),
            endY = eDim.top + startY + (eDim.left > 0 ? eDim.height - html.clientHeight : 0);

        var maxDelta = Math.max( Math.abs(endX - startX), Math.abs(endY - startY)),
            totalDuration = duration || ~~(Math.pow(maxDelta, 0.54) * 15),
            now = Date.now();

        (function animation () {
          var time = Math.min(1, ((Date.now() - now) / totalDuration)),
              easedTime = (--time) * time * time + 1;
          window.scrollTo(~~(easedTime * (endX - startX)) + startX, ~~(easedTime * (endY - startY)) + startY);
          if(time) { requestAnimationFrame(animation); }
          else { done(null); }
        })();

      }, [selector, duration]);
  });
};

In both scroll custom commands, the duration can be fixed, passed as an argument, or it can be calculated by the command itself based on the distance from the current scroll position.