【问题标题】:Nested Page Objects in Protractor量角器中的嵌套页面对象
【发布时间】:2017-05-07 15:45:36
【问题描述】:

问题:

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

用例:

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

【问题讨论】:

    标签: javascript selenium protractor pageobjects project-organization


    【解决方案1】:

    这个想法是将页面对象定义为一个包 - 以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() 是一个简化导入的辅助函数。


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

    【讨论】:

    • 尝试掌握量角器一周,并准备拍摄自己处理异步 js 和 Promises,所以你的帖子很有帮助。 1)在 ParentPage 构造函数中,您正在实例化所有子页面对象,是否可以从父页面方法返回单个子页面?测试规范将调用父页面方法并获取子页面对象。 2)将诸如等待之类的常用方法抽象到基本页面对象并将其分配给其他页面对象的原型是否是一个好主意。 3) 任何指向对 Promises 有意义的链接的指针。谢谢
    • @Grasshopper 1) 是的,您可以将子页面对象封装到方法中,应该也可以。 2)你抽象得越多,你的测试就越可读(当然,所有东西总是太多了) 3) 至于 Protractor 中的承诺,请参阅protractortest.org/#/control-flow。希望对您有所帮助,谢谢!
    【解决方案2】:

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

    与其在父页面对象中实例化子页面对象,不如遵循 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's open method
    } },
    submit: { value: function() {
        this.form.click();
    } }
    });
    module.exports = ChildPage
    

    我们在 getter 函数中定义定位器,这些函数在您实际访问属性时进行评估,而不是在您生成对象时进行评估。这样,您总是在对其执行操作之前请求该元素。

    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's 开发者指南中找到了这种设计模式,我解释的上述大部分方法都取自该指南。随意探索它,让我知道你的想法!

    【讨论】:

    • 感谢您的详细解答。我仍然不确定原型继承对于嵌套页面对象是否更好。我们也在项目中使用它,但是我们需要从常见对象继承,例如 - 是/否、确认对话框、具有类似底层元素结构的屏幕。就可读性而言,我认为parentPage.childPage.someElement.click(); 是一件好事 - 在示例中它可能看起来很笨重,但在实际项目中它是,例如,queue.filters.refresh.click() - 非常好 - 我们立即了解这是什么来的来自..只是想法。
    • 另一点是..我认为这里甚至有一个逻辑上的混乱——对我们来说,父页面代表一个大屏幕,子页面代表屏幕的一部分——它是“一个包含许多”。但是,如果我们从继承的角度来看,这将意味着有点像从大屏幕继承的一部分......希望你明白我的意思。谢谢。
    • 在您提到的文章中指出 - “首先,我们需要一个称为 Page 的主页对象。它将包含所有页面对象都将从中继承的通用选择器或方法。”我认为它不处理包含可以跨页面重用或具有太多逻辑的组件的“大”页面。也不确定继承是否会减少耦合。它实际上使它紧密耦合。虽然原型继承与 java 等中的传统继承概念不同......
    • 当然,在您的情况下,子页面对象的可读性更有意义。我的想法是独立实例化页面对象并在我们的脚本中使用它们。如果我们在单个页面对象中实例化多个页面对象会更好吗? bcoz 当您在脚本中调用它们时,它看起来是这样的,我只是想了解它的好处!
    • @RamPasala 是的,我想这是解决问题的两种不同方法。知道量角器团队一般会推荐什么会很有趣。再次感谢您的见解!
    【解决方案3】:

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

    我使用可以称为“组件对象”的东西——我将页面划分为组件或部分,假设我被赋予了每个组件的范围,我根据它们的范围搜索并添加元素到组件。这样,我可以轻松地在不同页面中重用相同/相似的组件。

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

    【讨论】:

    • 是的,这正是我的答案——父页面对象就像子页面对象的“容器”——组件。很高兴我们有相同的想法。谢谢!
    • 那你在实现的过程中遇到了哪些困难?其实我也是用 JavaScript 写的,但是使用 theIntern 效果很好。
    • 没有困难,只是介绍了我们目前正在使用的方法并需要反馈。看来还有基于继承的不同解决方案。
    • 我明白了。通过我的自动化,我确实为组合框、复选框列表、日期选择器等基本元素编写 UIMap(我这么称呼它)......一个小组件是这些元素的集合,一个更大的组件是一个小组件的集合和也是基本元素。
    猜你喜欢
    • 2014-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-29
    • 1970-01-01
    • 1970-01-01
    • 2017-07-18
    • 1970-01-01
    相关资源
    最近更新 更多