Несколько браузеров и шаблон объекта Page

мы используем шаблон объекта страницы организовать наши внутренние тесты приложений 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"
    });
});

:

после того, как мы раздвоили новый браузер, как мы можем использовать те же поля и функции объекта страницы, но примененные к недавно созданному браузеру (browser2 в данном случае)?

другими словами, все element() вызовы здесь будут применяться к browser, но необходимо было применить к browser2. Как мы можем изменить контекст?


мысли:

  • один возможный подход здесь был бы изменить глобальный element = browser2.element временно находясь в контексте browser2. Проблема с этим подходом заключается в том, что у нас тоже есть browser.wait() вызовы внутри функций объекта страницы. Это значит, что browser = browser2 также должен быть установлен. В этом случае нам нужно будет запомнить browser глобальный объект в переменной temp и восстановить его, как только мы переключимся обратно в main browser контексте..

  • другой возможный подход было бы передать экземпляр браузера в объект страницы, что-то вроде:

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

    но для этого, вероятно, потребуется изменить каждый объект страницы, который у нас есть..

надеюсь, вопрос ясен - дайте мне знать, если он нуждается в уточнении.

2 ответов


посмотрите на мое решение. Я упрощенный пример, но мы используем этот подход в текущем проекте. В моем приложении есть страницы для обоих типов разрешений пользователя, и мне нужно выполнить некоторые сложные действия одновременно в обоих браузерах. Я надеюсь, что это может показать вам какой-то новый, лучший путь!

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

возможно, вы могли бы написать несколько функций, чтобы сделать регистрацию/запуск/переключение браузера более плавным. (В основном это ваш первый вариант с некоторой поддержкой.)

например:

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

таким образом, вы можете оставить объекты страницы как есть.

честно говоря, я думаю, что ваш второй подход чище... Использование глобальных переменных может откусить позже. Но если вы не хотите менять свой POs, это может также работать.

(Я не проверял его... извините за возможные опечатки / ошибки.) (Например, вы можете разместить функции поддержки в разделе onprepare вашего транспортира conf.)