We have written several test cases with casperjs now. In comparison to other testing frameworks it works like charm. But there is one crucial part of our app, where we fail to write a suitable test case.
In our app we have integrated a type of autocomplete plugin which is called Geocomplete (http://ubilabs.github.io/geocomplete/) which makes it possible to fetch geodata from the Google Maps Api.
There is the following workflow. On the start page of our site there is a form with one single input field, which is used for the autocomplete functionality. There the user can enter the name of a specific city and Google returns the data. In the background a backbone model is populated with that data.
Here is the code of the testcase:
casper.test.begin('Test User Login Form', 4, function suite(test) {
casper.options.verbose = true;
casper.options.logLevel = 'debug';
var url = 'http://localhost:8889/';
var session;
casper.start(url);
casper.test.comment('Start Testing');
casper.waitFor(function check() {
return this.evaluate(function() {
return document.getElementById('page-wrap');
});
}, function then() {
casper.waitForSelector('#landingForm', function() {
this.echo('waiting');
});
});
// input is populated with a some letters
casper.then(function() {
casper.sendKeys('#landingForm input[name="location.name"]', 'Klag', {
keepFocus: true
});
});
// .pac-item container whill show the autocomplete suggestions
casper.then(function() {
casper.waitUntilVisible('.pac-item', function() {
// we have tried several methods here like mouse_over + click etc.
this.sendKeys('#landingForm input[name="location.name"]', casper.page.event.key.Down, {
keepFocus: true
});
this.sendKeys('#landingForm input[name="location.name"]', casper.page.event.key.Enter, {
keepFocus: true
});
// form is submitted
this.click('#landingForm > div > div > div > span > button');
});
});
casper.then(function() {
// wait until next page is visible
casper.waitUntilVisible('div.activity-pic', function() {
// get backbone session model
session = casper.evaluate(function() {
return require('model/session');
});
// test if model was populated correctly with the data from google
test.assertEquals(session.filterModel.attributes.location.name, 'Klagenfurt', 'Name equals expected values.');
});
});
casper.run(function() {
casper.test.comment('Ending Testing');
test.done();
});
});
The test
test.assertEquals(session.filterModel.attributes.location.name, 'Klagenfurt', 'Name equals expected values.');
always fails and tells me that the name-attribute is undefined. The input field is filled in correclty with the name of the city. We have used the evaluate-method in other testcases to check the values and attributes of our models too, there it worked.
Does anybody has the same problem?
There are two possible approaches to this. Based on this comment you can add an event listener through evaluate and waitFor its execution (here as a reusable casper function):
casper.waitForGeocodeResult = function(){
this.thenEvaluate(function(){
// TODO: initialize $input
$input.bind('geocode:result', function(event, result) {
window._myGeocodeResultArrived = true;
}
});
this.waitFor(function check(){
return this.evaluate(function(){
return "_myGeocodeResultArrived" in window && window._myGeocodeResultArrived;
});
});
this.thenEvaluate(function(){
window._myGeocodeResultArrived = false;
});
};
You may call it like this:
casper.waitForGeocodeResult();
casper.then(function() {
// get backbone session model
session = casper.evaluate(function() {
return require('model/session');
});
// test if model was populated correctly with the data from google
test.assertEquals(session.filterModel.attributes.location.name, 'Klagenfurt', 'Name equals expected values.');
});
If this doesn't work for you may directly check the session model repeatedly (again as a reusable casper function):
casper.getBackboneModel = function(name, keyFunc){
var oldRetry;
this.then(function(){
oldRetry = this.options.retryTimeout;
// set retry timeout a little higher in case the require is a time intensive function
this.options.retryTimeout = 500;
});
this.waitFor(function check(){
var model = casper.evaluate(function(modelName){
return require(modelName);
}, name);
return keyFunc(model);
}, null, function onTimeout(){
this.echo("warning: geocomplete was unsuccessful");
});
this.then(function(){
// reset timeout
this.options.retryTimeout = oldRetry;
});
};
Call it like this:
casper.getBackboneModel(function(session){
try {
var temp = session.filterModel.attributes.location.name;
return "name" in session.filterModel.attributes.location;
} catch(e){
return false;
}
});
casper.then(function() {
// get backbone session model
session = casper.evaluate(function() {
return require('model/session');
});
// test if model was populated correctly with the data from google
test.assertEquals(session.filterModel.attributes.location.name, 'Klagenfurt', 'Name equals expected values.');
});
Related
I'm trying to put recaptcha v3 on a form but inserting the token into a hidden input field doesn't work - on the first submission.
Here is the code that I doctored a little. I added an alert and stopped the sumbit to see what's happening. This code is in a separate bundle.js file.
var form = document.querySelector('#contact-form');
var inputs = form.querySelectorAll('input');
$(form).submit(function(e) {
e.preventDefault();
grecaptcha.ready(function() {
grecaptcha.execute('secret_key', {
action: 'my_action'
}).then(function(token) {
document.getElementById('tokenField').value = token;
alert('token is; ' + token); // inserted for troubleshooting
});
});
removeMessageBox();
validation();
var alerts = document.querySelectorAll('.alert-text');
if (alerts.length == 0) {
// document.forms['contact-form'].submit();
}
});
With this code as written, the alert shows a valid token but tokenField has no value. tokenField gets the token AFTER you press OK on the alert. What is the problem??? I have tried everything.
If you take out the alert and uncomment submit, tokenField is empty on submission.
Note that this script also calls validation() and removeMessageBox(), which removes validation error messages.
If validation() stops the submission for some reason, you can fix the problem, submit again and tokenField gets it value and everything works great - the second time.
As far as I can tell, the field is being set inside of a callback, while the other functions are running in the main function. You can either put all of the code into the callback function for the .then function, or use an async function.
Callback
$(form).submit(function(e) {
e.preventDefault();
grecaptcha.ready(function() {
grecaptcha.execute('secret_key', {
action: 'my_action'
}).then(function(token) {
document.getElementById('tokenField').value = token;
removeMessageBox();
validation();
var alerts = document.querySelectorAll('.alert-text');
if (alerts.length == 0) {
// document.forms['contact-form'].submit();
}
});
});
});
Async
$(form).submit(function(e) {
e.preventDefault();
grecaptcha.ready(async function() {
const token = await grecaptcha.execute('secret_key', {
action: 'my_action'
});
document.getElementById('tokenField').value = token;
removeMessageBox();
validation();
var alerts = document.querySelectorAll('.alert-text');
if (alerts.length == 0) {
// document.forms['contact-form'].submit();
}
});
});
I have a script that makes $.ajax request for a json api. So what I want to do is to build unit test so I can test the result from the ajax request. For example if I get json object back. I know result should include "items" and "result" which is an array. The things is I dont know how to initialize the $.ajax function which is inside a
$("#button").click(function() { });
Here's the skeleton of my javascript index.js file. The file is not complete. as it is longer. I just included the relevant parts. But it works. Here's the app live online http://pctechtips.org/apps/books/
$(document).ready(function() {
var item, tile, author, publisher, bookLink, bookImg;
var outputList = document.getElementById("list-output");
var bookUrl = "https://www.googleapis.com/books/v1/volumes?q=";
var searchData;
$("#search").click(function() {
outputList.innerHTML = ""; //empty html output
searchData = $("#search-box").val();
//handling empty search input field
if(searchData === "" || searchData === null) {
displayError();
}
else {
// console.log(searchData);
// $.get("https://www.googleapis.com/books/v1/volumes?q="+searchData, getBookData()});
$.ajax({
url: bookUrl + searchData,
dataType: "json",
success: function(response) {
console.log(response)
if (response.totalItems === 0) {
alert("no result!.. try again")
}
else {
$("#title").animate({'margin-top': '5px'}, 1000); //search box animation
$(".book-list").css("visibility", "visible");
displayResults(response);
}
},
error: function () {
alert("Something went wrong.. <br>"+"Try again!");
}
});
}
$("#search-box").val(""); //clearn search box
});
});
In your test you need first to prepare a HTML fixture which will contain all the required elements like #search. After preparing it, you can load your script via $.getScript() - it will attach click event listener to #search. Finally, you have to spy on $.ajax and trigger the click manually via $('#search').trigger('click')
I am new to Mocha and Webdriver.io, so please excuse me if I am being stupid...
Here is my code -
// required libraries
var webdriverio = require('webdriverio'),
should = require('should');
// a test script block or suite
describe('Login to ND', function() {
// set timeout to 10 seconds
this.timeout(10000);
var driver = {};
// hook to run before tests
before( function () {
// load the driver for browser
driver = webdriverio.remote({ desiredCapabilities: {browserName: 'firefox'} });
return driver.init();
});
// a test spec - "specification"
it('should be load correct page and title', function () {
// load page, then call function()
return driver
.url('https://ND/ilogin.php3')
// get title, then pass title to function()
.getTitle().then( function (title) {
// verify title
(title).should.be.equal("NetDespatch Login");
// uncomment for console debug
console.log('Current Page Title: ' + title);
return driver.setValue("#userid", "user");
return driver.setValue("#password", "pass");
return driver.click("input[alt='Log in']");
});
});
// a "hook" to run after all tests in this block
after(function() {
return driver.end();
});
});
I can execute this with Mocha, and the test passes, even though it doesn't seem to do all of the "steps" I have defined..
It opens the page, logs the website title, and enters 'user' in the userid, BUT..
It doesn't populate the password field, or select the login link, and there doesn't appear to be any errors displayed..
Login to ND
Current Page Title: ND Login
✓ should be load correct page and title (2665ms)
1 passing (13s)
But, as it hasn't executed all the steps, I don't expect it to pass, though, I also don't understand why it won't do the last few steps.
Any help would be welcome.
Thanks
Karl
As mentioned in the original post comments, you should only have one return in your test:
it('should be load correct page and title', function () {
// load page, then call function()
return driver
.url('https://ND/ilogin.php3')
// get title, then pass title to function()
.getTitle().then( function (title) {
// verify title
(title).should.be.equal("NetDespatch Login");
// uncomment for console debug
console.log('Current Page Title: ' + title);
})
.setValue("#userid", "user")
.setValue("#password", "pass")
.click("input[alt='Log in']");
});
I'm trying to test different parts of a "mostly" single page application. I'd like to split the tests up, but I really only want to load the page once and then have the tests go through and click the links etc.
Here's my code:
PRE.js
var port = require('system').env.PORT
var tester;
casper.options.viewportSize = {width: 1024, height: 768};
casper.test.begin('Test login', function suite(test) {
var done = false;
casper.on("page.error", function(msg, trace) {
this.echo("Error: " + msg, "ERROR");
this.echo("file: " + trace[0].file, "WARNING");
this.echo("line: " + trace[0].line, "WARNING");
this.echo("function: " + trace[0]["function"], "WARNING");
});
casper.on('remote.message', function(message) {
this.echo('remote message caught: ' + message);
if (message == "done") {
done = true;
}
});
casper.start('http://localhost:' + port, function() {
// Verify that the main menu links are present.
test.assertExists('input[name=username]');
// 10 articles should be listed.
test.assertElementCount('input', 3);
casper.fill("form", {
"username": "username",
"password": "my password goes right here you cant have it"
}, true);
casper.then(function() {
casper.waitFor(function(){
return done;
}, function(){
tester = casper.evaluate(function(){
return tester;
});
test.assert("undefined" != typeof tester);
test.assert(Object.keys(tester).length > 0);
});
});
});
casper.run(function() {
test.done();
});
});
and then I have a second file (and there will be lots more like this):
TEST.js
casper.test.assert(true);
casper.capture('.screenshot.png');
casper.test.done();
I'm hoping to get a screenshot of the browser session from pre.js.
I run it from a specialized program that starts up my program, but in essence it runs:
casperjs test casper_tests --pre=pre.js
casper_tests holds both files above
My Question:
What's the right way to do this? No screenshot is being taken, and perhaps more important (though I haven't tried it yet) I want to be able to click things inside and verify that other pieces are working. The screenshot just verifies that i'm in the right neighborhood.
This will not be easily possible and potentially dangerous. Every action that you do, would need to be reversed to not break the other tests. If you later decide that writing tests in a modular manner is a good thing, you will have a headache writing your tests.
PRE.js will be your start script which you modify to execute your tests in between. In the following fully working example you see how you can schedule multiple test cases for one execution of casper. This is bad, because the canvas test case depends on the proper back execution of the link test case.
casper.start('http://example.com');
casper.then(function() {
this.test.begin("link", function(test){
var url = casper.getCurrentUrl();
test.assertExists("a");
casper.click("a");
casper.then(function(){
test.assert(this.getCurrentUrl() !== url);
this.back(); // this is bad
test.done();
});
});
this.test.begin("canvas", function(test){
test.assertNotExists("canvas");
test.done();
});
});
casper.run();
Of course you can open the root again for the new test case, but then you have the same problem as with your initial code.
var url = 'http://example.com';
casper.start();
casper.thenOpen(url, function() {
this.test.begin("link", function(test){
var url = casper.getCurrentUrl();
test.assertExists("a");
casper.click("a");
casper.then(function(){
test.assert(this.getCurrentUrl() !== url);
test.done();
});
});
});
casper.thenOpen(url, function() {
this.test.begin("canvas", function(test){
test.assertNotExists("canvas");
test.done();
});
});
casper.run();
Now the test cases don't depend on each other, but you also load the page multiple times.
If you need some initial actions for every test case then the PRE.js is not the right place for that.
Create include.js and put the following code there:
function login(suite, username, password){
username = username || "defaultUsername";
password = password || "defaultPassword";
casper.test.begin('Test login', function suite(test) {
var done = false;
// event handlers
casper.start('http://localhost:' + port, function() {
// login if the session expired or it is the first run
if (!loggedIn) {
// login
}
// wait
});
casper.then(function(){
suite.call(casper, test);
});
casper.run(function() {
test.done();
});
});
}
Then you can run it as casperjs test casper_tests --includes=include.js with test files like
login(function(test){
this.click("#something");
this.waitForSelector(".somethingChanged");
this.then(function(){
test.assertExists(".somethingElseAlsoHappened");
});
});
Of course you can have different login functions (with different names) or more lightweight ones.
Building on the previous snippets, you can make a start script and load the test files yourself. Then you have all the flexibility you need to do this.
include.js:
function login(testScript, username, password, next){
// event handlers
casper.start('http://localhost:' + port, function() {
// login if the session expired or it is the first run
// waiting
});
testScript.forEach(function(case){
casper.thenOpen(case.name, function(){
this.test.begin(function suite(test){
case.func.call(casper, test);
casper.then(function(){
test.done();
});
});
});
});
casper.run(next);
}
start.js:
// pass the test file folder to the script and read it with sys.args
// fs.list(path) all files in that path and iterate over them
var filesContents = files.map(function(filename){
return require(filename).testcases;
});
var end = null;
// stack the test cases into the `run` callback of the previous execution
filesContents.forEach(function(case){
var newEnd = end;
var newFunc = function(){ login(case, u, p, newEnd) };
end = newFunc;
});
end(); // run the stack in reverse
each test file would look like this:
exports.testcases = [
{
name: "sometest",
func: function(test){
test.assert(true)
this.echo(this.getCurrenturl());
}
},
{
name: "sometest2",
func: function(test){
test.assert(true)
this.echo(this.getCurrenturl());
}
},
];
This is just a suggestion.
My code is working fine, but I do not like at all.
I would like to split one file into two files, one containing webServices and another one with a controller.
My file do something like this:
File: Validacion.js (controller)
// Load next view
var MainView = Alloy.createController('index').getView('tabGroup');
// this a function call when I click a button "validar" on Validación View.
function btnClick(){
var url = 'www.cocoloco.com/whatever';
var webService = Ti.Network.createHTTPClient({
onload: function(e){
// open new view
MainView.open();
// close actual view
$.tabValidacion.close();
$.tabValidacion = null;
},
onerror: function(e){
alert('onerror: ' + e.error);
},
timeout: 5000
});
webService.open('POST', url);
webService.send();
}
But I would like to do something like this below (divided in two files: webServices.js -library- and validation.js -controller-).
The problem is that I always have the message "error" because I pass throught "success = webServices.protocol();" but as far as it is "asynchronous" it doesn't stop and goes to following line of code without having server answer yet.
File: webServices.js (library)
exports.protocol = function(){
var url = 'www.cocoloco.com/whatever';
var webService = Ti.Network.createHTTPClient({
onload: function(e){
// on sucess exit with true
return(true);
},
onerror: function(e){
alert('onerror: ' + e.error);
// on sucess exit with false
return(false);
},
timeout: 5000
});
webService.open('POST', url);
webService.send();
}
File: Validacion.js (controller)
// Load next view
var MainView = Alloy.createController('index').getView('tabGroup');
function btnClick(){
var webServices = require('webServices');
var success = webServices.protocol();
if(success){
// open new view
MainView.open();
// close actual view
$.tabValidacion.close();
$.tabValidacion = null;
}else{
alert('error');
}
}
I have thought about two possible options:
Using promises.
Fire a new event on "success" and use that event run another callback function (in this function I open the new view and close the previous one).
I do not know how difficult is this as far as the event is one file (library) and the callback function in another one (controller)
I have never used any of these solutions, so I do not know how "good" they are.
Any suggestion?
The callback approach works fine in most cases. Just pass the function as a parameter, you can return an object containing anything from a success message to responseText and status.
webServices.js
exports.protocol = function(callback) {
var url = 'www.cocoloco.com/whatever';
var webService = Ti.Network.createHTTPClient({
onload: function(e){
// on success call callback
callback({ success: true });
},
onerror: function(e){
// on error call callback
callback({ success: false });
},
timeout: 5000
});
webService.open('POST', url);
webService.send();
}
Validacion.js
function btnClick(){
var webServices = require('webServices');
webServices.protocol(function(e) {
if(e.success){
// open new view
MainView.open();
// close actual view
$.tabValidacion.close();
$.tabValidacion = null;
} else {
alert('error');
}
});
}