E2E Testing In Angular 2 From Zero To Hero (Final Part)
Dear future Masters of Angular 2 E2E Testing.
This is it. At the end of this lesson you will finally have every tools to create functional tests for your Angular 2 applications. No more excuses: You've got the power!
We will first get familiar with Locators then we will see how awesome Page Objects can be, followed by how to handle asynchronousness and finally the easiest part that is triggering Actions.
If you don't have the basics right, head there otherwise you will be a little bit lost.
Locators
One if not the most important thing in E2E testing is finding the elements that you want to test in your view. Most of the time, you will ask yourself the question "why can't I find this **** element?" followed by a "oooohhhhh".
You have many ways to do this. Most of them depend of your application. In order to get your elements, you will use a range of Locators. Most of the time, you can find your elements by:
- Class name.
- Id.
- Model.
- Binding.
For the upcoming tests, we will use this template.html:
<div class="parent2">
<div class="parent1">
<div class="nested-class" id="nested-id" style="color:blue;">
I am a nested element
</div>
</div>
</div>
Let's focus on nested-class and nested-id.
By Class:
describe("App", () => {
beforeEach(() => {
browser.get("/#/home");
});
it("should find the nested class", () => {
let elem = element(by.css(".nested-class"));
expect(elem.isPresent()).toBeTruthy();
});
});
We grab here our element using element(by.css(selector)) then we test if it's present or not.
By Id:
it("should find the nested id", () => {
let elem = element(by.id("nested-id"));
expect(elem.getText()).toBe("I am a nested element");
});
We grab here our element using element(by.id(elem-id)) then we test if it's present or not.
The following locators a very useful in AngularJS but not yet implemented in Angular 2 so just keep them on the side for now.
By Model:
it("should find the model", () => {
let elem = element(by.model("modelToFind"));
expect(elem.isPresent()).toBeTruthy();
});
By Bindings:
it("should find the binding", () => {
let elem = element(by.model("bindingToFind"));
expect(elem.isPresent()).toBeTruthy();
});
In order to debug your tests, you can just add "browser.pause()" in your test, you will be able to run all the previous instructions and stop wherever you want.
Once it reaches your pause, you will have a screen similar to this one:
As you can see in this image, in order to get into the driver's seat you need to type repl.
Now let's say you don't have any test and you just want to play around with Protractor. You can just type in your terminal:
protractor --elementExplorer
If you have some issues with your path, you can type:
./node_modules/protractor/bin/elementexplorer.js
No need to type repl this time, just use the browser object and you are ready to sail!
Page Objects
What if you have tests that share the same scenario? Are you going to copy paste your code everywhere? What if this scenario changes? Now you have to go through every files and make the modifications.
That's where Pages Objects become very useful. Write Once, Share Everywhere!
A Page Object is an object that contains the elements and scenarios concerning a page.
First a very simple Page Object:
export class HomePage {
nestedClass;
constructor() {
this.nestedClass = element(by.css(".nested-class"));
}
}
Then we use it in our test:
import { HomePage } from "./home.page.ts";
describe("App", () => {
let homePage;
beforeEach(() => {
homePage = new HomePage();
browser.get("/#/home");
});
it("should find the nested class using a page", () => {
expect(homePage.nestedClass.isPresent()).toBeTruthy();
});
});
We import the Page Object, create an instance then use it's property.
Asynchronous Adventures
Now my friends, you can access any elements on your page! I mean almost :).
There will be some special cases.
If you were (un)lucky enough to work with jQuery, you must be familiar with document.ready().
By the asynchronous nature of JavaScript, there will be cases where you will try to access an element before it has yet appeared on the page.
In order to handle that, you will have to use a feature of Protractor in order to wait for your element to be ready before looking for it.
Here is an example displaying a button after a timeout expiration.
First the class that contains the timeout:
export class Home {
isHidden = true;
triggerTimeOut = ((){
setTimeout(() => this.isHidden = false, 2250)
})
}
Then the template.html:
<button class="trigger-timeout-button" (click)="triggerTimeOut()">Trigger timeout</button>
<div class="dynamic-text" *ngIf="!isHidden">Show me</div>
We have here a button that will trigger our timeout, once the timeout expires, the text is displayed.
Here are the tests.
it("should not find the button", () => {
let btn = element(by.css(".trigger-timeout-button"));
btn.click();
let elem = element(by.css(".dynamic-text"));
expect(elem.isPresent()).toBeFalsy();
});
We find our button, click on it, try to find our text BUT we expect it not to be present.
And here is the way we handle this case ;).
it("should find the button", () => {
let btn = element(by.css(".trigger-timeout-button"));
btn.click();
let elem = element(by.css(".dynamic-text"));
browser.wait(function() {
return browser.isElementPresent(by.css(".dynamic-text"));
}, 5000);
expect(elem.isPresent()).toBeTruthy();
});
We use "browser.wait" combined with "browser.isElementPresent" testing our Locator. After 5 seconds the rest of the code is executed. Don't forget to put a timer here otherwise your tests will be blocked forever!
So if one of your element is supposed to be here but you can't get it, your gut instinct should tell you to remember what you have just read here.
Actions
Now that you have your precious element, you have done 90% of the work!
If you can't find it, it's either that your application doesn't work as it should (that's why we do tests) or you need to get better at testing and spend less time on Pokemon GO hunting dratinis near a river at 1am (we have all been there ...).
Let's keep going with the actions that you can use on an element. Here they are:
- elem.sendKeys: Type something in an input.
- elem.click: Click.
- elem.clear: Erase everything in an input.
- elem.getAttribute('attrName'): Return a specific attribute of the element.
- elem.submit: Submit a form.
- elem.isPresent: Test if the element is present.
We are using Protractor which relies on WebDriver. If you need more fancy actions, you can have a look at what is available here.
Let’s test some of them.
For the template.html:
<input type="text" id="input-one" />
We will play a bit with the text input. Quick quiz, what does this test do (don’t read the description)?
it("should set then clear the value of an input", () => {
let value = "Some text";
let elem = element(by.id("input-one"));
elem.sendKeys(value);
expect(elem.getAttribute("value")).toBe(value);
elem.clear();
expect(elem.getAttribute("value")).toBe("");
});
Solution: We get our input, type “Some text” inside, test that we got the value set, clear it and finally test that the value is empty.
Legitimately we can ask ourself what is the best way to trigger actions? This link here shows one of the flaw of using JavaScript instead of WebDriver.
Sure writing in JavaScript is way more compact but it comes with some risks!
You can do anything that you want with an element. The best way to test it is to use the matchers that are available. Why? Because they are elegant and close to the human language, example:
expect(apples.length !== 0).toBe(true);
expect(apples).not.toBeEmpty();
Conclusion
This was the final post of this series, right now you have 90% of the skills to test your Angular 2 applications. You will get the other 10% by experience. Remember, first getting your elements using Locators, then putting the one that might be replicated in a Page Object, if you can’t find your element and you are sure that it is here, it might be might because it hasn’t appeared yet and you need to handle the asynchronousness. Finally test the **** out of your elements with Actions.