CasperJS Waiting for live DOM to populate - angularjs

I'm evaluating using Casper.js to do functional/acceptance testing for my app. The biggest problem I've seen so far is that my app is an SPA that uses handlebars templates (which are compiled to JS) The pages of my app are nothing more than a shell with an empty div where the markup will be injected via JS.
I've messed around with Casper a little and tried using its waitFor functions. All I can seem to get from it are my main empty page before any of the markup is injected. I've tried waitForSelector but it just times out after 5 seconds. Should I try increasing the timeout? The page typically loads in a browser very quickly, so it seems like there may be another issue.
I'm using Yadda along with Casper for step definitions:
module.exports.init = function() {
var dictionary = new Dictionary()
.define('LOCALE', /(fr|es|ie)/)
.define('NUM', /(\d+)/);
var tiles;
function getTiles() {
return document.querySelectorAll('.m-product-tile');
}
function getFirstTile(collection) {
return Array.prototype.slice.call(collection)[0];
}
var library = English.library(dictionary)
.given('product tiles', function() {
casper.open('http://www.example.com/#/search?keywords=ipods&resultIndex=1&resultsPerPage=24');
casper.then(function() {
// casper.capture('test.png');
casper.echo(casper.getHTML());
casper.waitForSelector('.m-product-tile', function() {
tiles = getTiles();
});
});
})
.when('I tap a tile', function() {
casper.then(function() {
casper.echo(tiles); //nodelist
var tile = Array.prototype.slice.call(tiles)[0];
casper.echo(tile); //undefined!
var pid = tile.getAttribute('data-pid');
})
})
.then('I am taken to a product page', function() {
});
return library;
};
Any Angular, Backbone, Ember folks running into issues like this?

Related

Use protractor on an non angular page

