yuitest-functional

A Y.Test.Case subclass useful for functional tests, typically with arrow.
Problem
Functional tests require a lot of waiting around in multiple steps of a single scenario. This can quickly get cumbersome
when using yuitest because of all the nested wait and resume calls riddled with custom timeouts that do not scale
from a maintenance viewpoint.
Also, functional tests require some semblance of sanity with respect to how input events are simulated. This module aims
to address this issue as well with a set of functions that can help with consistent simulation of inputs and a cleaner
abstract API for this.
Solution
Think of every functional test as an asynchronous, callback-driven test that has exactly one wait and
resume. The meat of the test case executes a series of asynchronous functions chained together with callbacks
in the manner of async.series. The timeout for the test case is automatically calculated as the sum
of the worst-case timeouts for every individual step.
A simple example
YUI.add("my-functional-test-module", function (Y) {
Y.Test.Runner.add(new Y.Test.FunctionalTestCase({
"should test an ajax page with appropriate waits": function () {
this.batch().
debug('Starting test').
waitAndClick('#start-button').
waitForElement("#overlay").
setValue("#overlay-title", "Foo bar").
setValue("#overlay-description", "My giant foobar").
click("#overlay-save").
wait(2000).
add(function assertions() {
Y.Assert.areEqual("Foo bar", Y.one("ul.list li .name").get("innerHTML"));
Y.Assert.areEqual("My giant foobar", Y.one("ul.list li.desc").get("innerHTML"));
}).
run();
}
}));
}, "0.1", {requires: [ "functional-test-case" ]});
Under the covers
Every sugar function found under this.batch() is a generator that returns a function accepting a single callback. The
returned function also has a timeout attribute that represents the worst-case timeout of the step. The run method
accumulates these timeouts and uses the final value as the timeout for the single test.wait call. The test.resume call
simply asserts that no errors were returned by the last step executed.
Since thrown exceptions are turned into callback errors, all exceptions including assertions will be returned to the
resume function for friendly error reporting.
API
batch = this.batch([defaults])
returns a TestBatch object that has a chainable API to add multiple steps, each being a function to be executed.
defaults is an optional object with the following supported properties:
timeout: the default timeout in milliseconds for steps that wait, when an explicit timeout has not been specified (global default is 10 seconds)
shortWait: the default amount of time in milliseconds for waiting after mouse clicks etc. (global default is 100ms)
poll: the default polling interval in milliseconds for steps that poll, when an explicit interval has not been specified (global default is 200ms)
batch.add(f [, millis])
adds an execution step. The first argument may be a function or another Batch to be run.
- When a function has arity 0, it is assumed to be synchronous and wrapped with an async wrapper
- When a function has arity 1, it is assumed to be one that accepts a single callback argument.
The optional millis parameter specifies the expected worst-case runtime of the function in milliseconds. This is
only honored when you pass a function as the first argument. When ths first argument is a Batch, it already has an
expected worst-case runtime associated with it based on the functions that it calls.
batch.createBatch([defaults])
creates a secondary batch with potentially different default configuration to which steps may be added.
Note that this secondary batch is not automatically added as a step for this one. You need to add it explicitly.
batch.run()
runs all the functions added to the batch in series, asynchronously under the wait and resume methods
of the test associated with this batch.
Subsequent method descriptions should be read as adds an asynchronous function that ...
batch.wait([timeout])
waits for timeout milliseconds or for a default interval (typically 10 seconds).
batch.waitUntil(testFn [, timeout [, pollInterval]])
waits up to a maximum of timeout milliseconds, polling every pollInterval milliseconds running the testFn
each time. Returns when the the test function returns true or errors when the timeout is exhausted.
batch.waitForElement(selector [,timeout [, pollInterval]])
waits for the DOM node represented by the selector to be present and visible in the DOM tree and errors when the timeout
is exhausted.
batch.click(selector)
generates a click event on the selector and waits for a small amount of time before returning. Errors when no
node matching the selector is found.
This uses the node-event-simulate YUI module to generate a series of events including mousedown, mouseup etc.
to trigger event handlers that may be listening on these events.
batch.setValue(selector, value)
sets value as value of the node represented by selector and waits for a small amount of time before returning.
Errors when no node matching the selector is found.
This uses the node-event-simulate YUI module to generate a series of events including keydown, keyup etc.
to trigger event handlers that may be listening on these events.
Currently only text input elements are supported by this method.
batch.waitAndClick(selector [, timeout])
combination of waitForElement and click executed in series.
Creating your own sugar functions
Custom sugar functions may be created using the extend method exposed as a class method of the Batch class
available as Y.Test.FunctionalTestCase.Batch
YUI.add('test-helpers', function (Y) {
var Batch = Y.Test.FunctionalTestCase.Batch;
Batch.extend('waitForSuccessfulExecution', function () {
return this.createBatch().
waitForElement('#popup').
waitForElement('#popup .close').
wait(100).
click('#popup .close');
});
}, "0.1", { requires: [ 'functional-test-case' ]});
Now a test method can use this sugar method in its chain as in:
this.batch().
waitAndClick('#some-button').
waitForElement('#overlay').
setValue('#overlay-field', '1000').
waitAndClick('#overlay-submit').
waitForSuccessfulExecution().
run();