Upgrade to Pro — share decks privately, control downloads, hide ads and more …

E2E SPA tests with protractor

E2E SPA tests with protractor

Maciej Treder

October 11, 2019
Tweet

More Decks by Maciej Treder

Other Decks in Programming

Transcript

  1. TypeScript TypeScript ES2016 ES2015 ES5 • Superset of JS which

    compiles to JavaScript • Strongly typed • Object-oriented • Classes, inheritance, interfaces
  2. Jasmine • Behavior-driven framework for testing JS code • Does

    not require a DOM • Clean and obvious syntax [ expect(a).toBe(true) ]
  3. Selenium • npm i -G webdriver-manager • webdriver-manager update •

    webdriver-manager start webdriver-manager: using global installed version 12.1.7 [10:56:53] I/start - java -Djava.security.egd=file:///dev/./urandom - Dwebdriver.gecko.driver=/usr/lib/node_modules/webdriver- manager/selenium/geckodriver-v0.25.0 -jar /usr/lib/node_modules/ webdriver-manager/selenium/selenium-server- standalone-3.141.59.jar -port 4444 [10:56:53] I/start - seleniumProcess.pid: 6866 10:56:54.317 INFO [GridLauncherV3.parse] - Selenium server version: 3.141.59, revision: e82be7d358 10:56:54.578 INFO [GridLauncherV3.lambda$buildLaunchers$3] - Launching a standalone Selenium Server on port 4444 2019-09-18 10:56:54.760:INFO::main: Logging initialized @1270ms to org.seleniumhq.jetty9.util.log.StdErrLog 10:56:55.640 INFO [WebDriverServlet.<init>] - Initialising WebDriverServlet 10:56:55.927 INFO [SeleniumServer.boot] - Selenium Server is up and running on port 4444
  4. Setup npm project • npm init • npm i —save

    typescript @types/node @types/jasmine protractor • adjust scripts in package.json { "name": "e2e", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "pretest": "rm -rf build/;tsc”, "test": "protractor build/conf.js" }, "author": "", "license": "ISC", "dependencies": { "@types/jasmine": "^3.4.0", "@types/node": "^12.7.5", "protractor": "^5.4.2", "typescript": "^3.6.3" } }
  5. conf.ts export const config = { specs: ['tests/**/*.spec.js'], capabilities: {

    'browserName': 'chrome' }, seleniumAddress: ‘http://localhost:4444/wd/hub', framework: 'jasmine2', };
  6. Jasmine • describe() • beforeAll() • beforeEach() • it() •

    expect(what?) • toBe() • toBeCloseTo() • toBeGreater() • afterEach() • afterAll()
  7. tests/helloWorld.spec.ts describe('Hello World Suite’, () => { it('First test case',

    () => { console.log('Hello World!'); }); it('This always fail', () => { expect(true).toBeFalsy(); }); it('This always pass', () => { expect(true).toBeTruthy(); }); });
  8. npm test [11:03:37] I/launcher - Running 1 instances of WebDriver

    [11:03:37] I/hosted - Using the selenium server at http://192.168.0.206:4444/wd/hub Started Hello World! .F. Failures: 1) Hello World This always fail Message: Expected true to be falsy. Stack: Error: Failed expectation at UserContext.<anonymous> (/home/pi/e2e/build/tests/helloWorld.spec.js:6:22) at /home/pi/e2e/node_modules/jasminewd2/index.js:112:25 at new ManagedPromise (/home/pi/e2e/node_modules/selenium-webdriver/lib/promise.js:1077:7) at ControlFlow.promise (/home/pi/e2e/node_modules/selenium-webdriver/lib/promise.js:2505:12) at schedulerExecute (/home/pi/e2e/node_modules/jasminewd2/index.js:95:18) at TaskQueue.execute_ (/home/pi/e2e/node_modules/selenium-webdriver/lib/promise.js:3084:14) at TaskQueue.executeNext_ (/home/pi/e2e/node_modules/selenium-webdriver/lib/promise.js:3067:27) at asyncRun (/home/pi/e2e/node_modules/selenium-webdriver/lib/promise.js:2974:25) at /home/pi/e2e/node_modules/selenium-webdriver/lib/promise.js:668:7 3 specs, 1 failure Finished in 0.031 seconds
  9. tests/basic.spec.ts import { browser, by, ElementFinder, element } from 'protractor';

    describe('Basic tests', () => { const menuContainer: ElementFinder = element(by.tagName('menu')); const pageTitle: ElementFinder = element(by.css('h1')); beforeAll(() => { browser.get('https://maciejtreder.github.io/e2e/static'); }); it('Should be able to navigate to the application', async () => { expect(await menuContainer.isPresent()).toBeTruthy(); }); it('Should be able to navigate within the application', async () => { menuContainer.element(by.xpath(`//li[text()="Done"]`)).click(); expect (await pageTitle.getText()).toBe("What you have done so far:"); menuContainer.element(by.xpath(`//li[text()="Todo list"]`)).click(); expect (await pageTitle.getText()).toBe("Todo"); }); }); by, ElementFinder, element const menuContainer: ElementFinder = element(by.tagName('menu')); const pageTitle: ElementFinder = element(by.css('h1')); beforeAll(() => { browser.get('https://maciejtreder.github.io/e2e/static'); }); it('Should be able to navigate to the application', async () => { expect(await menuContainer.isPresent()).toBeTruthy(); }); menuContainer.element(by.xpath(`//li[text()="Done"]`)).click(); expect (await pageTitle.getText()).toBe("What you have done so far:"); menuContainer.element(by.xpath(`//li[text()="Todo list"]`)).click(); expect (await pageTitle.getText()).toBe("Todo");
  10. menu.component.ts import { element, ElementFinder, by } from 'protractor'; export

    class MenuComponent { private container : ElementFinder; constructor() { this.container = element(by.tagName('menu')); } public getContainer(): ElementFinder { return this.container; } public getItemByText(text: string): ElementFinder { return this.container.element(by.xpath(`//li[text()="${text}"]`)); } }
  11. tests/basic.spec.ts import { browser, by, ElementFinder, element } from 'protractor';

    import { MenuComponent } from '../pages/menu.component'; describe('Basic tests', () => { const pageTitle: ElementFinder = element(by.css('h1')); beforeAll(() => { browser.get('https://maciejtreder.github.io/e2e/static'); }); it('Should be able to navigate to the application', async () => { expect(await menu.getContainer().isPresent()).toBeTruthy(); }); it('Should be able to navigate within the application', async () => { expect(await pageTitle.getText()).toBe("What you have done so far:"); expect (await pageTitle.getText()).toBe("Todo"); }); }); const menuContainer: ElementFinder = element(by.tagName('menu')); const menu: MenuComponent = new MenuComponent(); menuContainer.element(by.xpath(`//li[text()="Done"]`)).click(); menuContainer.element(by.xpath(`//li[text()="Todo list"]`)).click(); menu.getItemByText("Done").click(); menu.getItemByText("Todo list").click();
  12. base.page.ts import { element, by, ElementFinder } from 'protractor'; export

    abstract class BasePage { private title: ElementFinder = element(by.css('h1')); public abstract go(): void; public abstract isOnPage(): Promise<boolean>; public getTitle(): ElementFinder { return this.title; } }
  13. todo.page.ts import { browser } from 'protractor'; import { BasePage

    } from './base.page'; export class TodoPage extends BasePage { public go() { browser.get(`http://maciejtreder.github.io/e2e/static/todos`); } public async isOnPage(): Promise<boolean> { const title = await this.getTitle().getText(); return title === 'Todo'; } }
  14. done.page.ts import { browser } from 'protractor'; import { BasePage

    } from './base.page'; export class DonePage extends BasePage { public go() { browser.get(`http://maciejtreder.github.io/e2e/static/done`); } public async isOnPage(): Promise<boolean> { const title = await this.getTitle().getText(); return title === 'What you have done so far:'; } }
  15. tests/basic.spec.ts import { browser, by, ElementFinder, element } from 'protractor';

    import { MenuComponent } from '../pages/menu.component'; import { TodoPage } from '../pages/todo.page'; import { DonePage } from '../pages/done.page'; describe('Basic tests', () => { const menu: MenuComponent = new MenuComponent(); beforeAll(() => { browser.get('https://maciejtreder.github.io/e2e/static'); }); it('Should be able to navigate to the application', async () => { expect(await menu.getContainer().isPresent()).toBeTruthy(); }); it('Should be able to navigate within the application', async () => { menu.getItemByText("Done").click(); menu.getItemByText("Todo list").click(); }); }); const todoPage: TodoPage = new TodoPage(); const donePage: DonePage = new DonePage(); const pageTitle: ElementFinder = element(by.css('h1')); expect (await pageTitle.getText()).toBe("What you have done so far:"); expect (await pageTitle.getText()).toBe("Todo"); expect(await donePage.isOnPage()).toBe(true); expect(await todoPage.isOnPage()).toBe(true);
  16. todo.page.ts public getNewTaskInput(): ElementFinder { return element(by.tagName('input')); } public getNewTaskSubmitButton():

    ElementFinder { return element(by.xpath('//button[text()="Save"]')); } public getNewTaskError(): ElementFinder { return element(by.css('span.error')); } public getTaskByName(name: string): ElementFinder { return element(by.xpath(`//li[contains(text(), '${name}')]`)); } public getErrorInfo(): ElementFinder { return element(by.css('.error')); } public async getMarkAsDoneButton(name: string): Promise<ElementFinder> { const liObject = await this.getTaskByName(name); return liObject.element(by.css('span')); }
  17. tests/todoPage.spec.ts import { TodoPage } from "../pages/todo.page"; describe('Todo Page', ()

    => { const todoPage = new TodoPage(); beforeEach(() => todoPage.go()); it('Should contain all elements', async () => { expect(await todoPage.getNewTaskInput().isPresent()).toEqual(true); expect(await todoPage.getNewTaskSubmitButton().isPresent()).toEqual(true); expect(await todoPage.getNewTaskError().isPresent()).toEqual(false); }); it('Should not be able to submit empty form', async () => { await todoPage.getNewTaskInput().click(); await todoPage.getTitle().click(); expect(await todoPage.getNewTaskSubmitButton().isEnabled()).toEqual(false); expect(await todoPage.getErrorInfo().isPresent()).toEqual(true); }); it('Should be able to add task', async () => { const newTaskName = `mtreder${new Date().getTime()}`; await todoPage.getNewTaskInput().sendKeys(newTaskName); await todoPage.getNewTaskSubmitButton().click(); expect(await todoPage.getTaskByName(newTaskName).isDisplayed()).toEqual(true); //cleanup should be done here }); }); //cleanup should be done here
  18. services/task.http.service.ts import { RxHR } from '@akanass/rx-http-request'; import { Task

    } from '../model/task.model'; import { map } from 'rxjs/operators'; export class TaskHttpService { private readonly SERVICE_URI: string = 'https://e2e-workshop-backend.herokuapp.com/tasks'; public deleteTask(id: string): Promise<void> { return RxHR.delete(this.SERVICE_URI + `/delete/${id}`).pipe(map(response => { return; })).toPromise(); } public async deleteTaskByName(name: string): Promise<void> { const allTasks: Task[] = await RxHR.get(this.SERVICE_URI, {json: true}) .pipe(map(response => response.body as Task[])).toPromise(); const taskId = allTasks.find(subject => subject.name === name)._id; return this.deleteTask(taskId); } }
  19. tests/todoPage.spec.ts import { TodoPage } from "../pages/todo.page"; import { TaskHttpService

    } from "../services/task.http.service"; describe('Todo Page', () => { const todoPage = new TodoPage(); const tasksHttpService = new TaskHttpService(); beforeEach(() => { todoPage.go(); }); it('Should be able to add task', async () => { const newTaskName = `mtreder${new Date().getTime()}`; await todoPage.getNewTaskInput().sendKeys(newTaskName); await todoPage.getNewTaskSubmitButton().click(); expect(await todoPage.getTaskByName(newTaskName).isDisplayed()).toEqual(true); await tasksHttpService.deleteTaskByName(newTaskName); }); }); await tasksHttpService.deleteTaskByName(newTaskName);
  20. services/task.http.service.ts public createTask(task: Task): Promise<Task> { return RxHR.post(this.SERVICE_URI, { json:

    true, body: task }).pipe(map(response => response.body as Task)).toPromise(); }
  21. tests/todoPage.spec.ts import { TodoPage } from "../pages/todo.page"; import { TaskHttpService

    } from "../services/task.http.service"; describe('Todo Page', () => { const todoPage = new TodoPage(); const tasksHttpService = new TaskHttpService(); let task; beforeEach(async () => { task = await tasksHttpService.createTask({name: 'mtreder' + new Date().getTime()}); todoPage.go(); }); it('Should display task on todo page', async () => { expect(await todoPage.getTaskByName(task.name).isDisplayed()).toEqual(true); }); it('Should be able to mark task as done', async () => { const taskEntry = todoPage.getTaskByName(task.name); (await todoPage.getMarkAsDoneButton(task.name)).click(); expect(await taskEntry.isPresent()).toEqual(false); }); afterEach( async () => { await tasksHttpService.deleteTask(task._id); }); }); task = await tasksHttpService.createTask({name: 'mtreder' + new Date().getTime()}); afterEach( async () => { await tasksHttpService.deleteTask(task._id); });
  22. conf.ts export const config = { specs: ['tests/**/*.spec.js'], capabilities: {

    'browserName': 'chrome' }, seleniumAddress: 'http://89.69.19.165:4444/wd/hub', framework: 'jasmine2', onPrepare: () => { //add reporters }, onComplete: () => { //generate html report } };
  23. conf.ts import * as jasmineReporters from 'jasmine-reporters'; import * as

    fs from 'fs-extra'; import { browser } from 'protractor'; onPrepare: () => { fs.emptyDir('./reports/xml/', console.log); fs.emptyDir('./reports/screenshots/', console.log); jasmine.getEnv().addReporter(new jasmineReporters.JUnitXmlReporter({ savePath: './reports/xml/', filePrefix: 'xmlresults', consolidateAll: true })); jasmine.getEnv().addReporter({ specDone: result => { browser.getCapabilities().then(capabilities => { const browserName = capabilities.get('browserName'); browser.takeScreenshot().then(png => { const stream = fs.createWriteStream(`./reports/screenshots/${browserName}- stream.write(new Buffer(png, 'base64')); stream.end(); }); }); } }); } fs.emptyDir('./reports/xml/', console.log); fs.emptyDir('./reports/screenshots/', console.log); jasmine.getEnv().addReporter(new jasmineReporters.JUnitXmlReporter({ savePath: './reports/xml/', filePrefix: 'xmlresults', consolidateAll: true })); jasmine.getEnv().addReporter({ specDone: result => { browser.getCapabilities().then(capabilities => { const browserName = capabilities.get('browserName'); browser.takeScreenshot().then(png => { const stream = fs.createWriteStream(`./reports/screenshots/${browserName}-$ {result.fullName}.png`); stream.write(new Buffer(png, 'base64')); stream.end(); }); }); } });
  24. conf.ts import * as htmlReporter from 'protractor-html-reporter-2'; import { browser

    } from 'protractor'; onComplete: () => { const capabilities$ = browser.getCapabilities(); capabilities$.then(capabilities => { const testConfig = { reportFile: 'Protractor Test Execution Report', outputPath: './reports/', outputFilename: 'ProtractorTestReport', screenshotPath: './screenshots', testBrowser: capabilities.get('browserName'), browserVersion: capabilities.get('version'), testPlatform: capabilities.get('platform'), modifiedSuiteName: false, screenshotsOnlyOnFailure: false }; new htmlReporter().from('./reports/xml/xmlresults.xml', testConfig); }); }
  25. conf.ts export const config = { specs: ['tests/**/*.spec.js'], capabilities: {

    'browserName': process.env.browser ? process.env.browser : 'chrome' }, seleniumAddress: getFirstAvailableInstance(), framework: 'jasmine2' } const request = require('sync-request'); const gridInstances: string[] = process.env.grid ? [process.env.grid] : [ 'http://localhost:4444', 'http://otherGrid.com', 'http://yetAnotherSelenium.com:4444' ]; function getFirstAvailableInstance(): string { let availableGrid; gridInstances.forEach(address => { try { let res = request('GET', `${address}/wd/hub`); if (res.statusCode === 200) availableGrid = `${address}/wd/hub` } catch(e) {} }); if (availableGrid) return availableGrid; throw new Error('No grid instance available'); } 'browserName': process.env.browser ? process.env.browser : 'chrome' seleniumAddress: getFirstAvailableInstance()