in my recent test I need to first login and take some actions on an non angular page (https://www.qa.dealertrack.com/default1.aspx)
then switch to an angular page and finish the test.
In conf I have
global.driver = browser.driver;
My page object looks like:
var LogInPage = function() {
this.loginUrl = 'https://www.qa.dealertrack.com/default1.aspx';
this.id = browser.driver.findElement(by.name('username'));
this.password = browser.driver.findElement(by.name('password'));
this.loginButton = browser.driver.findElement(by.name('login'));
this.logIn = function(id, password) {
// maximize window
driver.manage().window().maximize();
// log in
driver.get('https://www.qa.dealertrack.com/default1.aspx');
this.id.sendKeys(id);
this.password.sendKeys(password);
this.loginButton.click();
}
};
My test looks like:
describe('Sample Test - Log In', function() {
var loginPage = require('../pages/LogInPage.js');
/**
* Disable waiting for AngularJS for none Angular page
*/
beforeEach(function() {
isAngularSite(false);
});
it('logging in', function() {
loginPage.logIn('xxx', 'xxx');
})
})
However, even before getting to the site, protractor throws error NoSuchElementError: no such element: Unable to locate element:{'method':'name','selector':'username'}
But when I commented out all the element variables and related lines, only left
driver.get(this/loginUrl);
It worked. Why would browser.driver.get works but browser.driver.findElement does not?
This is my first question on Stackoverflow. Thank everyone!!
Before login you need to set
browser.driver.ignoreSynchronization=true;
So it shold be like
browser.driver.ignoreSynchronization=true;
login();
browser.driver.ignoreSynchronization=false;
I tried to removed all the elements I declared outside my functions in page object. Instead of that, I just hard code the elements in the function. And it worked.

Use Service in angular

I use in service in my angular app as follows:
app.service('sharedProperties', function () {
var property;
return {
getProperty: function () {
return property;
},
setProperty: function(value) {
property = value;
}
};
});
$scope.Somefunc= function(Mname)
{
$http.post("SomeServlet",{
"name": Mname,
}).then(function(response) {
sharedProperties.setProperty(response.data);
window.location = "/blabla/page2.html";
});
};
And in page2 (another conroller) i get the value:
app.controller('controller2', function($scope,$http,sharedProperties) {
$scope.UserProperties = sharedProperties.getProperty();
});
and its doesnt work, always i get undefined.
In order for it to work, you must set the angular routing function which will take care of moving to another page without refreshing your current page (also called SPA - Single Page Application).
Take a look here, at the example in the end of the page there are the different pages and files in the mini system.
I remind you that angular is meant to work with single page applications, so if you refresh the page and change it into another one - You sort of lose the point of using it.

Backbonejs - Back button doesn't work if page transition on same page

Short description of my program and finally the problem:
I have got two pages. The first page list products in rows with a short description. If you click on one you will land on a detail page.
The detail page lists the product details and underneath a couple of related products. If you click on one of the releated products the same page is rendered again with the new information fetched from a REST interface.
If I want to use the browser-back-button or the own back-button to get to the previous product-detail-page a blank page appears. This only happens on my iPad. Using Chrome on a desktop browser works fine. I debugged the application and I figured out, that the backbonejs route is never called. I have no idea why.
Here is my code of the details page:
define([
"jquery",
"lib/backbone",
"lib/text!/de/productDetails.html"
],
function(
$,
Backbone,
ContentTemplate
){
var PageView = Backbone.View.extend({
// product details template
template: _.template(ContentTemplate),
// back-button clicked
events:{
'click a#ac-back-button':'backInHistory',
},
// init
initialize: function(options){
this.options=options;
// bind functions
_.bindAll(this,
'render',
'renderRelatedSeriePlainproduct',
'backInHistory'
);
// listen for collection
this.listenTo(this.options.relatedCollectionPlainproduct, 'reset',this.renderRelatedSeriePlainproduct);
},
// back button
backInHistory: function(e){
e.preventDefault();
window.history.back();
},
// render template
render: function(){
// render template
this.$el.html(this.template(this.model.models[0].attributes));
return this;
},
// render related products
renderRelatedSeriePlainproduct: function (){
var models = this.options.relatedCollectionPlainproduct.models;
if(models.length==0){
$('.ac-plainproduct').hide();
} else{
var elem = $('#ac-related-listing-plainproduct');
var ct="";
ct+='<ul id="ac-list-related-plainproduct">';
$.each(models, function(key, value){
ct+='<li>';
ct+='<a href="index.html?article_id='+value.get('article_id')+'&type='+value.get('type')+'&serie='+value.get('series')+'#product-detail">Link';
ct+='</a>';
ct+='</li>';
});
ct+='</ul>';
elem.append(ct);
}
}
});
// Returns the View class
return PageView;
});
I follow one of the links from renderRelatedSeriePlainproduct.If I click on the back button on the new page the backInHistory function is called, but the window.history.back(); does not call the backbone router.
Maybe the problem is the #hash in the URL, that is not changed during page transition. But this would not explain, why it works perfectly with my Chrome on my desktop machine. For me it seemed to be a problem of asynchronous calls but even there I could not find a problem.
Maybe it helps to list my router code as well. First of all I was thinking it is an zombie issue in backbone, but I remove all events and views while making the transition.
// function called by the route
// details page
productdetail: function() {
$.mobile.loading("show");
_self = this;
// lazy loading
require([
'collection/ProductDetailCollection',
'collection/RelatedCollection',
'view/ProductDetailView'
],
function(ProductDetailCollection, RelatedCollection, ProductDetailView){
// get URL parameters
var articleID = _self.URLParameter('article_id');
var type = _self.URLParameter('type');
var serie = _self.URLParameter('serie');
// product - details
var productDetail = new ProductDetailCollection.ProductDetail({id: articleID});
// related products
_self.relatedCollectionPlainproduct = new RelatedCollection({serie:serie, type:"Electronics", article_id:articleID});
// assign binded context
productDetail.fetch({
// data fetched
success: function (data) {
// page transition
_self.changePage(new ProductDetailView({
model:data,
relatedCollectionPlainproduct:_self.relatedCollectionPlainproduct
}));
// fetch data
_self.relatedCollectionPlainproduct.fetch({reset:true});
}
});
});
},
// page transition
changePage:function (page) {
// remove previous page from DOM
this.page && this.page.remove() && this.page.unbind();
// assign
this.page = page;
// assign page tag to DOM
$(page.el).attr('data-role', 'page');
// render template
page.render();
// append template to dom
$('body').append($(page.el));
// set transition
var transition = "fade";
// we want to slide the first page different
if (this.firstPage) {
transition = "fade";
this.firstPage = false;
}
// make transition by jquery mobile
$.mobile.changePage($(page.el), {changeHash:true, transition: transition});
// page was rendered - trigger event
page.trigger('render');
$.mobile.loading("hide");
},
I tried to use allowSamePageTransition but with no success. Maybe someone could give me a hint. Thanks!
Looks like jQuery Mobile and Backbone's routers are conflicting. Take a look here:
http://coenraets.org/blog/2012/03/using-backbone-js-with-jquery-mobile/
Thats not the reason. I disabled the routing of jquery mobile.
// Prevents all anchor click handling
$.mobile.linkBindingEnabled = false;
// Disabling this will prevent jQuery Mobile from handling hash changes
$.mobile.hashListeningEnabled = false;

backbone events not triggering in Safari

For a learning exercise I converted a sinatra/backbone app to the Rails environment. I got it working on Chrome and Firefox but it doesn't work on Safari. It turns out that the original app http://backbone-hangman.heroku.com doesn't work on Safari either. When you click "new game" it doesn't seem to fire an event. The Safari console doesn't show any errors (although I'm not that experienced with Safari's developer tools as I never use them).
Since there is a live version of the app available here http://backbone-hangman.heroku.com I won't post a lot of code, but this is the view code that sets an event on click #new_game, triggering the startNewGame function. Nothing happens in Safari. Source code for the original is here https://github.com/trivektor/Backbone-Hangman
I googled a bit and found some mention of Safari treating events differently but couldn't find a solution. Can any recommend anything?
$(function() {
window.OptionsView = Backbone.View.extend({
el: $("#options"),
initialize: function() {
this.model.bind("gameStartedEvent", this.removeGetAnswerButton, this);
this.model.bind("guessCheckedEvent", this.showGetAnswerButton, this);
},
events: {
'click #new_game': 'startNewGame',
'click #show_answer': 'showAnswer'
},
startNewGame: function() {
this.model.new();
},
removeGetAnswerButton: function() {
$("#show_answer").remove();
},
showGetAnswerButton: function(response) {
console.log("showGetAnswerButton");
console.log(response);
var threshold = this.model.get("threshold");
console.log(threshold);
if (response.incorrect_guesses == this.model.get("threshold")) {
$(this.el).append('<input type="button" id="show_answer" class="action_button" value="Show answer" />');
}
},
showAnswer: function() {
this.model.get_answer();
}
})
})
Update
Based on one of the comments below the OP, I'm posting more code. This is hangman.js where the objects are instantiated
var game = new Game
var options_view = new OptionsView({model: game});
var characters_view = new CharactersView({model: game});
var hint_view = new HintView({model: game});
var word_view = new WordView({model: game});
var hangman_view = new HangmanView({model: game});
var answer_view = new AnswerView({model: game});
var stage_view = new StageView({model: game});
The views and models are attached to the window like this
window.AnswerView = Backbone.View.extend({ ...
Update
Aside from Backbone, jQuery and Underscore which are loaded sitewide, the following files are loaded for this specific app in the Rails system.
This is jQuery + Safari issue (document.ready)
You can just move your scripts inside the body tag and remove $(function(){ /**/ }) wrapper in every file.
Also I added requirejs support and made pull request
EDIT:
First of all sorry for my English :)
File views/index.haml:
We should embed js at the bottom of the page (to avoid Safari error)
= javascript_include_tag "javascript/require.js", :"data-main" => "javascript/config"
Here javascript/config is the path to requirejs config.
File public/javascript/config.js:
"deps" : ["hangman"]
This means that application will start with hangman.js
File public/javascript/hangman.js:
We don't need $(function() { wrapper because our script initialized from the body and document is already 'ready'
define([
'models/game',
'views/answerView',
/* ... */
],
function(Game, OptionsView, /* ... */) {
// ...
}
Here we load our modules (first array element will be available in the first function argument and so on)
Other files
We just replace $(function() { with define(['backbone'], function(Backbone) {
In the first line we load backbone module. When it will be fetched it will be available inside anonymous function (first parameter - Backbone)
Next we should return the view to avoid undefined module value (public/javascript/hangman.js file should initialize a lot views. It can't initialize undefined it should initialize Backbone.View that we should return)
To learn more you should read requirejs documentation.
I recomend you to start with this article
Try this instead.
var OptionsView = Backbone.View.extend({
el: $("#options"),
initialize: function() {
this.model.bind("gameStartedEvent", this.removeGetAnswerButton, this);
this.model.bind("guessCheckedEvent", this.showGetAnswerButton, this);
},
events: {
'click #new_game': 'startNewGame',
'click #show_answer': 'showAnswer'
},
startNewGame: function() {
this.model.new();
},
removeGetAnswerButton: function() {
$("#show_answer").remove();
},
showGetAnswerButton: function(response) {
console.log("showGetAnswerButton");
console.log(response);
var threshold = this.model.get("threshold");
console.log(threshold);
if (response.incorrect_guesses == this.model.get("threshold")) {
$(this.el).append('<input type="button" id="show_answer" class="action_button" value="Show answer" />');
}
},
showAnswer: function() {
this.model.get_answer();
}
});
Your code dosen't need to be in a document ready (it's not directly manipulating DOM, it's just declaring an object);
Make sure game.js goes after all your declarations though.
It looks like a problem Safari has in adding variables to the Global Object. Using var in the global context makes sure window.OptionsView exists. You might want to consider using require.js in the future to manage all of these global object problems.

using twiiter tooltip with backbone.js

full sample here
I have a very simple backbone js structure.
var Step1View = Backbone.View.extend({
el:'.page',
render:function () {
var template = _.template($('#step1-template').html());
this.$el.html(template);
}
});
var step1View = new Step1View();
var Router = Backbone.Router.extend({
routes:{
"":"home"
}
});
var router = new Router;
router.on('route:home', function () {
step1View.render();
})
Backbone.history.start();
This works well however i am unable to get this simple jquery function called.
$(document).ready(function() {
$('.tip').tooltip();
});
Update
School boy error here. Jquery onload functions need to be placed in the route. I'm very new to backbone so i'm not sure if this is best practice. But the following works.
render:function () {
var that = this;
var savings = new Savings();
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
// put your jquery good ness here
$('.tip').tooltip();
$(".step3-form").validate();
}
})
}
Looks like you found your answer! Just wanted to also share that you could scope down your jQuery a bit by doing this instead.
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
that.$el.find('.tip').tooltip();
that.$el.find(".step3-form").validate();
}
What you have in your example works but it's also scanning the whole document every time for HTML with the class tip where you could use the element you just created to scan downward only for the tip you just created inside it. Slight optimization.
Hope this is helpful!
Looks like you found your answer! Just wanted to also share that you could scope down your jQuery a bit by doing this instead.
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
that.$el.find('.tip').tooltip();
that.$el.find(".step3-form").validate();
}
What you have in your example works but it's also scanning the whole document every time for HTML with the class tip where you could use the element you just created to scan downward only for the tip you just created inside it. Slight optimization.
Hope this is helpful!

Resources