Access Scope of elements in page object model - nightwatch.js

I am working on to automate a form to create a profile. While doing that I observed a something i need to understand. Please look at the code below and if it would be great if anyone can explain the reason behind this.
I am using page object for my project.
I have selectors defined under elements and i am accessing the elements/selectors in the functions down below.
I am not able to access the elements inside the function for the below code
For the this.api.perform function when I try to access the element(subjectGenderFemale) to click on it, it errors out with the error "ERROR: Unable to locate element: "#subjectGenderFemale" using: css selector". So i had to access it with the actual selector 'input[value="F"]'. Please refer to the code below # .api.perform.
'use strict';
var assert = require('assert');
var subjectJSON = require('../../../e2e/data/subjectData.json');
module.exports = {
elements: {
newSubjectButton: {
selector: '.btn.btn--primary'
},
subjectFirstName: {
selector: 'input[name^="first_name"]'
},
subjectLastName: {
selector: 'input[name^="last_name"]'
},
subjectDateOfBirth: {
selector: 'input[name^="date_of_birth"]'
},
subjectGenderFemale: {
selector: 'input[value="F"]'
},
subjectGenderMale: {
selector: 'input[value="M"]'
},
submitButton: {
selector: '.col.col-sm-offset-2.col-sm-8>div>form>button'
}
},
commands: [{
openCreateSubjectForm: function() {
return this
.waitForElementPresent('#newSubjectButton', 1000)
//the below href needs to change to proper element
.click('a[href="/subject/create"]')
},
populateSubjectForm: function() {
return this
.setValue('#subjectFirstName', subjectJSON["createSubject"]["firstName"])
.setValue('#subjectLastName', subjectJSON["createSubject"]["lastName"])
.setValue('#subjectDateOfBirth', subjectJSON["createSubject"]["dateOfBirth"])
.api.perform(function() {
if (subjectJSON["createSubject"]["gender"]=="F") {
this.api.click('input[value="F"]')
}else if (subjectJSON["createSubject"]["gender"]=="M") {
this.api.click('input[value="M"]')
}else if (subjectJSON["createSubject"]["gender"]=="Both") {
this.api.click('input[value="Both"]')
}else {
this.api.click('input[value="No preference"]')
}
})
},
submitCreateSubjectForm: function() {
return this.click('#submitButton');
}
}]
};

i got around this problem by accessing it via this.elements.subjectGenderFemale.selector

Related

Upgrading from 0.9.19 to 1.1.0 seems to break Page objects

I have a page object with this code:
var MyToolComp = require('./MyToolComponent').MyToolComponent;
var MyToolCommand = {
get: function (cssLocator, timeout) {
return new MyToolComp({client: this, locator: cssLocator, timeout: timeout});
},
assertMyToolCount: function (expectedMyToolesCount, timeoutMs) {
console.log('Validate number of MyTool in the page to be [' + expectedMyToolesCount + ']');
this.waitForElementsCount(this.section.john_container.selector, expectedMyToolesCount, timeoutMs);
return this;
},
};
module.exports = {
commands: [MyToolCommand],
sections: {
john_container: {
selector: '.john_container',
elements: {
john_MyTool: {
selector: '.john_MyTool'
},
header: {
selector: '.john_MyTool_header'
}
}
},
multi_widget: {
selector: '.john_multi_widget'
}
}
};
After upgrading to 1.1.0, I can't run this code in my test:
var myToolPage = browser.page.myTool();
myToolPage.assertMyToolCount(1);
When debugging, I see myToolPage has "section" var, but no commands.
If I remove sections and only do
module.exports = MyToolCommand;
or
module.exports = {
commands: [MyToolCommand]
}
Then I can run
myToolPage.assertMyToolCount(1);
But then it failes since
this.section.john_container.selector
Isn't defined.
What am I doing wrong? I can't find anything here or here. Is there anything else I can read that'll help me? What else should I know when upgrading NW? (This is my first time seeing anything nightwatch-related so I kinda have to learn as I go).
Thanks in advance :)
The way I fixed this was replacing
module.exports = {
commands: [MyToolCommand],
sections: {
john_container: {
selector: '.john_container',
elements: {
john_MyTool: {
selector: '.john_MyTool'
},
header: {
selector: '.john_MyTool_header'
}
}
},
multi_widget: {
selector: '.john_multi_widget'
}
}
};
With
exports.commands = MyToolCommand;
exports.sections = {
john_container: {
selector: '.john_container',
elements: {
john_MyTool: {
selector: '.john_MyTool'
},
header: {
selector: '.john_MyTool_header'
}
}
},
multi_widget: {
selector: '.john_multi_widget'
}
};

Only reloading resolves without reloading html

