Getting the count of elements in a list from Nightwatch - nightwatch.js

I have the following DOM structure:
<div data-qa-id="criteriaDisplay">
<ul>
<li>...</li>
<li>...</li>
<li>...</li>
</ul>
</div>
I would like to write a command that returns the number of items in the list above. I am a newbie to Nightwatch. Here's my attempt to do this. My thinking is to start with the <ul> under the criteriaDisplay element and then count the <li> items under it. So I am trying to use the elementIdElements api, but it is not working:
var criteriaDisplayCommands = {
getNumberOfItems: function(callback) {
var self = this;
console.log(this);
return this.api.elementIdElements('#myList', 'css selector', 'li', function(result) {
callback.call(self, result);
});
}
};
module.exports = {
url: function() {
return this.api.launchUrl + '/search';
},
sections: {
criteriaDisplay: {
selector: '[data-qa-id="criteriaDisplay"]',
commands: [criteriaDisplayCommands],
elements: {
myList: 'ul'
}
}
}
};
Can someone please help me with the right way to do this?

I dont think this is right:
return this.api.elementIdElements('#myList', 'css selector', 'li', function(result) {
callback.call(self, result);
});
Since elementIdElements requires the first parameter to be an elementId. In your case its a css selector.
The second i'm not sure about is that you are using custom selectors in the selenium api. I dont think that this is gonna work (but i'm not sure about this)
From the doc's section about defining-elements:
Using the elements property allows you to refer to the element by its name with an "#" prefix, rather than selector, when calling element commands and assertions (click, etc).
Since you are using the selenium api and not a command or assertion, i dont think that it would be a valid input.
You could use elements for getting your result, or an combination of element and elementIdElements.
The simple one is only using elements as shown below:
this.api.elements('css selector', 'ul li', function (result) {
console.log(result.value.length) //(if there are 3 li, here should be a count of 3)
callback.call(self, result);
});`

Related

Autocompleter DWR: how to make search engine starts only when at least three characters are typed?

The following code is taken from the link:
http://tsamuel.wordpress.com/2007/05/17/direct-web-remoting-a-tutorial/
It is an autocomplete text field that uses DWR.
<script type="text/javascript">
new Autocompleter.DWR('personName', 'personListDiv', updatePersonList,{ valueSelector: function(obj){ return obj.name; },
partialChars: 2, choices: 10 }); </script>
The updatePersonList:
function updatePersonList(autocompleter, token) {
DWRPersonService.getAllPersons(function(data) { autocompleter.setChoices(data); });
}
What I would like to do is to configure the code above in a way that the search only starts when three characters are typed (at least). Tried to change partialChars value but it doesn't seem to work...
I don't know how partialChars works, i would do something like
function updatePersonList(autocompleter, token)
{
if(token.length < 3) return;
DWRPersonService.getAllPersons(function(data)
{
autocompleter.setChoices(data);
});
}

jquery/ajax load scripts - best practices

I'm trying to get the hang of using ajax loads (mostly via jquery) to make my site more efficient. Wondering if anyone can provide any suggestions re "best practices" for using ajax?
Is there a way to simplify a script for multiple ajax calls? For example, I currently have the working script:
$(document).ready(function() {
$('#dog').click(function () {
$('#body').load("dog.html");
});
$('#cat').click(function () {
$('#body').load("cat.html");
});
$('#bird').click(function () {
$('#body').load("bird.html");
});
$('#lizard').click(function () {
$('#body').load("lizard.html");
});
});
The script just gets longer and longer with each additional function. Is there a simpler, more efficient way to write this script to cover multiple load scripts?
Also, should I be using ajax loads to replace the majority of actual links?
Here is a suggestion, since the code you posted seems to have a pattern between the id and the filename:
$(document).ready(function () {
$(document).on('click', 'commonParentElementHere', function (e) {
$('#body').load(e.target.id + ".html");
});
});
This suggestion uses .on() and you just need to add a commonParentElementHere, a id or a class of the common parent of those elements.
Another option is to use a class on all elements that should be clickable, and then use the code passing the id to the html file name, like:
$(document).ready(function () {
$(document).on('click', '.theCOmmonClass', function () {
$('#body').load(this.id + ".html");
});
});
I'd say give all the elements you want to click on a class say ajax then.
$(document).ready(function() {
$('.ajax').click(function () {
$('#body').load(this.id + ".html");
});
});
Assuming that the id matches the file name the script can be simplified to:
$(document).ready(function() {
$('#dog,#cat,#bird,#lizard').click(function () {
var fileName = this.id + ".html";
$('#body').load(fileName);
});
});
This script simply specifies each id in a single selector that separates each id with a comma. This will calls the click function to be fired for each element. With the anonymous function attached to the click event, the id of each element is obtained and concatenated to create the file name which is then passed to the load function.
If the id doesn't always match the element you could use the following approach.
var mappings = [
{id: "fileName1", file:"file.html"},
{id: "fileName2", file:"file2.html"}
];
$(document).ready(function() {
for(var i = 0; i < mappings; i++){
createMapping(mappings[i]);
}
function createMapping(mapping){
$("#" + mapping.id).click(function(){
$('#body').load(mapping.file);
});
}
});

Reduce events in dynamic menu in AngularJS?

I've built a dynamic menu which also have highlighting. Now i got a problem, number of path-change events are increasing along with menu elements.
Of course, that's the result of applying directive on each element of the menu.
Custom directives at the moment is my most weak place, and i don't have idea how to refactor all this.
I also made an attempt to put directive in root element of menu (ul) in order to register watch once, but stuck on accessing the deep children elements (ul->li->a.href).
Here's the directive:
app.directive("testdir", function($location)
{
return {
restrict: 'A',
link: function(scope, element, attrs, controller) {
scope.$watch(function() { return $location.path(); }, function(path)
{
scope.$parent.firedEvents++;
path=path.substring(1);
if(path === element.children().attr("href").substring(2))
{
element.addClass("activeLink");
}
else
{
element.removeClass("activeLink");
}
})
}
};
And HTML Part:
<ul ng-app="test" ng-controller="MenuCtrl">
<li ng-repeat="link in menuDef" testdir>{{link.linkName}}</li>
</ul>
Whole example on JsFiddle
How this can be refactored? I'm exhausted.
and, i'm moving in the right direction? I have a feeling that this thing could be done in bit easier way, but maybe i'm wrong.
First of all, your firedEvents means how many time the callback has been called, not how many times the location actually changed, it's not the "number of path-change events"!
You have 20 (as in your fiddle) scopes are watching the location change, when you click on a different link other than the current active one, the location changes, ALL of the 20 scopes will see the change and call their own $watch callback functions, and each call to the callback will increase your firedEvents, so the result is what you have seen: the count goes up by 20.
Therefore, if you want to make the firedEvents to count how many time location has changed, you should move scope.$parent.firedEvents++; into the if. But keep in mind that every click will still cause the callback function be called by 20 times!
There are many ways to achieve the same effect you're trying to do here, I have a solution for you without digging into directive at all. Here you go:
HTML
<ul ng-controller="MenuCtrl">
<li ng-repeat="link in menuDef" ng-class="{activeLink: link.isActive}" ng-click="onLinkClick(link)">{{link.linkName}}
</li>
</ul>
JS
app.controller("MenuCtrl", function ($scope, $location) {
var menugen = [];
for (var i = 1; i <= 20; i++) {
menugen.push({
linkName: "Link " + i,
url: "#/url" + i
});
}
$scope.menuDef = menugen;
var activeLink = null;
$scope.onLinkClick = function (link) {
if (activeLink && $scope.activeLink !== link) {
activeLink.isActive = false;
}
link.isActive = true;
activeLink = link;
};
});
jsFiddle
Update
My first attempt is targeting simplicity, but as #VirtualVoid pointed out, it has a huge drawback -- it can't easily handle location change from outside of the menu.
Here, I came up a better solution: adding a directive to the ul, watch location change in there, and update activeLink in the callback function of the watch. In this way, one $watch is called, and only one callback will be called for a click.
JS
app.directive('menu', function ($location) {
return {
restrict: 'A',
controller: function ($scope, $location) {
var links = [];
this.registerLink = function (elem, path) {
links.push({
elem: elem,
path: path
});
};
$scope.$watch(function () {
return $location.path();
}, function (path) {
for (var i = 0; i < links.length; i++) {
if (path === links[i].path) {
links[i].elem.addClass('activeLink');
} else {
links[i].elem.removeClass('activeLink');
}
}
});
}
};
}).
directive("testdir", function () {
return {
restrict: 'A',
require: '^menu',
link: function (scope, element, attrs, controller) {
controller.registerLink(element, scope.link.url.substring(1));
}
};
});
HTML
<ul ng-app="test" ng-controller="MenuCtrl" menu>
<li ng-repeat="link in menuDef" testdir>
{{link.linkName}}
</li>
</ul>
jsFiddle: http://jsfiddle.net/jaux/MFYCX/

Calling a function when ng-repeat has finished

What I am trying to implement is basically a "on ng repeat finished rendering" handler. I am able to detect when it is done but I can't figure out how to trigger a function from it.
Check the fiddle:http://jsfiddle.net/paulocoelho/BsMqq/3/
JS
var module = angular.module('testApp', [])
.directive('onFinishRender', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
element.ready(function () {
console.log("calling:"+attr.onFinishRender);
// CALL TEST HERE!
});
}
}
}
});
function myC($scope) {
$scope.ta = [1, 2, 3, 4, 5, 6];
function test() {
console.log("test executed");
}
}
HTML
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" on-finish-render="test()">{{t}}</p>
</div>
Answer:
Working fiddle from finishingmove: http://jsfiddle.net/paulocoelho/BsMqq/4/
var module = angular.module('testApp', [])
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit(attr.onFinishRender);
});
}
}
}
});
Notice that I didn't use .ready() but rather wrapped it in a $timeout. $timeout makes sure it's executed when the ng-repeated elements have REALLY finished rendering (because the $timeout will execute at the end of the current digest cycle -- and it will also call $apply internally, unlike setTimeout). So after the ng-repeat has finished, we use $emit to emit an event to outer scopes (sibling and parent scopes).
And then in your controller, you can catch it with $on:
$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
//you also get the actual event object
//do stuff, execute functions -- whatever...
});
With html that looks something like this:
<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
<div>{{item.name}}}<div>
</div>
Use $evalAsync if you want your callback (i.e., test()) to be executed after the DOM is constructed, but before the browser renders. This will prevent flicker -- ref.
if (scope.$last) {
scope.$evalAsync(attr.onFinishRender);
}
Fiddle.
If you really want to call your callback after rendering, use $timeout:
if (scope.$last) {
$timeout(function() {
scope.$eval(attr.onFinishRender);
});
}
I prefer $eval instead of an event. With an event, we need to know the name of the event and add code to our controller for that event. With $eval, there is less coupling between the controller and the directive.
The answers that have been given so far will only work the first time that the ng-repeat gets rendered, but if you have a dynamic ng-repeat, meaning that you are going to be adding/deleting/filtering items, and you need to be notified every time that the ng-repeat gets rendered, those solutions won't work for you.
So, if you need to be notified EVERY TIME that the ng-repeat gets re-rendered and not just the first time, I've found a way to do that, it's quite 'hacky', but it will work fine if you know what you are doing. Use this $filter in your ng-repeat before you use any other $filter:
.filter('ngRepeatFinish', function($timeout){
return function(data){
var me = this;
var flagProperty = '__finishedRendering__';
if(!data[flagProperty]){
Object.defineProperty(
data,
flagProperty,
{enumerable:false, configurable:true, writable: false, value:{}});
$timeout(function(){
delete data[flagProperty];
me.$emit('ngRepeatFinished');
},0,false);
}
return data;
};
})
This will $emit an event called ngRepeatFinished every time that the ng-repeat gets rendered.
How to use it:
<li ng-repeat="item in (items|ngRepeatFinish) | filter:{name:namedFiltered}" >
The ngRepeatFinish filter needs to be applied directly to an Array or an Object defined in your $scope, you can apply other filters after.
How NOT to use it:
<li ng-repeat="item in (items | filter:{name:namedFiltered}) | ngRepeatFinish" >
Do not apply other filters first and then apply the ngRepeatFinish filter.
When should I use this?
If you want to apply certain css styles into the DOM after the list has finished rendering, because you need to have into account the new dimensions of the DOM elements that have been re-rendered by the ng-repeat. (BTW: those kind of operations should be done inside a directive)
What NOT TO DO in the function that handles the ngRepeatFinished event:
Do not perform a $scope.$apply in that function or you will put Angular in an endless loop that Angular won't be able to detect.
Do not use it for making changes in the $scope properties, because those changes won't be reflected in your view until the next $digest loop, and since you can't perform an $scope.$apply they won't be of any use.
"But filters are not meant to be used like that!!"
No, they are not, this is a hack, if you don't like it don't use it. If you know a better way to accomplish the same thing please let me know it.
Summarizing
This is a hack, and using it in the wrong way is dangerous, use it only for applying styles after the ng-repeat has finished rendering and you shouldn't have any issues.
If you need to call different functions for different ng-repeats on the same controller you can try something like this:
The directive:
var module = angular.module('testApp', [])
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit(attr.broadcasteventname ? attr.broadcasteventname : 'ngRepeatFinished');
});
}
}
}
});
In your controller, catch events with $on:
$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) {
// Do something
});
$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) {
// Do something
});
In your template with multiple ng-repeat
<div ng-repeat="item in collection1" on-finish-render broadcasteventname="ngRepeatBroadcast1">
<div>{{item.name}}}<div>
</div>
<div ng-repeat="item in collection2" on-finish-render broadcasteventname="ngRepeatBroadcast2">
<div>{{item.name}}}<div>
</div>
The other solutions will work fine on initial page load, but calling $timeout from the controller is the only way to ensure that your function is called when the model changes. Here is a working fiddle that uses $timeout. For your example it would be:
.controller('myC', function ($scope, $timeout) {
$scope.$watch("ta", function (newValue, oldValue) {
$timeout(function () {
test();
});
});
ngRepeat will only evaluate a directive when the row content is new, so if you remove items from your list, onFinishRender will not fire. For example, try entering filter values in these fiddles emit.
If you’re not averse to using double-dollar scope props and you’re writing a directive whose only content is a repeat, there is a pretty simple solution (assuming you only care about the initial render). In the link function:
const dereg = scope.$watch('$$childTail.$last', last => {
if (last) {
dereg();
// do yr stuff -- you may still need a $timeout here
}
});
This is useful for cases where you have a directive that needs to do DOM manip based on the widths or heights of the members of a rendered list (which I think is the most likely reason one would ask this question), but it’s not as generic as the other solutions that have been proposed.
I'm very surprised not to see the most simple solution among the answers to this question.
What you want to do is add an ngInit directive on your repeated element (the element with the ngRepeat directive) checking for $last (a special variable set in scope by ngRepeat which indicates that the repeated element is the last in the list). If $last is true, we're rendering the last element and we can call the function we want.
ng-init="$last && test()"
The complete code for your HTML markup would be:
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" ng-init="$last && test()">{{t}}</p>
</div>
You don't need any extra JS code in your app besides the scope function you want to call (in this case, test) since ngInit is provided by Angular.js. Just make sure to have your test function in the scope so that it can be accessed from the template:
$scope.test = function test() {
console.log("test executed");
}
A solution for this problem with a filtered ngRepeat could have been with Mutation events, but they are deprecated (without immediate replacement).
Then I thought of another easy one:
app.directive('filtered',function($timeout) {
return {
restrict: 'A',link: function (scope,element,attr) {
var elm = element[0]
,nodePrototype = Node.prototype
,timeout
,slice = Array.prototype.slice
;
elm.insertBefore = alt.bind(null,nodePrototype.insertBefore);
elm.removeChild = alt.bind(null,nodePrototype.removeChild);
function alt(fn){
fn.apply(elm,slice.call(arguments,1));
timeout&&$timeout.cancel(timeout);
timeout = $timeout(altDone);
}
function altDone(){
timeout = null;
console.log('Filtered! ...fire an event or something');
}
}
};
});
This hooks into the Node.prototype methods of the parent element with a one-tick $timeout to watch for successive modifications.
It works mostly correct but I did get some cases where the altDone would be called twice.
Again... add this directive to the parent of the ngRepeat.
Very easy, this is how I did it.
.directive('blockOnRender', function ($blockUI) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (scope.$first) {
$blockUI.blockElement($(element).parent());
}
if (scope.$last) {
$blockUI.unblockElement($(element).parent());
}
}
};
})
Please have a look at the fiddle, http://jsfiddle.net/yNXS2/. Since the directive you created didn't created a new scope i continued in the way.
$scope.test = function(){... made that happen.

JQuery - Iterating JSON Response

The Story So far....
Trying to learn JS and JQuery and i thought i would start with the basics and try alittle AJAX "search as you type" magic. Firstly i just wanted to get the AJAX part right and iterating through the return JSON object and appending it to a unordered list. Im doing no validation on the inputted value vs. the returned JSON results at this time, i just want a controlled way of when to do the AJAX getJSON call. Later i will do the validation once i get this right.
Anyways im having trouble displaying the Account Numbers in in the ul. At the moment the only thing that is being displayed is AccountNumber in the li and not my ACCOUNT NUMBERS
My JS Code is here:
http://jsfiddle.net/garfbradaz/HBYvq/54/
but for ease its here as well:
$(document).ready(function() {
$("#livesearchinput").keydown(function(key) {
$.ajaxSetup({
cache: false
});
$.getJSON(" /gh/get/response.json//garfbradaz/MvcLiveSearch/tree/master/JSFiddleAjaxReponses/", function(JSONData) {
$('<ul>').attr({
id: "live-list"
}).appendTo('div#livesearchesults');
$.each(JSONData, function(i, item) {
var li = $('<li>').append(i).appendTo('ul#live-list');
//debugger;
});
});
});
});​
My JSON file is hosted on github, but again for ease, here it is:
https://github.com/garfbradaz/MvcLiveSearch/blob/master/JSFiddleAjaxReponses/demo.response.json
{
"AccountNumber": [
1000014,
1015454,
1000013,
1000012,
12
]
}
Also here is my Fiddler results proving my JSON object is being returned.
EDIT:
There were so queries about what i was trying to achieve, so here it goes:
Learn JQuery
To build a "Search as you Type" input box. Firstly i wanted to get the AJAX part right first, then i was going to build an MVC3 (ASP.NET) Application that utilises this functionality, plus tidy up the JQuery code which includes validation for the input vs. the returned JSON.
Cheesos answer below worked for me and the JSFiddle can be found here:
http://jsfiddle.net/garfbradaz/JYdTU/
First, I think keydown is probably the wrong time to do the json call, or at least... it's wrong to do a json call with every keydown. That's too many calls. If I type "hello" in the input box, within about .8 seconds, then there are 5 json requests and responses.
But you could make it so that it retrieves the json only the first time through, using a flag.
Something like this:
$(document).ready(function() {
var $input = $("#livesearchinput"), filled = false;
$.ajaxSetup({ cache: false });
$input.keydown(function(key) {
if (!filled) {
filled = true;
$.getJSON("json101.js", function(JSONData) {
var $ul =
$('<ul>')
.attr({id: "live-list"})
.appendTo('div#livesearchesults');
$.each(JSONData, function(i, item) {
$.each(item, function(j, val) {
$('<li>').append(val).appendTo($ul);
});
});
});
}
});
});
The key thing there is I've used an inner $.each().
The outer $.each() is probably unnecessary. The JSON you receive has exactly one element in the object - "AccountNumber", which is an array. So you don't need to iterate over all the items in the object.
That might look like this:
$.each(JSONData.AccountNumber, function(i, item) {
$('<li>').append(item).appendTo($ul);
});
What you probably want is this:
$.each(JSONData.AccountNumber, function(i, item) {
var li = $('<li>').append(item).appendTo('ul#live-list');
});
Your code:
$.each(JSONData, function(i, item) {
var li = $('<li>').append(i).appendTo('ul#live-list');
});
Says "iterate over the keys and values of the outer JSON structure, and print the keys". The first key is "AccountNumber", so you end up printing that.
What you want to do is iterate over the array stored at JSONData.AccountNumber, and print the values:
$.each(JSONData.AccountNumber, function() {
var li = $('<li>').append(this).appendTo('ul#live-list');
});

Resources