多个浏览器和页面对象模式

时间:2015-06-16 17:21:52

标签: javascript selenium testing protractor pageobjects

我们使用页面对象模式来组织我们的内部AngularJS应用程序测试。

以下是我们的示例页面对象:

var LoginPage = function () {
    this.username = element(by.id("username"));
    this.password = element(by.id("password"));

    this.loginButton = element(by.id("submit"));
}

module.exports = LoginPage;

在单浏览器测试中,很清楚如何使用它:

var LoginPage = require("./../po/login.po.js");

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

    beforeEach(function () {
        browser.get("/#login");

        scope.page = new LoginPage();
    });

    it("should successfully log in a user", function () {
        scope.page.username.clear();
        scope.page.username.sendKeys(login);
        scope.page.password.sendKeys(password);
        scope.page.loginButton.click();

        // assert we are logged in
    });
});

但是,当涉及多个浏览器实例化的测试并且需要在单个测试中切换它们时,如何在多个浏览器中使用相同的页面对象变得不清楚:

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

    beforeEach(function () {
        browser.get("/#login");

        scope.page = new LoginPage();
    });

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

        // assert we are logged in

        // fire up a different browser and log in
        var browser2 = browser.forkNewDriverInstance();

        // the problem is here - scope.page.username.clear() would be applied to the main "browser"
    });
});

问题:

在我们分叉了一个新浏览器之后,我们如何使用相同的Page Object字段和函数,但是应用于新实例化的浏览器(在这种情况下为browser2)?

换句话说,此处的所有element()次调用都将应用于browser,但需要应用于browser2。我们如何切换上下文?

思想:

  • 此处可能的一种方法是在browser2的上下文中临时redefine the global element = browser2.element。这种方法的问题是我们在页面对象函数中也有browser.wait()个调用。这意味着还应设置browser = browser2。在这种情况下,我们需要记住临时变量中的browser全局对象,并在切换回主browser上下文后恢复它。

  • 另一种可能的方法是将浏览器实例传递给页面对象,例如:

    var LoginPage = function (browserInstance) {
        browser = browserInstance ? browserInstance : browser;
        var element = browser.element;
    
        // ...
    }
    

    但这可能需要更改我们拥有的每个页面对象..

希望问题很清楚 - 如果需要澄清,请告诉我。

2 个答案:

答案 0 :(得分:6)

也许您可以编写一些函数来使浏览器注册/启动/切换更顺畅。 (基本上这是你的第一选择,有一些支持。)

例如:

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].$$;
}

你可以在你的例子中使用它:

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

因此,您可以保留页面对象。

老实说,我觉得你的第二种方法更清洁...... 使用全局变量可以稍后回顾。 但是,如果您不想更改您的PO,这也可以。

(我没有测试过...抱歉可能出现错别字/错误。) (例如,您可以将支持函数放置到量角器conf的onprepare部分。)

答案 1 :(得分:3)

看看我的解决方案。我简化了示例,但我们在当前项目中使用此方法。我的应用程序有两个用户权限类型的页面,我需要在两个浏览器中同时执行一些复杂的操作。我希望这可能会给你一些新的,更好的方法!

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