Multiple browsers and the Page Object pattern

左心房为你撑大大i 提交于 2019-11-30 08:37:16

Look at my solution. I simplified example, but we are using this approach in current project. My app has pages for both user permissions types, and i need to do some complex actions same time in both browsers. I hope this might show you some new, better way!

"use strict";

//In config, you should declare global browser roles. I only have 2 roles - so i make 2 global instances
//Somewhere in onPrepare() function
global.admin = browser;
admin.admin = true;

global.guest = browser.forkNewDriverInstance();
guest.guest = true;

//Notice that default browser will be 'admin' example:
// let someElement = $('someElement'); // this will be tried to be found in admin browser.



class BasePage {
    //Other shared logic also can be added here.
    constructor (browser = admin) {
        //Simplified example
        this._browser = browser
    }
}

class HomePage extends BasePage {
    //You will not directly create this object. Instead you should use .getPageFor(browser)
    constructor(browser) {
        super(browser);

        this.rightToolbar = ToolbarFragment.getFragmentFor(this._browser);
        this.chat = ChatFragment.getFragmentFor(this._browser);
        this.someOtherNiceButton = this._browser.$('button.menu');
    }

    //This function relies on params that we have patched for browser instances in onPrepare();
    static getPageFor(browser) {
        if (browser.guest) return new GuestHomePage(browser);
        else if (browser.admin) return new AdminHomePage(browser);
    }

    openProfileMenu() {
        let menu = ProfileMenuFragment.getFragmentFor(this._browser);
        this.someOtherNiceButton.click();

        return menu;
    }
}


class GuestHomePage extends RoomPage {
    constructor(browser) {
        super(browser);
    }

    //Some feature that is only available for guest
    login() {
        // will be 'guest' browser in this case.
        this._browser.$('input.login').sendKeys('sdkfj'); //blabla
        this._browser.$('input.pass').sendKeys('2345'); //blabla
        this._browser.$('button.login').click();
    }
}


class AdminHomePage extends RoomPage {
    constructor(browser) {
        super(browser);
    }

    acceptGuest() {
        let acceptGuestButton = this._browser.$('.request-admission .control-btn.admit-user');
        this._browser.wait(EC.elementToBeClickable(acceptGuestButton), 10000,
                'Admin should be able to see and click accept guest button. ' +
                'Make sure that guest is currently trying to connect to the page');

        acceptGuestButton.click();
        //Calling browser directly since we need to do complex action. Just example.
        guest.wait(EC.visibilityOf(guest.$('.central-content')), 10000, 'Guest should be dropped to the page');
    }

}

//Then in your tests
let guestHomePage = HomePage.getPageFor(guest);
guestHomePage.login();
let adminHomePage = HomePage.getPageFor(admin);
adminHomePage.acceptGuest();
adminHomePage.openProfileMenu();
guestHomePage.openProfileMenu();

Maybe you could write few functions to make the the browser registration/start/switch smoother. (Basically it is your first option with some support.)

For example:

var browserRegistry = [];

function openNewBrowser(){
  if(typeof browserRegistry[0] == 'undefined'){
    browseRegistry[0] = {
      browser: browser,
      element: element,
      $: $,
      $$: $$,
      ... whatever else you need.
    }
  }
  var tmp = browser.forkNewDriverInstance();
  var id = browserRegistry.length;
  browseRegistry[id] = {
      browser: tmp,
      element: tmp.element,
      $: tmp.$,
      $$: tmp.$$,
      ... whatever else you need.
  }
  switchToBrowserContext(id);
  return id;
}
function switchToBrowserContext(id){
  browser=browseRegistry[id].browser;
  element=browseRegistry[id].element;
  $=browseRegistry[id].$;
  $$=browseRegistry[id].$$;
}

And you use it this way in your example:

describe("Login functionality", function () {
    var scope = {};

    beforeEach(function () {
        browser.get("/#login");
        scope.page1 = new LoginPage();
        openNewBrowser();
        browser.get("/#login");
        scope.page2 = new LoginPage();
    });

    it("should warn there is an opened session", function () {
        scope.page1.username.clear();
        scope.page1.username.sendKeys(login);
        scope.page1.password.sendKeys(password);
        scope.page1.loginButton.click();

        scope.page2.username.clear();
        scope.page2.username.sendKeys(login);
        scope.page2.password.sendKeys(password);
        scope.page2.loginButton.click();    
    });
}); 

So you can leave your page objects as they are.

To be honest I think your second approach is cleaner... Using global variables can bite back later. But if you don't want to change your POs, this can also work.

(I did not test it... sorry for the likely typos/errors.) (You can place the support functions to your protractor conf's onprepare section for example.)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!