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. @maciejtreder

    View Slide

  2. Why?

    View Slide

  3. What?

    View Slide

  4. How?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  14. 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

    View Slide

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

    View Slide

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

    View Slide

  17. Page Objects

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. 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';
    }
    }

    View Slide

  22. 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:';
    }
    }

    View Slide

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

    View Slide

  24. 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'));
    }

    View Slide

  25. 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

    View Slide

  26. Cleanup

    View Slide

  27. http calls with protractor

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. Reporting

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  37. Reporting

    View Slide

  38. Jenkins

    View Slide

  39. portainer.io

    View Slide

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

    View Slide

  41. View Slide

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

    View Slide

  43. @maciejtreder

    View Slide