It still seems a little clumsy, but I'm pretty new to javascript and
maybe there are some easy improvements. Maybe using the coffeescript
syntax would be an improvement (once I've learned coffeescript).
My code includes an 'interact' function which takes a WebPage object
and two function callbacks as input. The first callback contains the
code which will cause the page to reload (clicking a link, submitting
a form, etc.). The second callback contains the code which will be
called when the page load completes. So in order to navigate to a
second page you nest another 'interact' call inside the second
callback of the first 'interact' call. To navigate to a third page,
nest another interact call inside the second interact call, etc.
interact(page, function() {/* open, click, or form submit here
*/}, function() {/* Nested call to interact here */})
Here is an example interaction. It visits the phantomjs google-code
issue list page, clicks on the details link for the last issue on the
page, then displays the date/time of the last comment on the page (or
issue creation if there are no comments):
<code>
phantom.injectJs("interact.js");
page = new WebPage()
interact(page, function() {
// Open the issues list page
this.open("http://code.google.com/p/phantomjs/issues/list");
}, function() {
interact (this, function() {
// Click on the details link for the last issue on the page
// Would be nice to be able to write:
// click (this, '#resultstable > tbody >
tr:nth-last-of-type(1) > td.id > a')
// but I didn't figure out how
this.evaluate(function() {simulateMouseClick('#resultstable >
tbody > tr:nth-last-of-type(1) > td.id > a')})
}, function() {
last_update = this.evaluate(function() {
// Creation date or date of most recent comment
n = document.querySelectorAll('.author > .date,
.issuecomment > .date');
return n.item(n.length-1).getAttribute('title');
});
console.log ("Last updated " + last_update);
phantom.exit();
})
})
</code>
<code name='interact.js'>
var pageNum = 1;
function interact (page,causes_load_callback, onload_callback) {
console.log("Setting onLoadFinished");
page.onLoadFinished = function (status) {
// While developing a script, it is difficult to predict which
interactions
// QtWebkit will consider a page reload, so set a temporary callback to
// notify the script writer that an unexpected page reload took place
page.onLoadFinished = function(status) {
console.log(pageNum + "Unexpected page load: " + status + " - " +
page.evaluate(function() {
return document.title + " - " + document.location.href;
})
);
page.render("/tmp/unexpected-" + pageNum + ".png");
phantom.exit();
};
// For debugging, log the results and render the page to a
temp directory
console.log (status + ":" + pageNum + " Page load complete - " +
page.evaluate(function() {
return document.title + " - " + document.location.href;
})
);
page.render("/tmp/" + pageNum + ".png");
pageNum++;
// Allow elements to be clicked and call the page load callback
page.injectJs("simclick.js");
console.log("about to apply callback");
onload_callback.apply(page);
};
// Call the callback which will run code that will result in page reload
causes_load_callback.apply(page);
console.log("Return from interact");
return;
}
</code>
<code name='simclick.js'>
// code from http://code.google.com/p/phantomjs/issues/detail?id=47
function simulateMouseClick(selector) {
var targets = document.querySelectorAll(selector),
evt = document.createEvent('MouseEvents'),
i, len;
evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0,
false, false, false, false, 0, null);
for ( i = 0, len = targets.length; i < len; ++i ) {
targets[i].dispatchEvent(evt);
}
}
</code>
If I could figure out how to add code to phantomjs to download a file
(http://code.google.com/p/phantomjs/issues/detail?id=52), then most of
my automation needs would be resolved.
Brian
Looking at the way your code looks like, I believe we shall do
something in order to help you (and others with similar goals) produce
a much more readable and debuggable script.
I was reluctant to go the synchronous way, I even went as far as
removing the semi-blocking sleep() from 1.2. I was under the
impression that using a JavaScript microlibrary which allows writing
sequential statement will be good enough.
However, based on the feedback from other expert in the web testing
tool, I realize maybe this is a mistake from my side. Maybe we need
the synchronous mode after all, consider the interactive testing is
usually procedural and step-by-step. Consider the following
(pseudocode):
open a login page
set the user name
set the wrong password intentionally
click the login button
wait till the page reloads
verify that login is not possible
Having the sync mode built-in, with specified time-out, will allow the
test script to run linearly. Once we have remote debugging capability,
it's supereasy to trace and debug.
Being able to do certain operations both async and sync can lead to
confusion. However, my understanding is people with use only one mode
most of the time. For running unit tests headlessly, you don't really
care about sync mode. For interactive/user-emulation tests, you don'y
really care about async.
What do you guys think?
--
Ariya Hidayat
http://www.linkedin.com/in/ariyahidayat
The mysterious question would be, how to make such a procedural
testing easy to write and east to debug? Especially if this is such a
complicated and big test.
For running unit-tests headless, what we have is already more than
enough. For creating smoke tests and user simulations, I just believe
it could be significantly improved.
Regards,
Ariya
Sure, I wouldn't object to that. Long-term, that sounds useful.
Brian
Yeah, I think the recursive nesting of my original approach is much
more clumsy looking than it need be. I have made a change to allow
it to be linear. Here is the revised code:
<code>
phantom.injectJs("interact2.js");
page = new WebPage()
var steps = [
function() {
// Open the issues list page
this.open("http://code.google.com/p/phantomjs/issues/list");
},
function() {
// Click on the details link for the last issue on the page
// Would be nice to be able to write:
// click (this, '#resultstable > tbody >
tr:nth-last-of-type(1) > td.id > a')
// but I didn't figure out how
this.evaluate(function() {simulateMouseClick('#resultstable >
tbody > tr:nth-last-of-type(1) > td.id > a')})
},
function() {
last_update = this.evaluate(function() {
// Creation date or date of most recent comment
n = document.querySelectorAll('.author > .date,
.issuecomment > .date');
return n.item(n.length-1).getAttribute('title');
});
console.log ("Last updated " + last_update);
phantom.exit();
}
];
interact(page, steps)
</code>
<code interact2.js>
var pageNum = 1;
function interact (page,callback_list) {
console.log("Setting onLoadFinished");
page.onLoadFinished = function (status) {
// For debugging, log the results and render the page to a
temp directory
console.log (status + ":" + pageNum + " Page load complete - " +
page.evaluate(function() {
return document.title + " - " + document.location.href;
})
);
page.render("/tmp/" + pageNum + ".png");
pageNum++;
// Allow elements to be clicked
page.injectJs("simclick.js");
console.log("about to apply callback");
// Call interact again--pass only the functions that
// haven't been invoked yet
interact(page, callback_list.slice(1));
};
// Call the first callback in the list. It should result in page
reload so the
// above onLoadFinished gets called
callback_list[0].apply(page);
console.log("Return from interact");
return;
}
</code>
<code simclick.js -- unchanged from last email/>
Brian
On Sat, Jul 2, 2011 at 2:39 PM, Peter Lyons <pe...@peterlyons.com> wrote:
> I made my own variation of Brian's interact.js (in coffeescript).
[...]
> Please let me know if you find that useful or can offer other
> approaches to this type of use case.
I like it and have switched the code I was writing to use it. Thanks
for sharing.
I'm new to javascript/dom coding...could you explain why you attached
the function to the window object?
> window.interact = (page, actions, verbose=true) ->
Brian
Please star issue 157 to follow the updates.
Thoughts?
--
Ariya Hidayat
http://www.google.com/search?q=ariya+hidayat