Вложенные объекты страницы в транспортирторе
Вопрос:
Что такое канонический способ определения вложенных объектов страницы в Protractor?
Случай использования:
У нас сложная страница, состоящая из нескольких частей: панели фильтра, сетки, итоговой части, панели управления сбоку. Помещение всех определений элементов и методов в один файл, а один объект страницы не работает и масштабируется - он становится беспорядочным, который трудно поддерживать.
Ответы
Ответ 1
Идея заключается в том, что в качестве точки входа в качестве каталога-каталога указывается объект Page Object как пакет - каталог с index.js
. Объект родительской страницы будет действовать как контейнер для объектов дочерних страниц, которые в этом случае имеют значение "часть экрана".
Объект родительской страницы будет определен внутри index.js
, и он будет содержать все определения объектов дочерней страницы, например:
var ChildPage1 = require("./page.child1.po"),
ChildPage2 = require("./page.child2.po"),
var ParentPage = function () {
// some elements and methods can be defined on this level as well
this.someElement = element(by.id("someid"));
// child page objects
this.childPage1 = new ChildPage1(this);
this.childPage2 = new ChildPage2(this);
}
module.exports = new ParentPage();
Обратите внимание, как this
передается в конструкторы объектов дочерней страницы. Это может потребоваться, если для объекта дочерней страницы потребуется доступ к элементы или методы объекта родительской страницы.
Объект дочерней страницы будет выглядеть следующим образом:
var ChildPage1 = function (parent) {
// element and method definitions here
this.someOtherElement = element(by.id("someotherid"));
}
module.exports = ChildPage1;
Теперь было бы довольно удобно использовать этот объект страницы. Вам просто требуется объект родительской страницы и использовать точечную нотацию, чтобы получить доступ к объектам подстраницы:
var parentPage = requirePO("parent");
describe("Test Something", function () {
it("should test something", function () {
// accessing parent
parentPage.someElement.click();
// accessing nested page object
parentPage.childPage1.someOtherElement.sendKeys("test");
});
});
requirePO()
является вспомогательной функцией для облегчения импорта.
Пример структуры каталогов объектов вложенных страниц из одного из наших проектов автоматизации тестирования:
![введите описание изображения здесь]()
Ответ 2
Это более общая тема, когда дело доходит до объектов страницы и как их поддерживать. Некоторое время назад я наткнулся на одну из методик Pattern Object Pattern Pattern, которые мне нравились, и имел для меня большой смысл.
Вместо того, чтобы создавать объекты дочерних страниц в объектах родительской страницы, было бы идеально следовать концепции javascript Prototypal Inheritance. Это имеет целый ряд преимуществ, но сначала позвольте мне показать, как мы можем это достичь:
Сначала мы создадим объект родительской страницы ParentPage
:
// parentPage.js
var ParentPage = function () {
// defining common elements
this.someElement = element(by.id("someid"));
// defining common methods
ParentPage.prototype.open = function (path) {
browser.get('/' + path)
}
}
module.exports = new ParentPage(); //export instance of this parent page object
Мы всегда экспортируем экземпляр объекта страницы и никогда не создадим этот экземпляр в тесте. Поскольку мы пишем тесты от конца до конца, мы всегда рассматриваем страницу как конструкцию без учета состояния так же, как каждый HTTP-запрос является конструкцией без состояния.
Теперь создадим объекты дочерней страницы ChildPage
, мы будем использовать метод Object.create
для наследования прототипа нашей родительской страницы:
//childPage.js
var ParentPage = require('./parentPage')
var ChildPage = Object.create(ParentPage, {
/**
* define elements
*/
username: { get: function () { return element(by.css('#username')); } },
password: { get: function () { return element(by.css('#password')); } },
form: { get: function () { return element(by.css('#login')); } },
/**
* define or overwrite parent page methods
*/
open: { value: function() {
ParentPage.open.call(this, 'login'); // we are overriding parent page open method
} },
submit: { value: function() {
this.form.click();
} }
});
module.exports = ChildPage
мы определяем локаторы в функциях геттера. Эти функции вычисляются, когда вы действительно получаете доступ к свойству, а не при создании объекта. При этом вы всегда запрашиваете элемент, прежде чем выполнять действие над ним.
Метод Object.create
возвращает экземпляр этой страницы, поэтому мы можем сразу использовать его.
// childPage.spec.js
var ChildPage = require('../pageobjects/childPage');
describe('login form', function () {
it('test user login', function () {
ChildPage.open();
ChildPage.username.sendKeys('foo');
ChildPage.password.sendKeys('bar');
ChildPage.submit();
});
Обратите внимание, что нам требуется только объект дочерней страницы и использование/переопределение родительских страниц в наших спецификациях. Ниже приведены преимущества этого шаблона проектирования:
- удаляет жесткую связь между родительскими и дочерними страницами
- способствует наследованию между объектами страницы
- ленивая загрузка элементов
- инкапсуляция методов и действий
- чище и гораздо проще понять отношения элементов вместо
parentPage.childPage.someElement.click();
Я нашел этот шаблон дизайна в руководстве разработчика webdriverIO, большинство из описанных выше методов были взяты из этого руководства. Не стесняйтесь изучить его и сообщить мне свои мысли!
Ответ 3
Я не использую Protractor, но, возможно, вы можете попробовать эту идею ниже - по крайней мере, она работает для меня до сих пор:
Я использую то, что вы можете назвать "Component Object" - я делю страницу на компоненты или части и предполагаю, что мне предоставляется область каждого компонента, я ищу и добавляю элементы к компонентам на основе их областей. Таким образом, я могу легко использовать одни и те же/похожие компоненты на разных страницах.
Например, со страницей http://google.com я делю его на 3 части:
Скажем, мы назовем эти 3 части: Заголовок, SearchForm, Нижний колонтитул
Код для каждой части будет примерно таким:
class Header {
public Header(SearchContext context){
_context = context;
}
WebElement GmailLink {
get {
return _context.FindElement(By.CssSelector("[data-pid='23']"));
}
}
WebElement ImagesLink {
get {
return _context.FindElement(By.CssSelector("[data-pid='2']"));
}
}
SearchContext _context;
}
class SearchForm{
public Header(SearchContext context){
_context = context;
}
WebElement SearchTextBox {
get {
return _context.FindElement(By.Name("q")):
}
}
WebElement SearchButton {
get {
return _context.FindElement(By.Name("btnK")):
}
}
SearchContext _context;
}
..
И код страницы google.com будет выглядеть следующим образом:
class GoogleComPage{
WebDriver _driver;
public GoogleCompage(driver){
_driver = driver;
}
public Header Header{
get {
return new Header(_driver.FindElement(By.Id("gb")));
}
}
public SearchForm SearchForm{
get {
return new SearchForm(_driver.FindElement(By.Id("tsf")));
}
}
}