Вложенные объекты страницы в транспортирторе

Вопрос:

Что такое канонический способ определения вложенных объектов страницы в 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")));
       }
   }
}