Advanced Concepts
In this section you will learn some advanced Minium concepts that can help you dealing with some of the problems that may arise when creating end-to-end tests for web applications.
Waiting Presets
Sometimes, it is necessary to wait that some element is displayed on the page or not. For instance, a spinning wheel is often displayed to indicate that the application is doing something in background, and therefore you should wait until it disappears.
In Minium Mail sample app, it shows a spinning wheel after you perform some operation.
Let's try to delete an email item and then compose another one:
browser.get("http://minium.vilt.io/sample-app/");
var mailItemCheckbox = $(":checkbox");
var removeBtn = $("#remove-action");
var composeBtn = $("#compose");
mailItemCheckbox.click();
removeBtn.click();
composeBtn.click();
If you run that script all at once (select it all and press Ctrl + Enter
),
you'll notice it will fail when trying to click the Compose
button. The
reason is that the spinning wheel is being displayed and it "blocks" elements
behind the backdrop form being interacted with. So, we need to wait for that
spinning wheel to disappear before we can click the Compose
button. We can do
that with the .waitForUnexistence()
method:
browser.get("http://minium.vilt.io/sample-app/");
var loading = $(".loading").withCss("display", "block");
var mailItemCheckbox = $(":checkbox");
var removeBtn = $("#remove-action");
var composeBtn = $("#compose");
mailItemCheckbox.click();
removeBtn.click();
loading.waitForUnexistence();
composeBtn.click();
The interaction loading.waitForUnexistence()
will wait at most for a specified
amount of time (by default, 5 seconds) that the element doesn't exist. After
that time, it will fail, otherwise, as soon the element disappears, it will
proceed.
However, it is possible that the spinning wheel takes more than 5 seconds to disappear. Remember that, in real world applications, the spinning wheel is normally associated with time-consuming operations that involve AJAX requests.
In those situations where we know that it will probably take more time, we need to ensure it will wait using a different timeout.
Minium Mail lets us configure the loading time and that way we can simulate a time-consuming operation. The following code will change the loading time to be 8 seconds:
browser.get("http://minium.vilt.io/sample-app/");
var configBtn = $("#configure");
var loadingTimeFld = $("#loading-time-seconds");
var saveBtn = $("#config-save");
configBtn.click();
loadingTimeFld.fill("8");
saveBtn.click();
Note that the loading time gets reset every time we refresh the page, so we
won't reload the page using the browser.get(...)
method. If you do, you need
to change the loading time again.
If we now try to run the same interaction code as we were running before:
mailItemCheckbox.click();
removeBtn.click();
loading.waitForUnexistence();
composeBtn.click();
it will fail with a TimeoutException
. That's because
loading.waitForUnexistence()
timeout is 5 seconds and now the spinning wheel
is displayed for 8 seconds.
To fix it, we need to use waiting presets, which are basically labelled timeouts and polling intervals.
The following code creates two waiting presets, fast
and slow
:
// we need to load a module, we'll talk about this later
var timeUnits = require("minium/timeunits");
// browser configuration
browser.configure()
.waitingPreset("fast")
.timeout(1, timeUnits.SECONDS)
.done()
.waitingPreset("slow")
.timeout(10, timeUnits.SECONDS)
.interval(1, timeUnits.SECONDS)
.done();
If we now use slow
waiting preset when calling .waitForUnexistence()
, it
will now wait at most 10 seconds instead of 5 seconds, and that way it will
work:
mailItemCheckbox.click();
removeBtn.click();
loading.waitForUnexistence("slow");
composeBtn.click();
Note: There is a special waiting preset, immediate
, that doesn't wait at
all. Besides, you can use .checkForUnexistence()
/ .checkForExistence()
to
determine if any matching element exists or not without failing:
loading.checkForUnexistence("immediate") // returns true or false immediately
Interaction Listeners
Having to explicitly wait every time a spinning wheel is displayed only adds complexity to the code. Besides, when we explain someone how to use some functionality in a site, we never tell them they need to wait until the spinning wheel disappears, because it is already assumed. The same way, we want Minium to implicitly wait every time it sees a spinning wheel.
Interaction listeners are specially handy for these kind of situations, and also for error handling, like retrying some interaction when it fails. They intercept all interaction calls, and can perform additional logic before, after or when they fail.
ensureExistence / ensureUnexistence
These interaction listeners only allow the interaction to be performed after existence / unexistence or certain elements. In our case, we want to ensure that no spinning wheel is displayed in the page:
var loading = $(".loading").withCss("display", "block");
var loadingUnexistenceListener = minium.interactionListeners
.ensureUnexistence(loading)
.withWaitingPreset("slow");
// browser configuration
browser.configure()
.interactionListeners()
.add(loadingUnexistenceListener)
.done();
We can now run the same code that removes and email and then starts composing
a new mail without having to call loading.waitForUnexistence()
:
browser.get("http://minium.vilt.io/sample-app/");
var mailItemCheckbox = $(":checkbox");
var removeBtn = $("#remove-action");
var composeBtn = $("#compose");
mailItemCheckbox.click();
removeBtn.click();
composeBtn.click();
Error handling
There are situations where some exception can be thrown. For instance, if during
some interaction an alert
window is displayed, an UnhandledAlertException
is
thrown. Of course, we can explicitly handle alert
windows with
$(":root").alert().accept()
, for instance, but if we cannot predict when the
alert window will be displayed, we may need to handle it once it occurs:
// always accepts window alerts
var unhandledAlertListener = minium.interactionListeners
.onUnhandledAlert()
.accept();
// browser configuration
browser.configure()
.interactionListeners()
.add(unhandledAlertListener)
.done();
Another kind of errors that can happen is the StaleElementReferenceException
,
which basically happens when we try to interact with an element that no longer
exists in the page (for instance, if the page was refreshed, which forces that
element to be destroyed in the browser).
In case you start getting these kind of errors, you can register the following interaction listener:
// always accepts window alerts
var staleElementReferenceListener = minium.interactionListeners
.onStaleElementReference()
.thenRetry();
// browser configuration
browser.configure()
.interactionListeners()
.add(staleElementReferenceListener)
.done();
Timeout handling
It is also possible to handle timeout exceptions in a very advanced way. The following example shows an interaction listener that, when a timeout occurs, is triggered, and then checks if an loading element exists in the page. If it exists, it will wait for its unexistence with a provided waiting preset and then it will retry the interaction:
var timeoutListener = minium.interactionListeners
.onTimeout()
.when(loading)
.waitForUnexistence(loading)
.withWaitingPreset("slow")
.thenRetry();
browser.configure()
.interactionListeners()
.add(timeoutListener);
Base Expressions
The concept behing the Base Elements expression is that it should represent the root elements of the UI that can be interacted with.
For instance, when Modal elements are displayed (for instance, a bootstrap
modal dialog) we want base
to evaluate to that modal element, therefore
excluding all elements that are behind the backdrop element.
Then, by using base as the root of our elements expressions, we can get some assurance that we are getting the right elements instead of getting elements that are not interactable (or should not be interactable) at that point.
To demonstrate how useful this pattern can be, let's do a simple exercise: we'll start composing an email and then we'll try to click the first button it finds:
browser.get("http://minium.vilt.io/sample-app/");
// this will open the New message modal dialog
$("#compose").click();
// let's try to click the first available button
$("button").click();
If we try to evaluate that code, it will fail. That's because
$("button").click()
will try to click the first matching button (which is the
Compose
button), and that one is under the modal backdrop and for that reason,
it is not accessible for interactions.
You can try to evaluate $("button")
and you'll see that lots of buttons in
the page will highlight, both the ones that are in the modal dialog and the ones
in the main page, that are not accessible due to the modal backdrop:
So, let's consider the following base expression:
base = $(":root").unless(".modal-backdrop").add(".modal-dialog");
Let's try to explain what it does:
- the first part,
$(":root").unless(".modal-backdrop")
, evaluates the page root element when no modal backdrop exist (that is, when no modal dialog is open) - the second part,
.add(".modal-dialog")
, adds modal dialog elements. Note that, when a modal dialog is available, this second part evaluates into that element, but the first part will evaluate into an empty set because of the existence of the modal backdrop
So, basically that expression evaluates into the root element or into an opened
modal dialog, but never both at the same time. If we try to evaluate base
when a modal dialog is opened:
So, if we use base
as our "root" for finding elements in the page, we can now
restrict them to accessible ones.
Try to evaluate the following expression with the modal dialog open now:
base.find("button")
You'll see that only buttons inside the modal dialog were highlighted:
And now the following code will evaluate successfully:
browser.get("http://minium.vilt.io/sample-app/");
var base = $(":root").unless(".modal-backdrop").add(".modal-dialog");
// this will open the New message modal dialog
base.find("#compose").click();
// let's try to click the first available button
base.find("button").click();
Modules with step definitions
You can also define Step definitions at modules to reuse step definitions by several projects.
For this example we can create a module with step definitions to fill the form of the email. Create a
new file modules/forms-steps.js
and paste the following code there:
function formsSteps() {
this.When(/^I submit the form$/, function() {
var btn = base.find("submit");
btn.click();
});
this.When(/^I fill the field "(.*?)" with "(.*?)"$/, function(field, value) {
var fld = base.find("input, textarea").withLabel(field);
fld.fill(value);
});
}
if (typeof module !== 'undefined') module.exports = formsSteps;
After creating your module, you can copy the module with step definitions to other projects and reuse the same step definitions. To use the step definitions, simply call the module this way:
var mc = require("minium/cucumber");
mc.loadDefinitions("forms-steps");
So in your Features, you can now use the the steps definitions.
When I fill the field "Recipients" with "Rui Figueira"
And I fill the field "Subject" with "Minium Test"
And I fill the field "Message" with "My new Message"
And I submit the form
Create external modules (JAR file)
If you intend to create an external module from your project, open the pom.xml
(located at the project root folder) and add the following code there:
<build>
<resources>
<resource>
<directory>src/test/resources</directory>
<includes>
<include>modules/**/*</include>
</includes>
</resource>
</resources>
</build>
Finally, run the following command at the project root folder in order to install it into the local Maven repository:
mvn clean install
Now, you can load this file at other projects in order to reuse the modules code.
Load external modules
Minium allow you to import modules, in order to reuse code from other projects.
To load a module, just add the corresponding Maven dependency to the pom.xml
file of the projects where you want to use it. For example, the modules from project example:my-modules:1.0
can be imported like this:
<dependencies>
<dependency>
<groupId>example</groupId>
<artifactId>my-modules</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
The dependencies of a project are automatically loaded when it is opened. To update them while a project is opened, go to Project > Dependencies
and click on Update
.
Now, use the snippets above to import the modules and/or to import the step definitions.