How can i force ui router to reload the resolves on my state without reloading the entire ui/controller since
I am using components and since the data is binded from the state resolve,
i would like to change some parameters (pagination for example) without forcing the entire ui to reload but just the resolves
resolve : {
data: ['MailingListService', '$transition$', function (MailingListService, $transition$) {
var params = $transition$.params();
var ml = params.id;
return MailingListService.getUsers(ml, params.start, params.count)
.then(function (result) {
return {
users: result.data,
totalCount: result.totalCount
}
})
}],
node: ['lists', '$transition$', function (lists, $transition$) {
return _.find(lists, {id: Number($transition$.params().id)})
}]
},
I would like to change $transition$.params.{start|count} and have the resolve updated without reloading the html.
What you requested is not possible out of the box. Resolves are only resolved, when the state is entered.
But: one way of refreshing data could be, to check for state parameter changes in $doCheck and bind them to the components by hand.
Solution 1
This could look something like this:
export class MyComponent {
constructor($stateParams, MailingListService) {
this.$stateParams = $stateParams;
this.MailingListService = MailingListService;
this.paramStart = null;
this.paramCount = null;
this.paramId = null;
this.data = {};
}
$doCheck() {
if(this.paramStart !== this.$stateParams.start ||
this.paramCount !== this.$stateParams.count ||
this.paramId !== this.$stateParams.id) {
this.paramStart = this.$stateParams.start;
this.paramCount = this.$stateParams.count;
this.paramId = this.$stateParams.id;
this.MailingListService.getUsers(this.paramId, this.paramStart, this.paramCount)
.then((result) => {
this.data = {
users: result.data,
totalCount: result.totalCount
}
})
}
}
}
Then you have no binding in the parent component anymore, because it "resolves" the data by itself, and you have to bind them to the child components by hand IF you insert them in the template of the parent component like:
<my-component>
<my-child data="$ctrl.data"></my-child>
</my-component>
If you load the children via views, you are obviously not be able to bind the data this way. There is a little trick, but it's kinda hacky.
Solution 2
At first, resolve an empty object:
resolve : {
data: () => {
return {
value: undefined
};
}
}
Now, assign a binding to all your components like:
bindings: {
data: '<'
}
Following the code example from above, where you resolve the data in $doCheck, the data assignment would look like this:
export class MyComponent {
[..]
$doCheck() {
if(this.paramStart !== this.$stateParams.start ||
this.paramCount !== this.$stateParams.count ||
this.paramId !== this.$stateParams.id) {
[..]
this.MailingListService.getUsers(this.paramId, this.paramStart, this.paramCount)
.then((result) => {
this.data.value = {
users: result.data,
totalCount: result.totalCount
}
})
}
}
}
And last, you check for changes in the child components like:
export class MyChild {
constructor() {
this.dataValue = undefined;
}
$doCheck() {
if(this.dataValue !== this.data.value) {
this.dataValue = this.data.value;
}
}
}
In your child template, you access the data with:
{{ $ctrl.dataValue | json }}
I hope, I made my self clear with this hack. Remember: this is a bit off the concept of UI-Router, but works.
NOTE: Remember to declare the parameters as dynamic, so changes do not trigger the state to reload:
params: {
start: {
dynamic: true
},
page: {
dynamic: true
},
id: {
dynamic: true
}
}

Parameter in $stateParams are undefined in resolve

I have a page showing a list of contact and clicking on one of the contacts in the view should switch to detail state as below:
dashboard-contacts-controller.js
vm.viewContact = function(contactId) {
console.log("Load contact " + contactId);
$state.go("app.dashboards_contact", {"id": contactId});
}
contacts-module.js
.state('app.dashboards_contact', {
url: '/dashboard-contact/:id',
views: {
'content#app': {
templateUrl: 'app/main/apps/dashboards/contacts/about/about.html',
controller: 'DashboardContactController as vm'
}
},
resolve: {
contact: ['DashboardContactsDataService', function($stateParams, DashboardContactsDataService) {
console.log($stateParams.id);
return DashboardContactsDataService.get($stateParams.id);
}
]
},
bodyClass: 'dashboard-contact'
});
$stateParams.id in resolve is always undefined.
You didnt inject $stateParams in resolve. Shouldn't it be
resolve: {
contact: ['$stateParams', DashboardContactsDataService', function($stateParams, DashboardContactsDataService) {
console.log($stateParams.id);
return DashboardContactsDataService.get($stateParams.id);
}
]
},

How to target tree elements using external commands in nightwatch.js?

