Speed Up Web Form Testing With Bookmarklets

I was recently watching a colleague trying to recreate a bug that involved filling out a 20 field form, and after his 3rd effort, I thought "Wouldn't it be awesome if there was a quick way of pre-populating the form to speed up testing?". Obviously the major browsers have auto-complete options, but these seem to be more geared towards names, addresses, and credit card numbers, which was no good in this situation. It did get me thinking though, if the browser could auto-fill values, why couldn't we do the same using javascript?

The simplest way I knew to do this, was to use a bookmarklet, so I wrote a test bookmarklet (jsOutput)  to populate a field:

javascript:(function(){document.getElementById("sku").value = "123456";})();

It looked promising as a solution, but the downside was that it was still painful to write the script for a form of that size, due to the number of ids and values that would need to be keyed.

Then, on my bike-ride home, I had another Eureka moment -  if I can script the population of the form, why not script the creation of the bookmarklet url based on the values of an existing form?

A bit of playing around and I came up with my first cut of the jsGrab bookmarklet:

javascript:(function(){ function copyToClipboard (text) { window.prompt ("Copy to clipboard Using Ctrl+C and then paste into destination bookmarklet", text); } function htmlTree(obj){ var obj = obj || document.getElementsByTagName('body')[0]; var str = ""; if ((obj.tagName === 'INPUT' || obj.tagName === 'SELECT' || obj.tagName === 'TEXTAREA') && !obj.readonly && !obj.hidden && obj.id && obj.id != "" && !obj.disabled) { str = "setVal('"+obj.id + "','" + obj.value + "');"; } if (obj.hasChildNodes()) { var child = obj.firstChild; while (child) { if (child.nodeType === 1) { str += htmlTree(child) } child = child.nextSibling; } } return str; } copyToClipboard('javascript:(function(){function setVal(id,val) {var obj = document.getElementById(id); if (obj && !obj.readOnly && !obj.hidden){obj.value = val;}}' + htmlTree() + '})();'); alert('Text has been copied to clipboard');})();

It was a bit rough around the edges, and didn't handle checkboxes or radio buttons, but given a form on a page where the fields were keyed but not yet submitted, I could click on the bookmarklet, press Ctrl+c when the pop-up displayed the bookmarklet code, and paste this into the jsOutput bookmarklet. Here's a sample of the generated code:

javascript:(function(){function setVal(i,f,field,val) {var obj = null; if (f[i] && f[i] != "" && document.forms[f[i]] && document.forms[f[i]][field]) {obj = document.forms[f[i]][field];} else if(document.forms[i] && document.forms[i][field]) {obj = document.forms[i][field];} if (obj && !obj.readOnly && !obj.hidden){if (obj.type === "checkbox" || obj.type === "radio"){obj.checked = val} else {obj.value = unescape(val);}}} var f = ["facebookLoginForm","providerRegForm"]; setVal(0,f,'mode','DATA'); setVal(0,f,'accessToken',''); setVal(1,f,'',true); setVal(1,f,'profileImageUrl',''); setVal(1,f,'facebook',''); setVal(1,f,'linkedin',''); setVal(1,f,'skills','%22%22'); setVal(1,f,'summary',''); setVal(1,f,'education','%22%22'); setVal(1,f,'experience','%22%22'); setVal(1,f,'licenses','%22%22'); setVal(1,f,'emailAddress','test@test.com'); setVal(1,f,'firstName','John'); setVal(1,f,'lastName','Doe'); setVal(1,f,'username','johndoe'); setVal(1,f,'password1','password'); setVal(1,f,'password2','password');})();

I asked for some feedback from some of the full time web devs in the office, and the feeling was that the script would work well because we use the Spring framework (which generates ids for all form fields), but it would not work on any fields that didn't have an id set. The suggestion was that documents.forms["formName"]["fieldName"].value might be more reliable.

Never one to shy away from a challenge, I went about rewriting jsGrab, and whilst I was at it, I added support for checkboxes, radio buttons, and escaping of special characters:

