507 lines
16 KiB
JavaScript
507 lines
16 KiB
JavaScript
//>>built
|
|
// wrapped by build app
|
|
define("dojox/robot/recorder", ["dijit","dojo","dojox"], function(dijit,dojo,dojox){
|
|
dojo.provide("dojox.robot.recorder");
|
|
dojo.experimental("dojox.robot.recorder");
|
|
// summary:
|
|
// Generates a doh test as you interact with a Web page.
|
|
// To record a test, click inside the document body and press CTRL-ALT-ENTER.
|
|
// To finish recording a test and to display the autogenerated code, press CTRL-ALT-ENTER again.
|
|
//
|
|
|
|
|
|
(function(){
|
|
|
|
// CONSTANTS
|
|
|
|
// consolidate keypresses into one typeKeys if they occur within 1 second of each other
|
|
var KEYPRESS_MAXIMUM_DELAY = 1000;
|
|
|
|
// consolidate mouse movements if they occur within .5 seconds of each other
|
|
var MOUSEMOVE_MAXIMUM_DELAY = 500;
|
|
|
|
// absolute longest wait between commands
|
|
// anything longer gets chopped to 10
|
|
var MAXIMUM_DELAY = 10000;
|
|
|
|
// stack of commands recorded from dojo.connects
|
|
var commands = [];
|
|
|
|
// number to write next to test name
|
|
// goes up after each recording
|
|
var testNumber = 0;
|
|
|
|
// time user started test
|
|
var startTime = null;
|
|
|
|
// time since last user input
|
|
// robot commands work on deltas
|
|
var prevTime = null;
|
|
|
|
var start = function(){
|
|
// summary:
|
|
// Starts recording the user's input.
|
|
//
|
|
alert("Started recording.");
|
|
commands = [];
|
|
startTime = new Date();
|
|
prevTime = new Date();
|
|
}
|
|
|
|
var addCommand = function(name, args){
|
|
// summary:
|
|
// Add a command to the stack.
|
|
//
|
|
// name:
|
|
// doh.robot function to call.
|
|
//
|
|
// args:
|
|
// arguments array to pass to the doh.robot
|
|
//
|
|
|
|
// omit start/stop record
|
|
if(startTime == null
|
|
|| name=="doh.robot.keyPress"
|
|
&& args[0]==dojo.keys.ENTER
|
|
&& eval("("+args[2]+")").ctrl
|
|
&& eval("("+args[2]+")").alt){ return; }
|
|
var dt = Math.max(Math.min(Math.round((new Date()).getTime() - prevTime.getTime()),MAXIMUM_DELAY),1);
|
|
// add in dt
|
|
// is usually args[1] but there are exceptions
|
|
if(name == "doh.robot.mouseMove"){
|
|
args[2]=dt;
|
|
}else{
|
|
args[1]=dt;
|
|
}
|
|
commands.push({name:name,args:args});
|
|
prevTime = new Date();
|
|
}
|
|
|
|
var _optimize = function(){
|
|
// make the stack more human-readable and remove any odditites
|
|
var c = commands;
|
|
|
|
// INITIAL OPTIMIZATIONS
|
|
// remove starting ENTER press
|
|
if(c[0].name == "doh.robot.keyPress"
|
|
&& (c[0].args[0] == dojo.keys.ENTER || c[0].args[0] == 77)){
|
|
c.splice(0,1);
|
|
}
|
|
// remove ending CTRL + ALT keypresses in IE
|
|
for(var i = c.length-1; (i >= c.length-2) && (i>=0); i-- ){
|
|
if(c[i].name == "doh.robot.keyPress"
|
|
&& c[i].args[0]==dojo.keys.ALT || c[i].args[0]==dojo.keys.CTRL){
|
|
c.splice(i,1);
|
|
}
|
|
}
|
|
// ITERATIVE OPTIMIZATIONS
|
|
for(i = 0; i<c.length; i++){
|
|
var next, nextdt;
|
|
if(c[i+1]
|
|
&& c[i].name=="doh.robot.mouseMove"
|
|
&& c[i+1].name==c[i].name
|
|
&& c[i+1].args[2]<MOUSEMOVE_MAXIMUM_DELAY){
|
|
// mouse movement optimization
|
|
// if the movement is temporally close, collapse it
|
|
// example: mouseMove(a,b,delay 5)+mouseMove(x,y,delay 10)+mousePress(delay 1) becomes mouseMove(x,y,delay 5)+mousePress(delay 11)
|
|
// expected pattern:
|
|
// c[i]: mouseMove
|
|
// c[i+1]: mouseMove
|
|
// ...
|
|
// c[i+n-1]: last mouseMove
|
|
// c[i+n]: something else
|
|
// result:
|
|
// c[i]: last mouseMove
|
|
// c[i+1]: something else
|
|
// the c[i] mouse location changes to move to c[i+n-1]'s location, c[i+n] gains c[i+1]+c[i+2]+...c[i+n-1] delay so the timing is the same
|
|
|
|
next = c[i+1];
|
|
nextdt = 0;
|
|
while(next
|
|
&& next.name==c[i].name
|
|
&& next.args[2]<MOUSEMOVE_MAXIMUM_DELAY){
|
|
// cut out next
|
|
c.splice(i + 1,1);
|
|
// add next's delay to the total
|
|
nextdt += next.args[2];
|
|
// move to next mouse position
|
|
c[i].args[0]=next.args[0];
|
|
c[i].args[1]=next.args[1];
|
|
next = c[i+1];
|
|
}
|
|
// make the total delay the duration
|
|
c[i].args[3]=nextdt;
|
|
}else if(c[i+1]
|
|
&& c[i].name=="doh.robot.mouseWheel"
|
|
&& c[i+1].name==c[i].name
|
|
&& c[i+1].args[1]<MOUSEMOVE_MAXIMUM_DELAY){
|
|
// mouse wheel optimization
|
|
// if the movement is temporally close, collapse it
|
|
// example: mouseWheel(1,delay 5)+mouseWheel(-2,delay 10) becomes mouseWheel(-1,delay 5, speed 10)
|
|
// expected pattern:
|
|
// c[i]: mouseWheel
|
|
|
|
// c[i+1]: mouseWheel
|
|
// ...
|
|
// c[i+n-1]: last mouseWheel
|
|
// c[i+n]: something else
|
|
// result:
|
|
// c[i]: mouseWheel
|
|
// c[i+1]: something else
|
|
|
|
next = c[i+1];
|
|
nextdt = 0;
|
|
while(next
|
|
&& next.name==c[i].name
|
|
&& next.args[1]<MOUSEMOVE_MAXIMUM_DELAY){
|
|
// cut out next
|
|
c.splice(i + 1, 1);
|
|
// add next's delay to the total
|
|
nextdt += next.args[1];
|
|
// add in wheel amount
|
|
c[i].args[0]+=next.args[0];
|
|
next = c[i+1];
|
|
}
|
|
// make the total delay the duration
|
|
c[i].args[2]=nextdt;
|
|
}else if(c[i + 2]
|
|
&& c[i].name=="doh.robot.mouseMoveAt"
|
|
&& c[i+2].name=="doh.robot.scrollIntoView"){
|
|
// swap scrollIntoView of widget and mouseMoveAt
|
|
// the recorder traps scrollIntoView after the mouse click registers, but in playback, it is better to go the other way
|
|
// expected pattern:
|
|
// c[i]: mouseMoveAt
|
|
// c[i+1]: mousePress
|
|
// c[i+2]: scrollIntoView
|
|
// result:
|
|
// c[i]: scrollIntoView
|
|
// c[i+1]: mouseMoveAt
|
|
// c[i+2]: mousePress
|
|
var temp = c.splice(i+2,1)[0];
|
|
c.splice(i,0,temp);
|
|
}else if(c[i + 1]
|
|
&& c[i].name=="doh.robot.mousePress"
|
|
&& c[i+1].name=="doh.robot.mouseRelease"
|
|
&& c[i].args[0]==c[i+1].args[0]){
|
|
// convert mousePress+mouseRelease to mouseClick
|
|
// expected pattern:
|
|
// c[i]: mousePress
|
|
// c[i+1]: mouseRelease
|
|
// mouse buttons are the same
|
|
c[i].name = "doh.robot.mouseClick";
|
|
// delete extra mouseRelease
|
|
c.splice(i + 1,1);
|
|
// if this was already a mouse click, get rid of the next (dup) one
|
|
if(c[i+1] && c[i+1].name == "doh.robot.mouseClick" && c[i].args[0] == c[i+1].args[0]){
|
|
c.splice(i + 1, 1);
|
|
}
|
|
}else if(c[i + 1]
|
|
&& c[i - 1]
|
|
&& c[i - 1].name=="doh.robot.mouseMoveAt"
|
|
&& c[i].name=="doh.robot.mousePress"
|
|
&& c[i+1].name=="doh.robot.mouseMove"){
|
|
// convert mouseMoveAt+mousePress+mouseMove to mouseMoveAt+mousePress+mouseMoveAt+mouseMove
|
|
// this is to kick off dojo.dnd by moving the mouse 1 px
|
|
// expected pattern:
|
|
// c[i-1]: mouseMoveAt
|
|
// c[i]: mousePress
|
|
// c[i+1]: mouseMove
|
|
|
|
// insert new mouseMoveAt, 1px to the right
|
|
var cmd={name:"doh.robot.mouseMoveAt",args:[c[i-1].args[0], 1, 100, c[i-1].args[3]+1,c[i-1].args[4]]};
|
|
c.splice(i+1,0,cmd);
|
|
}else if(c[i + 1]
|
|
&& ((c[i].name=="doh.robot.keyPress"
|
|
&& typeof c[i].args[0] =="string")
|
|
|| c[i].name=="doh.robot.typeKeys")
|
|
&& c[i+1].name=="doh.robot.keyPress"
|
|
&& typeof c[i+1].args[0] =="string"
|
|
&& c[i+1].args[1]<=KEYPRESS_MAXIMUM_DELAY
|
|
&& !eval("("+c[i].args[2]+")").ctrl
|
|
&& !eval("("+c[i].args[2]+")").alt
|
|
&& !eval("("+c[i+1].args[2]+")").ctrl
|
|
&& !eval("("+c[i+1].args[2]+")").alt){
|
|
// convert keyPress+keyPress+... to typeKeys
|
|
// expected pattern:
|
|
// c[i]: keyPress(a)
|
|
// c[i+1]: keyPress(b)
|
|
// ...
|
|
// c[i+n-1]: last keyPress(z)
|
|
// c[i+n]: something else
|
|
// result:
|
|
// c[i]: typeKeys(ab...z)
|
|
// c[i+1]: something else
|
|
// note: does not convert alt or ctrl keypresses, and does not convert non-character keypresses like enter
|
|
c[i].name = "doh.robot.typeKeys";
|
|
c[i].args.splice(3,1);
|
|
next = c[i+1];
|
|
var typeTime = 0;
|
|
while(next
|
|
&& next.name == "doh.robot.keyPress"
|
|
&& typeof next.args[0] =="string"
|
|
&& next.args[1]<=KEYPRESS_MAXIMUM_DELAY
|
|
&& !eval("("+next.args[2]+")").ctrl
|
|
&& !eval("("+next.args[2]+")").alt){
|
|
c.splice(i + 1,1);
|
|
c[i].args[0] += next.args[0];
|
|
typeTime += next.args[1];
|
|
next = c[i+1];
|
|
}
|
|
// set the duration to the total type time
|
|
c[i].args[2] = typeTime;
|
|
// wrap string in quotes
|
|
c[i].args[0] = "'"+c[i].args[0]+"'";
|
|
}else if(c[i].name == "doh.robot.keyPress"){
|
|
// take care of standalone keypresses
|
|
// characters should be wrapped in quotes.
|
|
// non-characters should be replaced with their corresponding dojo.keys constant
|
|
if(typeof c[i].args[0] == "string"){
|
|
// one keypress of a character by itself should be wrapped in quotes
|
|
c[i].args[0] = "'"+c[i].args[0]+"'";
|
|
}else{
|
|
if(c[i].args[0]==0){
|
|
// toss null keypresses
|
|
c.splice(i,1);
|
|
}else{
|
|
// look up dojo.keys.constant if possible
|
|
for(var j in dojo.keys){
|
|
if(dojo.keys[j] == c[i].args[0]){
|
|
c[i].args[0] = "dojo.keys."+j;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var toggle = function(){
|
|
// summary:
|
|
// Toggles recording the user's input.
|
|
// Hotkey: CTRL- ALT-ENTER
|
|
//
|
|
if(!startTime){ start(); }
|
|
else{ stop(); }
|
|
}
|
|
|
|
var stop = function(){
|
|
// summary:
|
|
// Stops recording the user's input,
|
|
// and displays the generated code.
|
|
//
|
|
|
|
var dt = Math.round((new Date()).getTime() - startTime.getTime());
|
|
startTime = null;
|
|
_optimize();
|
|
var c = commands;
|
|
console.log("Stop called. Commands: " + c.length);
|
|
if(c.length){
|
|
var s = "doh.register('dojox.robot.AutoGeneratedTestGroup',{\n";
|
|
s += " name: 'autotest" + (testNumber++)+"',\n";
|
|
s += " timeout: " + (dt+2000)+",\n";
|
|
s += " runTest: function(){\n";
|
|
s += " var d = new doh.Deferred();\n";
|
|
for(var i = 0; i<c.length; i++){
|
|
s += " "+c[i].name+"(";
|
|
for(var j = 0; j<c[i].args.length; j++){
|
|
var arg = c[i].args[j];
|
|
s += arg;
|
|
if(j != c[i].args.length-1){ s += ", "; }
|
|
}
|
|
s += ");\n";
|
|
}
|
|
s += " doh.robot.sequence(function(){\n";
|
|
s += " if(/*Your condition here*/){\n";
|
|
s += " d.callback(true);\n";
|
|
s += " }else{\n";
|
|
s += " d.errback(new Error('We got a failure'));\n";
|
|
s += " }\n";
|
|
s += " }, 1000);\n";
|
|
s += " return d;\n";
|
|
s += " }\n";
|
|
s += "});\n";
|
|
var div = document.createElement('div');
|
|
div.id="dojox.robot.recorder";
|
|
div.style.backgroundColor = "white";
|
|
div.style.position = "absolute";
|
|
var scroll = {y: (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0),
|
|
x: (window.pageXOffset || (window["dojo"]?dojo._fixIeBiDiScrollLeft(document.documentElement.scrollLeft):undefined) || document.body.scrollLeft || 0)};
|
|
div.style.left = scroll.x+"px";
|
|
div.style.top = scroll.y+"px";
|
|
var h1 = document.createElement('h1');
|
|
h1.innerHTML = "Your code:";
|
|
div.appendChild(h1);
|
|
var pre = document.createElement('pre');
|
|
if(pre.innerText !== undefined){
|
|
pre.innerText = s;
|
|
}else{
|
|
pre.textContent = s;
|
|
}
|
|
div.appendChild(pre);
|
|
var button = document.createElement('button');
|
|
button.innerHTML = "Close";
|
|
var connect = dojo.connect(button,'onmouseup',function(e){
|
|
dojo.stopEvent(e);
|
|
document.body.removeChild(div);
|
|
dojo.disconnect(connect);
|
|
});
|
|
div.appendChild(button);
|
|
document.body.appendChild(div);
|
|
commands = [];
|
|
}
|
|
}
|
|
|
|
var getSelector = function(node){
|
|
// Selenium/Windmill recorders have a concept of a "selector."
|
|
// The idea is that recorders need some reference to an element that persists even after a page refresh.
|
|
// For elements with ids, this is easy; just use the id.
|
|
// For other elements, we have to be more sly.
|
|
|
|
if(typeof node =="string"){
|
|
// it's already an id to be interpreted by dojo.byId
|
|
return "'" + node+"'";
|
|
}else if(node.id){
|
|
// it has an id
|
|
return "'" + node.id+"'";
|
|
}else{
|
|
// TODO: need a generic selector, like CSS3/dojo.query, for the default return value
|
|
// for now, just do getElementsByTagName
|
|
var nodes = document.getElementsByTagName(node.nodeName);
|
|
var i;
|
|
for(i = 0; i<nodes.length; i++){
|
|
if(nodes[i] == node){
|
|
break;
|
|
}
|
|
}
|
|
// wrap in a function to defer evaluation
|
|
return "function(){ return document.getElementsByTagName('" + node.nodeName+"')["+i+"]; }";
|
|
}
|
|
}
|
|
|
|
var getMouseButtonObject = function(b){
|
|
// convert native event to doh.robot API
|
|
return "{left:" + (b==0)+", middle:" + (b==1)+", right:" + (b==2)+"}";
|
|
}
|
|
|
|
|
|
var getModifierObject = function(e){
|
|
// convert native event to doh.robot API
|
|
return "{'shift':" + (e.shiftKey)+", 'ctrl':" + (e.ctrlKey)+", 'alt':" + (e.altKey)+"}";
|
|
}
|
|
|
|
// dojo.connects
|
|
|
|
dojo.connect(document,"onkeydown",function(e){
|
|
// the CTRL- ALT-ENTER hotkey to activate the recorder
|
|
//console.log(e.keyCode + ", " + e.ctrlKey+", " + e.altKey);
|
|
if((e.keyCode == dojo.keys.ENTER || e.keyCode==77) && e.ctrlKey && e.altKey){
|
|
dojo.stopEvent(e);
|
|
toggle();
|
|
}
|
|
});
|
|
|
|
var lastEvent = {type:""};
|
|
|
|
var onmousedown = function(e){
|
|
// handler for mouse down
|
|
if(!e || lastEvent.type==e.type && lastEvent.button==e.button){ return; }
|
|
lastEvent={type:e.type, button:e.button};
|
|
var selector = getSelector(e.target);
|
|
var coords = dojo.coords(e.target);
|
|
addCommand("doh.robot.mouseMoveAt",[selector, 0, 100, e.clientX - coords.x, e.clientY-coords.y]);
|
|
addCommand("doh.robot.mousePress",[getMouseButtonObject(e.button-(dojo.isIE?1:0)), 0]);
|
|
};
|
|
|
|
var onclick = function(e){
|
|
// handler for mouse up
|
|
if(!e || lastEvent.type==e.type && lastEvent.button==e.button){ return; }
|
|
lastEvent={type:e.type, button:e.button};
|
|
var selector = getSelector(e.target);
|
|
var coords = dojo.coords(e.target);
|
|
addCommand("doh.robot.mouseClick",[getMouseButtonObject(e.button-(dojo.isIE?1:0)), 0]);
|
|
};
|
|
|
|
var onmouseup = function(e){
|
|
// handler for mouse up
|
|
if(!e || lastEvent.type==e.type && lastEvent.button==e.button){ return; }
|
|
lastEvent={type:e.type, button:e.button};
|
|
var selector = getSelector(e.target);
|
|
var coords = dojo.coords(e.target);
|
|
addCommand("doh.robot.mouseRelease",[getMouseButtonObject(e.button-(dojo.isIE?1:0)), 0]);
|
|
};
|
|
|
|
var onmousemove = function(e){
|
|
// handler for mouse move
|
|
if(!e || lastEvent.type==e.type && lastEvent.pageX==e.pageX && lastEvent.pageY==e.pageY){ return; }
|
|
lastEvent={type:e.type, pageX:e.pageX, pageY:e.pageY};
|
|
addCommand("doh.robot.mouseMove",[e.pageX, e.pageY, 0, 100, true]);
|
|
};
|
|
|
|
var onmousewheel = function(e){
|
|
// handler for mouse move
|
|
if(!e || lastEvent.type==e.type && lastEvent.pageX==e.pageX && lastEvent.pageY==e.pageY){ return; }
|
|
lastEvent={type:e.type, detail:(e.detail ? (e.detail) : (-e.wheelDelta / 120))};
|
|
addCommand("doh.robot.mouseWheel",[lastEvent.detail]);
|
|
};
|
|
|
|
var onkeypress = function(e){
|
|
// handler for key press
|
|
if(!e || lastEvent.type==e.type && (lastEvent.charCode == e.charCode && lastEvent.keyCode == e.keyCode)){ return; }
|
|
lastEvent={type:e.type, charCode:e.charCode, keyCode:e.keyCode};
|
|
addCommand("doh.robot.keyPress",[e.charOrCode==dojo.keys.SPACE?' ':e.charOrCode, 0, getModifierObject(e)]);
|
|
};
|
|
|
|
var onkeyup = function(e){
|
|
if(!e || lastEvent.type==e.type && (lastEvent.charCode == e.charCode && lastEvent.keyCode == e.keyCode)){ return; }
|
|
lastEvent={type:e.type, charCode:e.charCode, keyCode:e.keyCode};
|
|
}
|
|
|
|
// trap all native elements' events
|
|
dojo.connect(document,"onmousedown",onmousedown);
|
|
dojo.connect(document,"onmouseup",onmouseup);
|
|
dojo.connect(document,"onclick",onclick);
|
|
dojo.connect(document,"onkeypress",onkeypress);
|
|
dojo.connect(document,"onkeyup",onkeyup);
|
|
dojo.connect(document,"onmousemove",onmousemove);
|
|
dojo.connect(document,!dojo.isMozilla ? "onmousewheel" : 'DOMMouseScroll',onmousewheel);
|
|
|
|
dojo.addOnLoad(function(){
|
|
// get scrollIntoView for good measure
|
|
// catch: dojo.window might not be loaded (yet?) so addonload
|
|
if(dojo.window){
|
|
dojo.connect(dojo.window,"scrollIntoView",function(node){
|
|
addCommand("doh.robot.scrollIntoView",[getSelector(node)]);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get Dojo widget events too!
|
|
dojo.connect(dojo, "connect",
|
|
function(/*dijit._Widget*/ widget, /*String*/ event, /*Function*/ f){
|
|
// kill recursion
|
|
// check for private variable _mine to make sure this isn't a recursive loop
|
|
if(widget && (!f || !f._mine)){
|
|
var hitchedf = null;
|
|
if(event.toLowerCase() == "onmousedown"){
|
|
hitchedf = dojo.hitch(this,onmousedown);
|
|
}else if(event.toLowerCase() == (!dojo.isMozilla ? "onmousewheel" : 'dommousescroll')){
|
|
hitchedf = dojo.hitch(this,onmousewheel);
|
|
}else if(event.toLowerCase() == "onclick"){
|
|
hitchedf = dojo.hitch(this,onclick);
|
|
}else if(event.toLowerCase() == "onmouseup"){
|
|
hitchedf = dojo.hitch(this,onmouseup);
|
|
}else if(event.toLowerCase() == "onkeypress"){
|
|
hitchedf = dojo.hitch(this,onkeypress);
|
|
}else if(event.toLowerCase() == "onkeyup"){
|
|
hitchedf = dojo.hitch(this,onkeyup);
|
|
}
|
|
if(hitchedf == null){ return; }
|
|
hitchedf._mine = true;
|
|
dojo.connect(widget,event,hitchedf);
|
|
}
|
|
});
|
|
})();
|
|
|
|
});
|