多个浏览器和页面对象模式
Multiple browsers and the Page Object pattern
我们正在使用 页面对象模式 来组织我们内部的 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"
});
});
问题:
在我们 fork 一个新的浏览器之后,我们如何使用相同的页面对象字段和函数,但应用于新实例化的浏览器(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;
// ...
}
但这可能需要更改我们拥有的每个页面对象..
希望问题清楚 - 如果需要澄清,请告诉我。
也许您可以编写一些函数来使浏览器 registration/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();
});
});
因此您可以保持页面对象不变。
老实说,我认为您的第二种方法更简洁...
使用全局变量可能会在以后反咬一口。
但是如果你不想改变你的采购订单,这也可以。
(我没有测试它...抱歉可能 typos/errors。)
(例如,您可以将支持功能放在量角器 conf 的 onprepare 部分。)
看看我的解决方案。我简化了示例,但我们在当前项目中使用了这种方法。我的应用程序有两种用户权限类型的页面,我需要在两种浏览器中同时执行一些复杂的操作。我希望这可以向您展示一些新的、更好的方法!
"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();
我们正在使用 页面对象模式 来组织我们内部的 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"
});
});
问题:
在我们 fork 一个新的浏览器之后,我们如何使用相同的页面对象字段和函数,但应用于新实例化的浏览器(browser2
在这种情况下)?
换句话说,这里所有的element()
调用都会应用到browser
,但需要应用到browser2
。我们如何切换上下文?
想法:
一种可能的方法是在
browser2
的上下文中暂时 redefine the globalelement = browser2.element
。这种方法的问题是我们在页面对象函数内部也有browser.wait()
调用。这意味着browser = browser2
也应该被设置。在这种情况下,我们需要记住临时变量中的browser
全局对象,并在切换回主browser
上下文后恢复它..另一种可能的方法是将浏览器实例传递到页面对象中,例如:
var LoginPage = function (browserInstance) { browser = browserInstance ? browserInstance : browser; var element = browser.element; // ... }
但这可能需要更改我们拥有的每个页面对象..
希望问题清楚 - 如果需要澄清,请告诉我。
也许您可以编写一些函数来使浏览器 registration/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();
});
});
因此您可以保持页面对象不变。
老实说,我认为您的第二种方法更简洁... 使用全局变量可能会在以后反咬一口。 但是如果你不想改变你的采购订单,这也可以。
(我没有测试它...抱歉可能 typos/errors。) (例如,您可以将支持功能放在量角器 conf 的 onprepare 部分。)
看看我的解决方案。我简化了示例,但我们在当前项目中使用了这种方法。我的应用程序有两种用户权限类型的页面,我需要在两种浏览器中同时执行一些复杂的操作。我希望这可以向您展示一些新的、更好的方法!
"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();