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' }
}
};
Related
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'
}
};
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
}
}
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
I have managed to get my snippets working with the basic autocompletion:
ace.define("ace/snippets/bosun",["require","exports","module"], function(require, exports, module) {
exports.snippets = [
/* Sections */
{
name: "alert definition",
tabTrigger: "alertdef",
content:
"alert ${1:alertname} {\n\
warn =\n\
}\n"
},
{
name: "template def",
tabTrigger: "templatedef",
content:
"template ${1:templatename} {\n\
subject =\n\
}\n"
},
/* Funcs */
{
name: "avg reduction function",
tabTrigger: "avg",
content: "avg(${1:seriesSet})"
}
]
exports.scope = "bosun";
});
In the documentation on snippets it says:
When triggering a snippet through a menu or command you can configure
it to use the text selected prior to inserting the snippet in the
resulting code.
But I'm not clear on how I would create menu to list the snippets? (Ideally a menu that has submenus for each category of snippets, but happy to crawl first...)
Perhaps someone has a better way. But from reading the code in https://github.com/ajaxorg/ace/blob/master/lib/ace/snippets.js I have come up with:
$scope.aceLoaded = function (_editor) {
editor = _editor;
$scope.editor = editor;
editor.$blockScrolling = Infinity;
editor.focus();
editor.getSession().setUseWrapMode(true);
$scope.snippetManager = ace.require("ace/snippets").snippetManager;
$scope.bosunSnippets = $scope.snippetManager.snippetNameMap["bosun"];
editor.on("blur", function () {
$scope.$apply(function () {
$scope.items = parseItems();
});
});
};
$scope.listSnippets = function() {
var snips = $scope.snippetManager.snippetNameMap["bosun"];
if (snips) {
return Object.keys(snips)
}
return {};
}
$scope.insertSnippet = function(snippetName) {
$scope.snippetManager.insertSnippetForSelection($scope.editor, $scope.snippetManager.snippetNameMap["bosun"][snippetName].content);
$scope.editor.focus();
$scope.editor.tabstopManager.tabNext()
}
Which seems to work, perhaps there is a better way.
I found this working example of Inheritance Patterns that separates business logic and framework code. I'm tempted to use it as a boilerplate, but since it is an inheritance Pattern, then how can I extend the business logic (the methods in var Speaker)?
For instance, how can I extend a walk: method into it?
/**
* Object Speaker
* An object representing a person who speaks.
*/
var Speaker = {
init: function(options, elem) {
// Mix in the passed in options with the default options
this.options = $.extend({},this.options,options);
// Save the element reference, both as a jQuery
// reference and a normal reference
this.elem = elem;
this.$elem = $(elem);
// Build the dom initial structure
this._build();
// return this so we can chain/use the bridge with less code.
return this;
},
options: {
name: "No name"
},
_build: function(){
this.$elem.html('<h1>'+this.options.name+'</h1>');
},
speak: function(msg){
// You have direct access to the associated and cached jQuery element
this.$elem.append('<p>'+msg+'</p>');
}
};
// Make sure Object.create is available in the browser (for our prototypal inheritance)
// Courtesy of Papa Crockford
// Note this is not entirely equal to native Object.create, but compatible with our use-case
if (typeof Object.create !== 'function') {
Object.create = function (o) {
function F() {} // optionally move this outside the declaration and into a closure if you need more speed.
F.prototype = o;
return new F();
};
}
$.plugin = function(name, object) {
$.fn[name] = function(options) {
// optionally, you could test if options was a string
// and use it to call a method name on the plugin instance.
return this.each(function() {
if ( ! $.data(this, name) ) {
$.data(this, name, Object.create(object).init(options, this));
}
});
};
};
// With the Speaker object, we could essentially do this:
$.plugin('speaker', Speaker);
Any ideas?
How about simply using JavaScript's regular prototype inheritance?
Consider this:
function Speaker(options, elem) {
this.elem = $(elem)[0];
this.options = $.extend(this.defaults, options);
this.build();
}
Speaker.prototype = {
defaults: {
name: "No name"
},
build: function () {
$('<h1>', {text: this.options.name}).appendTo(this.elem);
return this;
},
speak: function(message) {
$('<p>', {text: message}).appendTo(this.elem);
return this;
}
};
Now you can do:
var pp = new Speaker({name: "Porky Pig"}, $("<div>").appendTo("body"));
pp.speak("That's all folks!");
Speaker.prototype.walk = function (destination) {
$('<p>', {
text: this.options.name + " walks " + destination + ".",
css: { color: "red" }
}).appendTo(this.elem);
return this;
}
pp.walk("off the stage");
Runnable version:
function Speaker(options, elem) {
this.elem = $(elem)[0];
this.options = $.extend(this.defaults, options);
this.build();
}
Speaker.prototype = {
defaults: {
name: "No name"
},
build: function () {
$('<h1>', {text: this.options.name}).appendTo(this.elem);
return this;
},
speak: function(message) {
$('<p>', {text: message}).appendTo(this.elem);
return this;
}
};
var pp = new Speaker({name: "Porky Pig"}, $("<div>").appendTo("body"));
pp.speak("That's all folks!");
Speaker.prototype.walk = function (destination) {
$('<p>', {
text: this.options.name + " walks " + destination + ".",
css: { color: "red" }
}).appendTo(this.elem);
return this;
}
pp.walk("off the stage");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>