Protractor - get specific element in a list - protractor

I have a list of pages in a table and need to click on the next page.
<td><span>1</span></td>
<td>2</td>
<td>3</td>
So, if the browser is currently on page 1, I need to click on page 2.
I am currently getting the elements by "td", going through a for loop (or mapping function) to try and find the current page, and then trying to find the next page. I keep getting memory issues or timeouts the way I'm currently trying to solve it.
Mapping function (memory issues) gist: https://gist.github.com/CaseyHaralson/9965492d894565bfe9d7
For loop (timeout) gist: https://gist.github.com/CaseyHaralson/7153be9f437f4e3a7f5c
The for loop also looks like it has the potential issue of not necessarily resolving the pages in the correct order.
Note: I can't change the html.

Assuming your current page is has the span, something like this should work...
var clickNextPage = function() {
$('td span').getText().then(function(pageNum) {
element(by.cssContainingText('td a', pageNum + 1)).click();
});
};

This function should click the href based on the page number you pass in.
var visitPage = function (pageNumber) {
$('a[href="' + pageNumber + '"']).click();
};
it('should visit page three', function () {
expect(browser.getCurrentUrl()).toContain('1');
visitPage('3');
expect(browser.getCurrentUrl()).toContain('3');
});

Related

Protractor page object definition not working as expected

I apologize for the slightly vague title, I'm not sure how exactly to word this.
I have my Page Object which, with one exception, works perfectly. Here's the excerpt:
module.exports = function(){
this.facilityList = element(by.name('facility')).all(by.tagName('option'));
this.randomFacility = element(by.name('facility')).all(by.tagName('option')).count().then(function(numberOfItems) {
var rnum = parseInt(Math.random() * numberOfItems);
return rnum;
}).then(function(randomNumber) {
element(by.name('facility')).all(by.tagName('option')).get(randomNumber)
});
}
I can access and use facilityList just fine. But then I realized that I'm almost always doing the same thing to facilityList so why don't I just create another line to make it choose a random one. So I create randomFacility using the code from the main conf.js.
It didn't work. The error I see displayed is:
Failed: Error while waiting for Protractor to sync with the page: "both angularJS testability and angular testability are undefined. This could be either because this is a non-angular page or because your test involves client-side navigation, which can interfere with Protractor's bootstrapping. See http://git.io/v4gXM for details"
I'm confused. Is this saying I can't do all that processing in the page object to get the random one or do I simply have to manipulate facilityList in the conf.js and be done with it?
You nee to know the mechanism about how protractor to find element. Protractor only to start find element from page when protractor's action API be called, like getText(), click(), count() etc.
So when you define variable to represent certain element on page, when Nodejs execute this line, protractor won't to start find element from page:
// page object login.page.js
module.exports = function LoginPage(){
this.sumbitButton = element(by.css('#submit'));
this.countName = element.all(by.css('.username')).count();
}
// use page object in conf.js
var LoginPage = require('./login.page.js');
var loginPage = new Loginpage();
When Nodejs execute line var loginPage = new Loginpage();, all lines in function LoginPage will be executed.
When execute the first line, protractor not to find element from current open page,
When execute the second line, protractor will find element from current open page, But at this time point, protractor is possible to launching browser with a blank page, the target page have not been opened or navigated to.
To fix your problem, you need to define randomFacility as class's Method, rather than Property:
module.exports = function() {
this.facilityList = element(by.name('facility')).all(by.tagName('option'));
this.randomFacility = function() {
return element(by.name('facility'))
.all(by.tagName('option')).count()
.then(function(numberOfItems) {
console.log('count: '+numberOfItems);
var rnum = parseInt(Math.random() * numberOfItems);
console.log('random index: '+rnum);
return rnum;
})
.then(function(randomNumber) {
console.log('argument randomNumber: '+randomNumber);
return element(by.name('facility'))
.all(by.tagName('option')).get(randomNumber)
});
}
};
// how to use
pageObject.randomFacility().then(function(ele){
return ele.click();
});

Protractor element handling

