Slide 1

Slide 1 text

@maciejtreder

Slide 2

Slide 2 text

Why?

Slide 3

Slide 3 text

What?

Slide 4

Slide 4 text

How?

Slide 5

Slide 5 text

TypeScript TypeScript ES2016 ES2015 ES5 • Superset of JS which compiles to JavaScript • Strongly typed • Object-oriented • Classes, inheritance, interfaces

Slide 6

Slide 6 text

Protractor • Built on top of WebDriverJS • Supports Angular-specific locator strategies • Automatic waiting

Slide 7

Slide 7 text

Jasmine • Behavior-driven framework for testing JS code • Does not require a DOM • Clean and obvious syntax [ expect(a).toBe(true) ]

Slide 8

Slide 8 text

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.] - Initialising WebDriverServlet 10:56:55.927 INFO [SeleniumServer.boot] - Selenium Server is up and running on port 4444

Slide 9

Slide 9 text

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" } }

Slide 10

Slide 10 text

tsconfig.json { "compilerOptions": { "outDir": "build", "lib": [ "dom", "es2017" ] }, "exclude": [ "node_modules" ] }

Slide 11

Slide 11 text

conf.ts export const config = { specs: ['tests/**/*.spec.js'], capabilities: { 'browserName': 'chrome' }, seleniumAddress: ‘http://localhost:4444/wd/hub', framework: 'jasmine2', };

Slide 12

Slide 12 text

Jasmine • describe() • beforeAll() • beforeEach() • it() • expect(what?) • toBe() • toBeCloseTo() • toBeGreater() • afterEach() • afterAll()

Slide 13

Slide 13 text

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(); }); });

Slide 14

Slide 14 text

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. (/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

Slide 15

Slide 15 text

https://maciejtreder.github.io/e2e/static

Slide 16

Slide 16 text

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");

Slide 17

Slide 17 text

Page Objects

Slide 18

Slide 18 text

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}"]`)); } }

Slide 19

Slide 19 text

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();

Slide 20

Slide 20 text

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; public getTitle(): ElementFinder { return this.title; } }

Slide 21

Slide 21 text

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 { const title = await this.getTitle().getText(); return title === 'Todo'; } }

Slide 22

Slide 22 text

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 { const title = await this.getTitle().getText(); return title === 'What you have done so far:'; } }

Slide 23

Slide 23 text

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);

Slide 24

Slide 24 text

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 { const liObject = await this.getTaskByName(name); return liObject.element(by.css('span')); }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Cleanup

Slide 27

Slide 27 text

http calls with protractor

Slide 28

Slide 28 text

model/task.model.ts export interface Task { readonly _id?: string; name: string; readonly status?: string; }

Slide 29

Slide 29 text

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 { return RxHR.delete(this.SERVICE_URI + `/delete/${id}`).pipe(map(response => { return; })).toPromise(); } public async deleteTaskByName(name: string): Promise { 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); } }

Slide 30

Slide 30 text

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);

Slide 31

Slide 31 text

services/task.http.service.ts public createTask(task: Task): Promise { return RxHR.post(this.SERVICE_URI, { json: true, body: task }).pipe(map(response => response.body as Task)).toPromise(); }

Slide 32

Slide 32 text

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); });

Slide 33

Slide 33 text

Reporting

Slide 34

Slide 34 text

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 } };

Slide 35

Slide 35 text

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(); }); }); } });

Slide 36

Slide 36 text

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); }); }

Slide 37

Slide 37 text

Reporting

Slide 38

Slide 38 text

Jenkins

Slide 39

Slide 39 text

portainer.io

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

https://github.com/maciejtreder/e2e 4b589dd8d5ae74c082688306e83c7fce0138fe1c

Slide 43

Slide 43 text

@maciejtreder