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

E2E SPA tests with protractor

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

E2E SPA tests with protractor

Maciej Treder

October 11, 2019

Transcript

43 slides
  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()

More Decks by Maciej Treder

Other Decks in Programming