I have a question regarding how protractor handles the locating of elements.
I am using page-objects just like I did in Webdriver.
The big difference with Webdriver is that locating the element only happens when a function is called on that element.
When using page-objects, it is advised to instantiate your objects before your tests. But then I was wondering, if you instantiate your object and the page changes, what happens to the state of the elements?
I shall demonstrate with an example
it('Change service', function() {
servicePage.clickChangeService();
serviceForm.selectService(1);
serviceForm.save();
expect(servicePage.getService()).toMatch('\bNo service\b');
});
When debugging servicePage.getService() returns undefined.
Is this because serviceForm is another page and the state of servicePage has been changed?
This is my pageobject:
var servicePage = function() {
this.changeServiceLink = element(by.id('serviceLink'));
this.service = element(by.id('service'));
this.clickChangeService = function() {
this.changeServiceLink.click();
};
this.getService = function() {
return this.service.getAttribute('value');
};
};
module.exports = servicePage;
Thank you in advance.
Regards
Essentially, element() is an 'elementFinder' which doesn't do any work unless you call some action like getAttribute().
So you can think of element(by.id('service')) as a placeholder.
When you want to actually find the element and do some action, then you combine it like element(by.id('service')).getAttribute('value'), but this in itself isn't the value that you are looking for, it's a promise to get the value. You can read all about how to deal with promises elsewhere.
The other thing that protractor does specifically is to patch in a waitForAngular() when it applies an action so that it will wait for any outstanding http calls and timeouts before actually going out to find the element and apply the action. So when you call .getAttribute() it really looks like
return browser.waitForAngular().then(function() {
return element(by.id('service')).getAttribute('value');
});
So, in your example, if your angular pages aren't set up correctly or depending on the controls you are using, you might be trying to get the value before the page has settled with the new value in the element.
To debug your example you should be doing something like
it('Change service', function() {
servicePage.getService().then(function(originalService) {
console.log('originalService: ' + originalService);
});
servicePage.clickChangeService();
serviceForm.selectService(1);
serviceForm.save();
servicePage.getService().then(function(newService) {
console.log('newService: ' + newService);
});
expect(servicePage.getService()).toMatch('\bNo service\b');
});
The other thing that I'm seeing is that your pageObject appears to be a constructor when you could just use an object instead:
// name this file servicePage.js, and use as 'var servicePage = require('./servicePage.js');'
module.exports = {
changeServiceLink: element(by.id('serviceLink')),
service: element(by.id('service')),
clickChangeService: function() {
this.changeServiceLink.click();
},
getService: function() {
return this.service.getAttribute('value');
}
};
Otherwise you would have to do something like module.exports = new servicePage(); or instantiate it in your test file.
When you navigate another page, the web elements will be clear, that you selected. So you have to select again. You can select all elements that is in a page of HTML. You can click that you see. So the protactor + Selenium can decide what is displayed.
You have a mistake in your code, try this:
expect(servicePage.getService()).toMatch('\bNo service\b');

Storing appended elements to localStorage

