Skip to content

Commit

Permalink
add protractor tests to contact-app-starter
Browse files Browse the repository at this point in the history
- add back `id="add-contact"` to the contact-list template
- add Protractor tests: contact list, new contact, and with page object
  tests
- add circle ci integration
- update .gitignore to not save output/ directory for junit tests

closes testing-angular-applications#14
  • Loading branch information
cnishina committed Sep 20, 2017
1 parent f390f67 commit c4c6b3e
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 16 deletions.
16 changes: 16 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 2
jobs:
build:
working_directory: ~/workspace
docker:
- image: circleci/node:7-browsers
steps:
- checkout

- run: export IS_CIRCLE=true
- run: node --version
- run: npm --version
- run: google-chrome --version

- run: npm install
- run: npm run ng e2e protractor.conf.js
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
/coverage
/libpeerconnection.log
npm-debug.log
/output/
testem.log
/typings
yarn-error.log
Expand Down
11 changes: 0 additions & 11 deletions e2e/app.po.ts

This file was deleted.

26 changes: 26 additions & 0 deletions e2e/po/base.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { browser as globalBrowser, element as globalElement, ElementHelper,
ExpectedConditions as globalExpectedConditions, ProtractorBrowser,
ProtractorExpectedConditions } from 'protractor';
import { promise as wdpromise } from 'selenium-webdriver';

export class PageObject {
browser: ProtractorBrowser;
element: ElementHelper;
expectedConditions: ProtractorExpectedConditions;

constructor(browser?: ProtractorBrowser) {
if (browser) {
this.browser = browser;
this.element = browser.element;
this.expectedConditions = browser.ExpectedConditions;
} else {
this.browser = globalBrowser;
this.element = globalElement;
this.expectedConditions = globalExpectedConditions;
}
}

getCurrentUrl() {
return this.browser.getCurrentUrl();
}
}
91 changes: 91 additions & 0 deletions e2e/po/contact-list.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { by, ProtractorBrowser, ElementArrayFinder } from 'protractor';
import { promise as wdpromise } from 'selenium-webdriver';
import { PageObject } from './base.po';
import { NewContactPageObject } from './new-contact.po';

export enum COL {
name = 1,
email = 2,
phoneNumber = 3
}

export class Contact {
constructor(public name?: string, public email?: string, public tel?: string) {}
}

export class ContactListPageObject extends PageObject {
static nameCol = 1;
trs: ElementArrayFinder;

constructor(browser?: ProtractorBrowser) {
super(browser);

let tbody = this.element(by.tagName('tbody'));
this.trs = tbody.all(by.tagName('tr'));
}

navigateTo() {
this.browser.get('/');
}

clickPlusButton() {
this.element(by.id('add-contact')).click();
return new NewContactPageObject();
}

/**
* Returns the ElementArrayFinder object for the table row. This is an array because there could
* be more than one entry with the same matching name
* @param name
*/
findContact(name: string): ElementArrayFinder {
return this.trs.filter(elem => {
return elem.all(by.tagName('td')).get(COL.name).getText().then(text => {
return text === name;
});
});
}

/**
* Returns a promise of a contact array.
*/
getContacts() {
return this.trs.map(elem => {
let contact: Contact = new Contact();
let tds = elem.all(by.tagName('td'));
// We need to get the values of the contact name and email. Since these are in a couple of
// different promises, we'll create a promise array.
let promises: any[] = [];

// Getting the text returns a promise of a string then the next function sets the contact's
// name. This function returns void so the final promise saved is of Promise<void>.
// We set the promise array to be of type any since we do not care about the promise type.
promises.push(tds.get(COL.name).getText().then(text => {
contact.name = text;
}));
promises.push(tds.get(COL.email).getText().then(text => {
contact.email = text;
}));
promises.push(tds.get(COL.phoneNumber).getText().then(text => {
contact.tel = text;
}));

// Resolve all the promises and return the contact.
return Promise.all(promises).then(() => {
return contact;
});
})
}

/**
* Returns a promise of comma delimited names
*/
getContactNames() {
return this.trs.reduce((acc, curr) => {
let name = curr.all(by.tagName('td')).get(COL.name);
return name.getText().then(text => {
return acc === '' ? text : acc + ', ' + text;
});
}, '');
}
}
2 changes: 2 additions & 0 deletions e2e/po/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './contact-list.po';
export * from './new-contact.po';
52 changes: 52 additions & 0 deletions e2e/po/new-contact.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { by, ElementFinder, ProtractorBrowser } from 'protractor';
import { promise as wdpromise } from 'selenium-webdriver';
import { PageObject } from './base.po';
import { ContactListPageObject } from './contact-list.po';

export class NewContactPageObject extends PageObject {
inputName: ElementFinder;
inputEmail: ElementFinder;
inputPhone: ElementFinder;

constructor(browser?: ProtractorBrowser) {
super(browser);
this.inputName = this.element(by.id('contact-name'));
this.inputEmail = this.element(by.id('contact-email'));
this.inputPhone = this.element(by.css('input[type="tel"]'));
}

/**
* Set extra fields for email and phone number. We should return a promise and since we are doing
* multiple actions, we should keep track of them in a promises array and return all promises
* as a single promise.
*
* @param email
* @param phoneNumber
*/
setContactInfo(name: string, email: string, phoneNumber: string) {
this.inputName.sendKeys(name);
if (email) {
this.inputEmail.sendKeys(email);
}
if (phoneNumber) {
this.inputPhone.sendKeys(phoneNumber);
}
}

clickCreateButton() {
this.element(by.buttonText('Create')).click();
return new ContactListPageObject();
}

getName() {
return this.inputName.getAttribute('value');
}

getEmail() {
return this.inputEmail.getAttribute('value');
}

getPhone() {
return this.inputPhone.getAttribute('value');
}
}
29 changes: 25 additions & 4 deletions protractor.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,41 @@ exports.config = {
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
browserName: 'chrome',
chromeOptions: {
args: (process.env.IS_CIRCLE ? ['--headless'] : [])
}
},
directConnect: true,
directConnect: !process.env.IS_JENKINS,
baseUrl: 'http://localhost:4200/',

// Jasmine
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {

// Plugins
beforeLaunch: () => {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
},
onPrepare: ()=> {
if (process.env.IS_JENKINS) {
let jasmineReporters = require('jasmine-reporters');
let junitReporter = new jasmineReporters.JUnitXmlReporter({
savePath: 'output/',
consolidateAll: false
});
jasmine.getEnv().addReporter(junitReporter);
} else {
let specReporter = new SpecReporter({
spec: { displayStacktrace: true }
});
jasmine.getEnv().addReporter(specReporter);
}
}
};
2 changes: 1 addition & 1 deletion src/app/contacts/contact-list/contact-list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
Delete All Contacts
</button>
<a *ngIf="!isLoading && !deletingContact" routerLink="/add">
<button md-fab class="add-fab"><md-icon class="add-fab-icon" mdTooltip="Add new contact">add</md-icon></button>
<button md-fab class="add-fab" id="add-contact"><md-icon class="add-fab-icon" mdTooltip="Add new contact">add</md-icon></button>
</a>
</div>

Expand Down

0 comments on commit c4c6b3e

Please sign in to comment.