javascript:(function(){function copyToClipboard (text) { window.prompt ("Copy to clipboard Using Ctrl+C and then paste into destination bookmarklet", text); } function addToForms(i,form) {var str = ''; var obj = document.forms[i].querySelectorAll(['textarea', 'select', 'input']); for (var j=0; j<obj.length; j++) { if (obj[j].type =="checkbox"||obj[j].type =="radio") {str += "setVal(" + i + ",f,'" + obj[j].name + "'," + obj[j].checked + ");";} else { str += "setVal(" + i + ",f,'" + obj[j].name + "','" + escape(obj[j].value) + "');";}} return str;}var forms = document.forms; var str = ""; var f = new Array();for (var i=0;i<forms.length;i++){f.push('"' + forms[i].name + '"' ); str += addToForms(i,forms[i]);} copyToClipboard('javascript:(function(){function setVal(i,f,field,val) {var obj = null; if (f[i] && f[i] != "" && document.forms[f[i]] && document.forms[f[i]][field]) {obj = document.forms[f[i]][field];} else if((!f[i] || f[i] === "") && document.forms[i] && document.forms[i][field]) {obj = document.forms[i][field];} if (obj && !obj.readOnly && !obj.hidden){if (obj.type === "checkbox" || obj.type === "radio"){obj.checked = val} else {obj.value = unescape(val);}}} var f = [' + f.toString() +'];' + str +'})();');})();

The code in the generated bookmarklet tries to match the form and field names, or uses the form index on the page if no form name is present:

javascript:(function(){function setVal(i,f,field,val) {var obj = null; if (f[i] && f[i] != "" && document.forms[f[i]] && document.forms[f[i]][field]) {obj = document.forms[f[i]][field];} else if((!f[i] || f[i] === "") && document.forms[i] && document.forms[i][field]) {obj = document.forms[i][field];} if (obj && !obj.readOnly && !obj.hidden){if (obj.type === "checkbox" || obj.type === "radio"){obj.checked = val} else {obj.value = unescape(val);}}} var f = ["search_form","profile_form"]; setVal(0,f,'subcats','Y'); setVal(0,f,'type','extended'); setVal(0,f,'status','A'); setVal(0,f,'pshort','Y'); setVal(0,f,'pfull','Y'); setVal(0,f,'pname','Y'); setVal(0,f,'pkeywords','Y'); setVal(0,f,'search_performed','Y'); setVal(0,f,'q',''); setVal(0,f,'',''); setVal(0,f,'dispatch','products.search'); setVal(1,f,'selected_section','general'); setVal(1,f,'default_cc',''); setVal(1,f,'profile_id',''); setVal(1,f,'user_data[email]','test@test.com'); setVal(1,f,'user_data[password1]','password'); setVal(1,f,'user_data[password2]','password'); setVal(1,f,'user_data[birthday]','01-01-1999'); setVal(1,f,'user_data[firstname]','John'); setVal(1,f,'user_data[lastname]','Doe'); setVal(1,f,'user_data[phone]','031234567'); setVal(1,f,'mailing_lists[1]','0'); setVal(1,f,'mailing_lists[1]',false); setVal(1,f,'mailing_lists[2]','0'); setVal(1,f,'mailing_lists[2]',false); setVal(1,f,'newsletter_format','2'); setVal(1,f,'dispatch[profiles.add.]', 'Create%20an%20Account');})();

It's worth noting that this method should not be relied on when testing, since it does not accurately simulate user behaviour. It may, for example, cause problems if there is javascript that fires when keys are pressed.

There may also be alternative solutions out there, such as keystroke recording and playback, that may provide a more authentic automated way of completing forms, but if you're looking for a quick and dirty way to repeatedly fill out the same form over and over again, then this method certainly shows promise.

/
If you found this post informative, enlightening, entertaining, or it's just helped you solve a problem then please feel free to shout me a coffee for my trouble from extensive menu below:
Today's Menu