I'm a teacher and creating a page to organize my lesson plans. There should be the ability to add new lessons (li) and new weeks (ul). The lessons are sortable between each of the weeks. Each newly added item will then be saved to localStorage.
So far, I'm able to create the lessons and new weeks. The sortable function works. The save function works... except that it will not save any of the new weeks (ul). When I refresh, the new lessons (li) are still on the page, but the new weeks (ul) are gone.
$("#saveAll").click(function(e) {
e.preventDefault();
var listContents = [];
$("ul").each(function(){
listContents.push(this.innerHTML);
})
localStorage.setItem('todoList', JSON.stringify(listContents));
});
$("#clearAll").click(function(e) {
e.preventDefault();
localStorage.clear();
location.reload();
});
loadToDo();
function loadToDo() {
if (localStorage.getItem('todoList')){
var listContents = JSON.parse(localStorage.getItem('todoList'));
$("ul").each(function(i){
this.innerHTML = listContents [i];
})
}
}
I created a fiddle here.
You can click the "Add New Week" button and then click the "Create Lesson" button and drag the new lesson into one of the weeks. After clicking "Save All", only the first week is saved.
I can't seem to figure out what's missing.
It's saving correctly, but since the page only has one <ul> element initially, that is the only one that gets populated in loadToDo(). (listContents has more than one element, but $("ul").each(...) only iterates over one element.)
There is a quick band-aid you can use to resolve this. Refactor your #new-week-but click handler into a named function:
function addNewWeek() {
var x = '<ul class="sortable weeklist"></ul>';
$(x).appendTo('.term').sortable({connectWith: '.sortable'});
}
$('#new-week-but').click(addNewWeek);
Then add this block after you fetch the array from storage but before you enumerate the <ul> elements:
var i;
for (i = 2; i < listContents.length; ++i) {
addNewWeek();
}
This will add the required number of <ul> elements before attempting to populate them.
I chose to initialize i to two because this creates two fewer than the number of elements in listContents. We need to subtract one because there is a <ul> in .term when the page loads, and another because the <ul id="new-lesson-list"> contents also get saved in listContents. (Consider filtering that element out in your #saveAll click handler.)
(Note that this requires merging all of your $(document).ready() functions into one big function so that addNewWeek() is visible to the rest of your code.)
Suggestions to improve code maintainability:
Give each editable <ul> a CSS class so that they can be distinguished from other random <ul> elements on the page. Filter for this class when saving data so that the "template" <ul> doesn't get saved, too.
Remove the one default editable <ul> from the page. Instead, in your loadToDo() function, add an else block to the if block and call addNewWeek() from the else block. Also, call it if listContents.length == 0. This will prevent duplicating the element in the HTML source (duplication is bad!) and having to account for it in your load logic.
If you implement both of these then you can initialize i to 0 instead of 2 in my sample code, which is a lot less weird-looking (and less likely to trip up future maintainers).

Using jquery-ias with async-loaded content

I've somewhat successfully integrated the jQuery Infinite Ajax Scroll plugin into my development site - it is used twice, first on the thread list on the left, second when you load up an individual topic - but I'm having trouble with the second ias instance here.
Basically the content of a topic is loaded via $.get and then rendered into the page, and then I trigger setupThreadDetailDownwardScroll() in JS which creates an instance of ias:
var iasDetail = jQuery.ias({
container: "#reply-holder",
item: ".post",
pagination: ".threaddetail-pagination",
next: ".load-next-inner-link a",
delay: 2000,
});
if (iasDetail.extension) {
iasDetail.extension(new IASPagingExtension());
iasDetail.extension(new IASTriggerExtension({
text: 'More Replies',
html: '<div class="scroll-pager"><span>{text}</span></div>',
offset: 10,
}));
iasDetail.extension(new IASNoneLeftExtension({html: '<div class="scroll-message"><span>No more replies</span></div>'}));
iasDetail.extension(new IASHistoryExtension({
prev: '.load-previous-inner-link a',
}));
}
iasDetail.on('load', function() {
$('#reply-holder').append(scrollLoading);
})
iasDetail.on('rendered', function() {
$('.scroll-loading').remove();
iasDetail.unbind();
})
But the problem is that this only works with whatever the first topic you load is - you'll get working pagination in the first thread, but then it'll fallback to anchor links when you open the next thread.
So I figured that I needed to rebind ias once this new content is inserted, and this is why I have added the unbind() call in rendered, and then I re-call setupThreadDetailDownwardScroll() whenever another thread is loaded. This doesn't work either though.
Is there a correct procedure here?
You are using jQuery.ias(...) which binds to the scroll event of $(window). In your case you probably want to bind to an overflow div. Therefor you should specify the scrollContainer like this:
$('#scrollContainer').ias(...)
Edit:
Based on you comment I took another look at it and might have found an answer. When you call jQuery.ias({...}); IAS gets setup and waits for $(document).ready to initialize. You say you want to initialize IAS in your setupThreadDetailDownwardScroll function. You can try to initialize IAS yourself with the following code
iasDetail.initialize();

Meteor: Elements from CollectionA re-rendering when I insert to CollectionB

I'm attempting to fade-in new elements in a reactive {{#each}} of the comments posted by users.
I have a code sample at https://gist.github.com/3119147 of a very simple comments section (textarea and new comment insert code not included, but it's very boilerplate.). Included is a snippet of CSS where I give .comment.fresh { opacity: 0; }, and then in my script, I have:
Template.individual_comment.postedago_str = function() {
var id = this._id;
Meteor.defer(function() {
$('#commentid_'+id+'.fresh').animate({'opacity':'1'}, function() {
$(this).removeClass('fresh');
});
});
return new Date(this.time).toString();
};
Which seems like a terrible place to execute an animation. My thinking is that each time a new comment is rendered, it will need to call all my Template.individual_comment.* functions, so that's why my animation defers from one of those. However, Meteor is calling Template.individual_comment.postedago_str() each time a different collection (Likes) is inserted to. This means I click the Like button, and my whole list of comments flashes white and fades back in (very annoying!).
I read the Meteor documentation and tried to figure out how to better slice up my templates so only chunks will update, and I added id="" attributes everywhere that seemed reasonable.. still this bug. Anyone know what's going on?
TIA!
As a workaround, you could wrap an {{if}} block around the fresh class on individual comments, that would check the comment's creation time and only add the fresh class in the first place if the comment is actually recent. Something like:
<div class="comment{{#if isActuallyFresh}} fresh{{/if}}" id="commentid_{{_id}}">
And then define the isActuallyFresh function:
Template.individual_comment.isActuallyFresh = function() {
if ((new Date().getTime() - this.time) < 300000) // less than 5 minutes old
return true;
else
return false;