0

I'm following a page object pattern for use with WebDriver JS based automation code. The tutorial called for a page object that looks something like what's below using Object.create() to create an object that extends a simple Page object -- but their example didn't cover having functions that referenced this from inside the created object. I've tried changing the the object param get and value around a couple different ways in the property configuration and haven't noticed a difference so far.

page.js:

"use strict";

function Page() {

}

Page.prototype.open = function(path) {
    browser.url('/' + path);
};

Page.prototype.getUrl = function() {
    return browser.getUrl();
};

module.exports = new Page();

login.page.js:

"use strict";

var Page = require('./page');

var LoginPage = Object.create(Page, {
    email:          { value: 'input#email' } ,
    password:       { value: 'input#password' } ,
    loginButton:    { value: 'button#login-button' },
    errorText:      { value: 'span.help-block' },

    loginErrorMessage: { value: 'These credentials do not match our records.' },

    open: { value: function() {
        Page.open('login');
    }},

    loginAsTestUser: { value: function() {
        this.open(); 
        browser.logger.info('Logging in');
        this.login('test@dev.com', 'test');
    }},

    login: { value: function(email, password) {
        browser.element(this.email).setValue(email);
        browser.element(this.password).setValue(password);
        browser.element(this.loginButton).click();
    }}
});

module.exports = LoginPage;

test.spec.js

"use strict";

var LoginPage = require('../pages/login.page.js');
var Header = require('../pages/header.page.js');

describe('Navigation bar', function() {
    before('open login page', LoginPage.open);

    it('should be visible', function() {
        browser.isVisibleWithinViewport(Header.navbar).should.be.true;
    });

    describe('when logged in', function() {
        before('login', LoginPage.loginAsTestUser);

        it('should have the Add dropdown list displayed', function() {
            browser.isVisibleWithinViewport(Header.addDropdownButton).should.be.true;
        });

        after('logout', Header.logout);
    });
});

Here's the error for this.open:

1) Navigation bar when logged in "before all" hook:
this.open is not a function
running chrome
TypeError: this.open is not a function
    at Context.<anonymous> (/code/tests/test/pages/login.page.js:20:18)
    at Promise.F (/code/tests/node_modules/core-js/library/modules/_export.js:35:28)

And when this.open(); is replaced with its function to let the code progress, this.login(); errors as well:

1) Navigation bar when logged in "before all" hook:
this.login is not a function
running chrome
TypeError: this.login is not a function
    at Context.<anonymous> (/code/tests/test/pages/login.page.js:24:18)
    at Promise.F (/code/tests/node_modules/core-js/library/modules/_export.js:35:28)

I guess I could see this not meaning much in that property config object being passed to Object.create but I'm not sure how else you'd do this - the documentation for Object.defineProperties doesn't even reference an example that works this way.

Edit: This is different from the proposed duplicate question: How to access the correct `this` inside a callback? - I am talking about referencing variable properties of an object while I'm creating it because the variables build on each other. The proposed duplicate is talking about JavaScript scope issues in general with this - this particular question is a lot more focused and is more about how to build properties of an object using other properties of that object before that object is fully created.

anjunatl
  • 1,027
  • 2
  • 11
  • 24
  • It might be that the 'this' you're trying to access is in the local scope (the function you're calling it scope). What you would want to try is to attach your open() method to the Class's scope. – James Jul 04 '17 at 23:34
  • What is `Page` here? – Andrew Li Jul 04 '17 at 23:44
  • 1
    How are you calling `loginAsTestUser`. The value of `this` depends almost entirely on how a function is called, not on how it is defined. – slebetman Jul 04 '17 at 23:46
  • I'll update the example, one moment – anjunatl Jul 04 '17 at 23:55
  • The example is updated, thanks. – anjunatl Jul 04 '17 at 23:59
  • @anjunatl What is `Page` though? A constructor? Plain object? – Andrew Li Jul 05 '17 at 00:03
  • @AndrewLi As I understand it, it's a plain object. I could be wrong. – anjunatl Jul 05 '17 at 00:04
  • @Adrián Do you mean like this - https://hastebin.com/izasikofag.js ? It works, but the code is a lot more verbose now. Is that the only way there is to access those more internal properties and methods? – anjunatl Jul 05 '17 at 00:07
  • 1
    Did you get that `page.js` from the tutorial (that you forgot to link)? `module.exports = new Page` is horribly misguided. – Bergi Jul 05 '17 at 00:07
  • @Bergi I'll find the link, thanks for catching that – anjunatl Jul 05 '17 at 00:11
  • I had the wrong spec example in place, it wasn't calling `LoginPage.loginAsTestUser` - thanks @slebetman - it's updated now – anjunatl Jul 05 '17 at 00:11
  • @Bergi The post is updated - it's http://webdriver.io/guide/testrunner/pageobjects.html – anjunatl Jul 05 '17 at 00:13
  • 1
    Yes, `before('login', LoginPage.loginAsTestUser);` is the culprit. You need to bind the method to the instance (see the duplicate) or [not use `this` at all](https://stackoverflow.com/q/10711064/1048572) which might work better in your singleton scenario – Bergi Jul 05 '17 at 00:14
  • @Bergi While I understand how this is conceptually a duplicate question, the solution related to `Object.create` / `Object.defineProperties` doesn't appear to be covered in the linked question. – anjunatl Jul 05 '17 at 00:16
  • @Bergi re: a solution that doesn't use this, would that be like this? Thanks https://hastebin.com/izasikofag.js - It's verbose as far as writing out the code goes, but I bet the `{ value: foo }` assignments could be automated through a function, so it might not be so bad. – anjunatl Jul 05 '17 at 00:17
  • @anjunatl That doesn't really matter. As you said, the concept is the same. The same principle applies, it doesn't matter how you created the object. – Andrew Li Jul 05 '17 at 00:17
  • @anjunatl The solution is not related to `Object.create`/`Object.defineProperties` at all, that's why it doesn't matter – Bergi Jul 05 '17 at 00:18
  • 1
    @anjunatl You could just refer to `LoginPage.…` statically instead of `this.…`. Although your solution with the scoped variables works just as well. Oh, and since you refer to `Page.open` instead of `this.open`, you actually don't need `Object.create` inheritance at all. Just return an object literal. – Bergi Jul 05 '17 at 00:19
  • @Bergi Took a min to make sure I had the test correct, but referring to `LoginPage.foo` worked. I thought about trying that earlier today, I don't know why I didn't think that'd work. Next time I'll just try it anyway & see if I surprise myself. Thanks! – anjunatl Jul 05 '17 at 00:25
  • @Bergi If you'd like to post that as the answer, I'll accept it – anjunatl Jul 05 '17 at 00:25

0 Answers0