I am looking for advice on how best to structure all the moving parts to get my nightwatch.js tests setup correctly using custom commands. I want to abstract out a command to load a dropdown menu, by putting it outside the page object, but it only works when I move the elements out of the tree section. How do I include the tree elements in the abstraction below - stuck atm. Thanks,
So my tests looks like this -
'xyz': function (client) {
var page = client.page.myPage();
page
.setDropDownButtonValue(page, '#createBtn', 'New Folder')
.api.useCss()
.waitForElementVisible('[placeholder="New Folder Name"]', 3000)
.setValue('[placeholder="New Folder Name"]', 'Whatever');
}
and my custom command - setDropDownButtonValue - looks like this
exports.command = function (scope, button, value) {
var xpathMatch = '//*[text()="' + value + '"]';
scope
.waitForElementVisible(button, 5000)
.click(button)
.waitForElementVisible('#dropdownMenu', 5000)
.api.useXpath();
return scope.
waitForElementVisible(xpathMatch, 5000)
.click(xpathMatch);
};
And myPage object looks like this -
'use strict';
module.exports = {
sections: {
tree: {
selector: '.drive-tree-container'
elements: {
createBtn: { selector: '[data-bind*="_onCreateActionClick"]' },
dropdownMenu: { selector: '[data-bind*="element: menuDOM"].dropdown-menu.create-menu' }
}
}
},
elements: {
tree: { selector: '.drive-tree-container' }
}
};
The above unit test only works when I remove the tree section -
'use strict';
module.exports = {
elements: {
createBtn: { selector: '[data-bind*="_onCreateActionClick"]' },
dropdownMenu: { selector: '[data-bind*="element: menuDOM"].dropdown-menu.create-menu' }
}
};

Marionette - throws error on `removeRegions` how to solve it

In my app, i have the regions as header,content,footer - in which on the login page, I don't want to use the header, and footer. for that, on onRender i remove the regions what i don't want to be.
But I am getting an error saying: Cannot read property 'empty' of undefined.
here is my template : (i use jade )
div#wrapper
script(type='text/template', id="appTemplate")
div#header
div#content
div#footer
script(type='text/template', id="loginTemplate")
div this is login template
here is my layout.js:
socialApp.AppLayout = Backbone.Marionette.LayoutView.extend({
el:'#wrapper',
template:'#appTemplate',
regions: {
header : '#header',
content : '#content',
footer : '#footer'
},
onRender : function () {
this.removeRegion("header", "#header"); //i am removing header alone here.
}
});
here is my controller.js
socialApp.loginController = Marionette.Controller.extend({
_initialize:function(){
this.loginView = new loginView({model:new loginModel});
this.layout.onRender(); //calling onRender from here...
this.layout.content.show(this.loginView);
}
});
But it's all not working. any one help me the correct way please?
You should never call methods that are prefixed with on manually. Those are there for your code to react to given events, in this case that the view’s render method was invoked.
I would suggest that you instead of trying to remove and then later re-add regions, you create two different layouts. Then when your router hits the login route, you render LoginLayout into your App’s root region, and for other routes, the ‘normal’ layout. Here’s how I solved something similar:
app.js:
var App = new Marionette.Application;
App.addRegions({ root: '#acme' });
// Instantiate User model
App.addInitializer(function()
{
this.user = new UserModel;
});
// Render App layout
App.addInitializer(function()
{
this.layout = this.user.get('id') ? new ContentLayoutView({ identifier: 'content' }) : new UserLayoutView({ identifier: 'user' });
this.root.show(this.layout);
// And let the routers decide what goes in the content region of each layout
this.router = {
content: new ContentRouter,
user: new UserRouter
};
});
layout/content.js
var ContentLayout = Marionette.LayoutView.extend(
{
identifier: 'content',
template: ContentLayoutTemplate,
regions: {
content: '[data-region="content"]',
panelLeft: '[data-region="panel-left"]',
panelRight: '[data-region="panel-right"]'
},
initialize: function()
{
this.content.once('show', function(view)
{
this.panelLeft.show(new PanelLeftView);
this.panelRight.show(new PanelRightView);
}.bind(this));
}
});
layout/user.js
var UserLayout = Marionette.LayoutView.extend(
{
identifier: 'user',
template: UserLayoutTemplate,
regions: {
content: '[data-region="content"]'
}
});
router/content.js
var ContentRouter = Marionette.AppRouter.extend(
{
routes: {
'(/)': '...'
},
createLayout: function(callback)
{
if(App.root.currentView.options.identifier != 'content')
{
var layout = new ContentLayoutView({ identifier: 'content' });
this.region = layout.content;
this.listenTo(layout, 'show', callback);
App.root.show(layout);
}
else
{
this.region = App.root.currentView.content;
callback();
}
},
execute: function(callback, args)
{
if(App.user.get('id'))
{
this.createLayout(function()
{
callback.apply(this, args);
}.bind(this));
}
else
App.router.user.navigate('login', true);
}
});
router/user.js
var UserRouter = Marionette.AppRouter.extend(
{
routes: {
'login(/)': 'showLogin',
'logout(/)': 'showLogout'
},
createLayout: function(callback)
{
if(App.root.currentView.options.identifier != 'user')
{
var layout = new UserLayoutView({ identifier: 'user' });
this.region = layout.content;
this.listenTo(layout, 'show', callback);
App.root.show(layout);
}
else
{
this.region = App.root.currentView.content;
callback();
}
},
execute: function(callback, args)
{
this.createLayout(function()
{
callback.apply(this, args);
}.bind(this));
},
showLogin: function()
{
var LoginView = require('view/detail/login');
this.region.show(new LoginView);
},
showLogout: function()
{
var LogoutView = require('view/detail/logout');
this.region.show(new LogoutView);
}
});

Resources