Today, we will extend the behavior-driven development example of the previous blog post and add the blog content to the document. Like last time, we will retrieve the HTML content from the WordPress API. Sounds easy, right? We will see that the challenge is to display the HTML content correctly, so we do not see escaped HTML like „<p>…“ on the page.
As in part 1, we will follow a „test first“ strategy: we will create the e2e test specification before we implement the actual code.
Within the Protractor/Jasmine framework, we will learn how to match the text and the inner HTML of browser DOM elements with functions like expect(...).toEqual("...")
, .toContain("...")
and .toMatch(/regex/)
functions. The latter gives us the full flexibility of regular expressions.
Check out this book on Amazon: Angular Test-Driven Development
Plan for Today
Today, we plan to complement the blog title we have shown last time with the blog content, similar to the blog post Angular 4 Hello World Quickstart, which we will uses as our data mine. We will only show the title and the content as follows:
Before we start coding, we will add an e2e test that defines our expectation.
Step 0: Clone the GIT Repository and install the Application
This step can be skipped if you have followed part 1 of this series.
I am assuming that you have a Docker host available with 1.5GB or more RAM, GIT is installed on that host.
alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 oveits/angular-cli:1.4.3 $@' alias protractor='docker run -it --privileged --rm --net=host -v /dev/shm:/dev/shm -v $(pwd):/protractor webnicer/protractor-headless $@' git clone https://github.com/oveits/consuming-a-restful-web-service-with-angular.git cd consuming-a-restful-web-service-with-angular git checkout -b 320ae88 cli npm i chown -R $(whoami) . cli ng serve --host 0.0.0.0
Phase 1: Create an e2e Test
Step 1.1: Create a GIT Feature Branch
As always with a new feature, let us create a feature branch (on a second terminal):
$ cd /vagrant/consuming-a-restful-web-service-with-angular/
$ protractor
[20:24:22] I/direct - Using ChromeDriver directly...
[20:24:22] I/launcher - Running 1 instances of WebDriver
Jasmine started
consuming-a-restful-web-service-with-angular App
? should display the title
Executed 1 of 1 spec SUCCESS in 0.756 sec.
[20:24:27] I/launcher - 0 instance(s) of WebDriver still running
[20:24:27] I/launcher - chrome #01 passed
$ git checkout -b feature/0004-add-blog-content
You might need to adapt the path to your project. The protractor command is optional, but it will ensure that the e2e tests have worked on your machine before you start changing the code. I have seen some permissions topic described in the Appendices, which have made me cautious.
Step 1.2 (optional): Apply new Test Functions to the Blog Title
We would like to add a test that checks, whether the blog content is showing on the page. There are many Jasmine specification examples out there. Somehow, I have stumbled over this example. In order to verify that the functions I found there work fine, I thought it would be a good idea to write a new test similar to the ones in the example, but apply the test to the blog title before we write a new test for the blog content. This way, we can verify that we apply the correct syntax.
I have kept the original specification code, but I have added following code to the spec:
// e2e/app.e2e-spec.ts import { browser, by, element } from 'protractor'; import { AppPage } from './app.po'; describe('consuming-a-restful-web-service-with-angular App', () => { let page: AppPage; beforeEach(() => { page = new AppPage(); }); it('should display the title', () => { page.navigateTo(); expect(page.getParagraphText()).toContain('Angular 4 Hello World Quickstart'); }); }); describe('Blog', () => { beforeEach(() => { browser.get('/'); }); it('should display the blog title as header 1 and id="blog_title"', () => { expect(element(by.css('h1')).getText()).toEqual('Angular 4 Hello World Quickstart'); }); });
Both protractor e2e tests are successful without changing the code:
$ protractor [20:59:51] I/direct - Using ChromeDriver directly... [20:59:51] I/launcher - Running 1 instances of WebDriver Jasmine started consuming-a-restful-web-service-with-angular App ? should display the title Blog ? should display the blog title as header 1 and id="blog_title" Executed 2 of 2 specs SUCCESS in 2 secs. [20:59:57] I/launcher - 0 instance(s) of WebDriver still running [20:59:57] I/launcher - chrome #01 passed
Save it. Note: for pushing the changes to Github, you will need to fork my project and work with your fork. Otherwise, you can keep the git backups locally only.
git commit -am'1.2 added an addional test for the title looking for the first H1 header (successful test)' git push
Step 1.3 (optional): Refine the Test
Step 1.3.1 Create a Test looking for a specific Element per ID
Since the blog content will not be a header, we will need to look for something, which is unique on the page. We use an ID for fetching the correct element from the page:
import { browser, by, element } from 'protractor'; ... describe('Blog', () => { beforeEach(() => { browser.get('/'); }); const blog_title = element(by.id('blog_title')); it('should display the blog title as header 1 and id="blog_title"', () => { expect(element(by.css('h1')).getText()).toEqual('Angular 4 Hello World Quickstart'); expect(blog_title.getText()).toEqual('Angular 4 Hello World Quickstart'); }); });
Now the protractor test will fail. This is because we have not set the ID on the HTML template yet:
$ protractor [21:07:51] I/direct - Using ChromeDriver directly... [21:07:51] I/launcher - Running 1 instances of WebDriver Jasmine started consuming-a-restful-web-service-with-angular App ? should display the title Blog ? should display the blog title as header 1 and id="blog_title" - Failed: No element found using locator: By(css selector, *[id="blog_title"]) at WebDriverError (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/error.js:27:5) at NoSuchElementError (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/error.js:242:5) at /usr/local/lib/node_modules/protractor/built/element.js:808:27 at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14) at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14) at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21) at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2775:27) at /usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:639:7 at process._tickCallback (internal/process/next_tick.js:103:7)Error at ElementArrayFinder.applyAction_ (/usr/local/lib/node_modules/protractor/built/element.js:461:27) at ElementArrayFinder._this.(anonymous function) [as getText] (/usr/local/lib/node_modules/protractor/built/element.js:103:30) at ElementFinder.(anonymous function) [as getText] (/usr/local/lib/node_modules/protractor/built/element.js:829:22) at Object. (/protractor/e2e/app.e2e-spec.ts:28:23) at /usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:94:23 at new ManagedPromise (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1082:7) at controlFlowExecute (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:80:18) at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14) at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21) at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2820:25) From: Task: Run it("should display the blog title as header 1 and id="blog_title"") in control flow at Object. (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:79:14) at /usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:16:5 at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14) at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14) at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21) at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2775:27) From asynchronous test: Error at Suite. (/protractor/e2e/app.e2e-spec.ts:26:3) at Object. (/protractor/e2e/app.e2e-spec.ts:18:1) at Module._compile (module.js:570:32) at Module.m._compile (/protractor/node_modules/ts-node/src/index.ts:392:23) at Module._extensions..js (module.js:579:10) at Object.require.extensions.(anonymous function) [as .ts] (/protractor/node_modules/ts-node/src/index.ts:395:12) ************************************************** * Failures * ************************************************** 1) Blog should display the blog title as header 1 and id="blog_title" - Failed: No element found using locator: By(css selector, *[id="blog_title"]) Executed 2 of 2 specs (1 FAILED) in 2 secs. [21:07:58] I/launcher - 0 instance(s) of WebDriver still running [21:07:58] I/launcher - chrome #01 failed 1 test(s) [21:07:58] I/launcher - overall: 1 failed spec(s) [21:07:58] E/launcher - Process exited with error code 1
To save the change:
git commit -am'1.3.1 search title by element id (failed e2e test)'
Step 1.3.2 Fix the Test
Let us fix the failed test like follows: In the HTML template src/app/app.component.html, we specify the element ID:
<h1 id="blog_title">{{title}}</h1>
Now the protractor test is successful again:
$ protractor [21:14:27] I/direct - Using ChromeDriver directly... [21:14:27] I/launcher - Running 1 instances of WebDriver Jasmine started consuming-a-restful-web-service-with-angular App ? should display the title Blog ? should display the blog title as header 1 and id="blog_title" Executed 2 of 2 specs SUCCESS in 2 secs. [21:14:34] I/launcher - 0 instance(s) of WebDriver still running [21:14:34] I/launcher - chrome #01 passed
That was simple. Now let us apply our learnings to the blog content.
To save the change:
git commit -am'1.3.2 add ID to HTML template (success)'; git push
Phase 2: Create the Test for the Blog Content
The content of the blog can be seen on WordPress:
The content starts with: In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application.
Let us search for that on our application.
Step 2.1 Add Blog Content e2e Tests
Similar to what we have done for the Blog Title, let us create an e2e test for the blog content. We add the parts in blue to e2e/app.e2e-spec.ts:
// e2e/app.e2e-spec.ts ... describe('Blog', () => { beforeEach(() => { browser.get('/'); }); const blog_title = element(by.id('blog_title')); const blog_content = element(by.id('blog_content')); it('should display the blog title as header 1 and id="blog_title"', () => { expect(element(by.css('h1')).getText()).toEqual('Angular 4 Hello World Quickstart'); expect(blog_title.getText()).toEqual('Angular 4 Hello World Quickstart'); }); it('should display the blog content', () => { expect(blog_content.getText()).toContain('In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application.'); }); });
Since the content is quite large, we did not compare it with the equality operator, but we have used the ‚toContain‘ function instead.
The new protractor test fails as expected:
$ protractor [21:23:04] I/direct - Using ChromeDriver directly... [21:23:04] I/launcher - Running 1 instances of WebDriver Jasmine started consuming-a-restful-web-service-with-angular App ? should display the title Blog ? should display the blog title as header 1 and id="blog_title" ? should display the blog content - Failed: No element found using locator: By(css selector, *[id="blog_content"]) at WebDriverError (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/error.js:27:5) at NoSuchElementError (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/error.js:242:5) at /usr/local/lib/node_modules/protractor/built/element.js:808:27 at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14) at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14) at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21) at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2775:27) at /usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:639:7 at process._tickCallback (internal/process/next_tick.js:103:7)Error at ElementArrayFinder.applyAction_ (/usr/local/lib/node_modules/protractor/built/element.js:461:27) at ElementArrayFinder._this.(anonymous function) [as getText] (/usr/local/lib/node_modules/protractor/built/element.js:103:30) at ElementFinder.(anonymous function) [as getText] (/usr/local/lib/node_modules/protractor/built/element.js:829:22) at Object. (/protractor/e2e/app.e2e-spec.ts:33:25) at /usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:94:23 at new ManagedPromise (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1082:7) at controlFlowExecute (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:80:18) at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14) at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21) at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2820:25) From: Task: Run it("should display the blog content") in control flow at Object. (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:79:14) at /usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:16:5 at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14) at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14) at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21) at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2775:27) From asynchronous test: Error at Suite. (/protractor/e2e/app.e2e-spec.ts:32:3) at Object. (/protractor/e2e/app.e2e-spec.ts:18:1) at Module._compile (module.js:570:32) at Module.m._compile (/protractor/node_modules/ts-node/src/index.ts:392:23) at Module._extensions..js (module.js:579:10) at Object.require.extensions.(anonymous function) [as .ts] (/protractor/node_modules/ts-node/src/index.ts:395:12) ************************************************** * Failures * ************************************************** 1) Blog should display the blog content - Failed: No element found using locator: By(css selector, *[id="blog_content"]) Executed 3 of 3 specs (1 FAILED) in 3 secs. [21:23:12] I/launcher - 0 instance(s) of WebDriver still running [21:23:12] I/launcher - chrome #01 failed 1 test(s) [21:23:12] I/launcher - overall: 1 failed spec(s) [21:23:12] E/launcher - Process exited with error code 1
To save the change:
git commit -am'2.1 add test for blog content (failed)'; git push
Step 2.2 Fix the Blog Content Test
Let us fix the test now.
Step 2.2.1 Add the Blog Content to the HTML Template
In order to display the blog content, we need to add the following to the HTML template src/app/app.component.html:
Step 2.2.2 Define the Variable ‚content‘ in the Component
However, as long as the variable ‚content‘ is not defined, we will have added an empty div. To define the variable, we must change the component src/app/app.component.ts
import { Component, OnInit } from '@angular/core'; import { Http } from '@angular/http'; import { Response } from '@angular/http'; import 'rxjs/add/operator/map' @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { title : any = null content : any = null constructor(private _http: Http) {} ngOnInit() { this._http.get('https://public-api.wordpress.com/rest/v1.1/sites/vocon-it.com/posts/3078') .map((res: Response) => res.json()) .subscribe(data => { this.title = data.title; this.content = data.content; console.log(data); }); } }
That’s it: the e2e tests are successful:
$ protractor [21:30:12] I/direct - Using ChromeDriver directly... [21:30:12] I/launcher - Running 1 instances of WebDriver Jasmine started consuming-a-restful-web-service-with-angular App ? should display the title Blog ? should display the blog title as header 1 and id="blog_title" ? should display the blog content Executed 3 of 3 specs SUCCESS in 3 secs. [21:30:19] I/launcher - 0 instance(s) of WebDriver still running [21:30:19] I/launcher - chrome #01 passed
To save the change:
git commit -am'2.2.2 Added content to HTML template and component (success)'; git push
Step 2.3 Explore the Result
Now let us have a look at what we have accomplished and let us open the browser on http://localhost:4200:
The good news is: the content is there.
😉
The bad news it: it is not readable because the HTML code in the blog content variable has been HTML escaped.
🙁
This is the standard behavior in Angular. So what can we do now? The solution to the problem can be found in Step 2.3 of my original post: we need to set the innerHTML of the div instead of adding the content as text. But, as we are performing a „behavior-driven“ approach, let us try to write the tests first.
Step 2.4 Improve the e2e Test Spec
Let us add an additional line to the test specification in order to make sure, we will see the HTML in the correct format:
import { browser, by, element } from 'protractor';
describe('Blog', () => {
beforeEach(() => {
browser.get('/');
});
const blog_title = element(by.id('blog_title'));
const blog_content = element(by.id('blog_content'));
it('should display the blog title as header 1 and id="blog_title"', () => {
expect(element(by.css('h1')).getText()).toEqual('Angular 4 Hello World Quickstart');
expect(blog_title.getText()).toEqual('Angular 4 Hello World Quickstart');
});
it('should display the blog content', () => {
expect(blog_content.getText()).toContain('In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application.');
});
});
With that, we test, whether the innerHTML of the div element starts with the correct HTML code. For that, we have made use of two functionalities of Jasmine:
- reading the innerHTML of an element with the getInnerHtml() function
- matching against a regular expression with toMatch(/regexp/)
As expected, the protractor test fails with the message
To save the change:
git commit -am'2.4 added innerHTML test for content with regular expression (fail)'; git push
Step 2.5 Fulfill the improved e2e Test
We can see that the content is escaped (e.g. instead of
). Let us fix that by specifying the innerHTML like follows:
As soon as the content is loaded, the innerHTML ‚Loading…‘ will be replaced by the content retrieved from WordPress.
Let us run the test:
$ protractor [20:55:50] I/direct - Using ChromeDriver directly... [20:55:50] I/launcher - Running 1 instances of WebDriver Jasmine started [20:55:56] W/element - more than one element found for locator By(css selector, app-root h1) - the first result will be used consuming-a-restful-web-service-with-angular App ? should display blog title [20:55:57] W/element - more than one element found for locator By(css selector, h1) - the first result will be used Blog ? should display the blog title as header 1 and id="blog_title" ? should display the blog content Executed 3 of 3 specs SUCCESS in 3 secs. [20:55:57] I/launcher - 0 instance(s) of WebDriver still running [20:55:57] I/launcher - chrome #01 passed
That was easy, again.
To save the change:
git commit -am'2.5 Fix the content innerHTML test (success)'; git push
Step 3: Explore the Final Result
Now let us head over to the browser on URL http://localhost:4200 again:
Even though there is no styling implemented yet, that looks much better now. This is, what we had in mind to implement today.
As a wrap-up, the changes can be merged into the develop branch: the tests are successful and also the explorative „tests“ have shown a correct result.
git checkout develop git merge feature/0004-add-blog-content git push
Summary
In this blog post, we have shown how to retrieve HTML-formated data from the WordPress API and display it in a correct format. In a „test-driven“ approach, we have created Protractor e2e test specifications, before we have implemented the function.
Appendix: Error message: failed loading configuration file ./protractor.conf.js
After successfully cloning and installing the repo, I had seen following error message, when trying to perform the e2e tests:
$ protractor [19:23:16] E/configParser - Error code: 105 [19:23:16] E/configParser - Error message: failed loading configuration file ./protractor.conf.js [19:23:16] E/configParser - Error: Cannot find module 'jasmine-spec-reporter' at Function.Module._resolveFilename (module.js:469:15) at Function.Module._load (module.js:417:25) at Module.require (module.js:497:17) at require (internal/module.js:20:19) at Object. (/protractor/protractor.conf.js:4:26) at Module._compile (module.js:570:32) at Object.Module._extensions..js (module.js:579:10) at Module.load (module.js:487:32) at tryModuleLoad (module.js:446:12) at Function.Module._load (module.js:438:3)
Resolution:
I have seen that the cli command was creating all files as user root. This was because I had defined
alias cli='docker run -it --rm -w /app -v $(pwd):/app oveits/angular-cli:1.4.3 $@'
After changing this to
alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 -u $(id -u $(whoami)) oveits/angular-cli:1.4.3 $@'
and re-performing the cli npm i after the clone, the problem was resolved. However, this has caused the next ’npm i‘ issue described below, and it is better to perform following workaround:
Better:
- Keep the first version of the alias
- After applying ‚cli npm i‘, perform the command
sudo chown -R $(whoami) PROJECT_ROOT_DIR
.
Appendix npm i: Error: EACCES: permission denied, mkdir ‚/.npm‘
npm ERR! Linux 4.2.0-42-generic npm ERR! argv "/usr/local/bin/node" "/usr/local/bin/npm" "i" npm ERR! node v6.11.2 npm ERR! npm v3.10.10 npm ERR! path /.npm npm ERR! code EACCES npm ERR! errno -13 npm ERR! syscall mkdir npm ERR! Error: EACCES: permission denied, mkdir '/.npm' npm ERR! at Error (native) npm ERR! { Error: EACCES: permission denied, mkdir '/.npm' npm ERR! at Error (native) npm ERR! errno: -13, npm ERR! code: 'EACCES', npm ERR! syscall: 'mkdir', npm ERR! path: '/.npm', npm ERR! parent: 'consuming-a-restful-web-service-with-angular' } npm ERR! npm ERR! Please try running this command again as root/Administrator. npm ERR! Please include the following file with any support request: npm ERR! /app/npm-debug.log
The reason is, that I had defined
alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 -u $(id -u $(whoami)) oveits/angular-cli:1.4.3 $@'
With that, npm i is run as the vagrant user with ID=900. However, inside the container, neither the user „vagrant“ nor the user ID 900 is defined. This seems to cause the problem that the cli npm i command wants to create a directory /$HOME/.npm, but $HOME is not set. Therefore, the user with id=900 wants to create the file /.npm, but only root is allowed to do so.
The better workaround is to define
alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 oveits/angular-cli:1.4.3 $@'
without the -u option and perform a command
chown -R $(whoami) .
where needed (e.g. after each npm i command).
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.
Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me?