Подтвердить что ты не робот

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

Мы используем шаблон объекта страницы для организации наших внутренних тестов приложений 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 и восстановить его, как только мы вернемся к основному контексту browser.

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

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

Например:

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, это также может работать.

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