Intern Creating Custom Commands
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.
page revision: 1, last edited: 28 Feb 2017 22:29