量角器中的嵌套页面对象

Nested Page Objects in Protractor

问题:

在量角器中定义嵌套页面对象的规范方法是什么?

用例:

我们有一个复杂的页面,由多个部分组成:过滤面板、网格、摘要部分、一侧的控制面板。将所有元素和方法定义放入单个文件和单个页面对象中是行不通的,也无法扩展——它变得一团糟,难以维护。

想法是将页面对象定义为包 - 以index.js 作为入口点的目录。父页面对象将充当子页面对象的 容器 ,在这种情况下具有 "part of a screen" 含义。

父页面对象将在 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();

Note how this is passed into the child page object constructors. This might be needed if a child page object would need access to the parent page object's elements or methods.

子页面对象如下所示:

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

is a helper function to ease imports.


来自我们的一个测试自动化项目的示例嵌套页面对象目录结构:

当涉及到页面对象以及如何维护它们时,这更像是一个一般性的话题。前段时间我偶然发现了一种我喜欢的页面对象设计模式技术,它对我来说很有意义。

最好遵循 javascript 的 原型继承 概念,而不是在父页面对象中实例化子页面对象。这有很多好处,但首先让我展示一下我们如何实现它:

首先我们将创建我们的父页面对象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

We will always export an instance of a page object and never create that instance in the test. Since we are writing end to end tests we always see the page as a stateless construct the same way as each http request is a stateless construct.

现在让我们创建我们的子页面对象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's open method
} },
submit: { value: function() {
    this.form.click();
} }
});
module.exports = ChildPage

we are defining locators in getter functions, These functions get evaluated when you actually access the property and not when you generate the object. With that you always request the element before you do an action on it.

Object.create 方法 returns 该页面的一个实例,以便我们可以立即开始使用它。

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

请注意,在我们的规范中我们只需要子页面对象和 utilizing/overriding 父页面对象。以下是这种设计模式的好处:

  • 消除父子页面对象之间的紧密耦合
  • 促进页面对象之间的继承
  • 延迟加载元素
  • 方法和动作的封装
  • parentPage.childPage.someElement.click();
  • 更清晰且更容易理解元素关系

我在webdriverIO's 开发者指南中找到了这个设计模式,我上面解释的大部分方法都来自那个指南。随意探索它,让我知道你的想法!

我不使用量角器,但也许你可以试试下面的想法 - 至少,到目前为止它对我来说效果很好:

我使用你可以调用的东西 "Component Object" - 我将一个页面分成组件或部分,假设我得到了每个组件的范围,我根据它们的范围搜索和添加元素到组件。这样,我可以轻松地在不同页面中重用 same/similar 组件。

例如,页面 http://google.com,我将其分为 3 个部分: 假设我们将这 3 个部分命名为:HeaderSearchFormFooter

每个部分的代码如下所示:

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