source: Dev/trunk/src/client/dojox/robot/recorder.js

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 15.8 KB
Line 
1dojo.provide("dojox.robot.recorder");
2dojo.experimental("dojox.robot.recorder");
3// summary:
4//      Generates a doh test as you interact with a Web page.
5//      To record a test, click inside the document body and press CTRL-ALT-ENTER.
6//      To finish recording a test and to display the autogenerated code, press CTRL-ALT-ENTER again.
7//
8
9
10(function(){
11
12// CONSTANTS
13
14// consolidate keypresses into one typeKeys if they occur within 1 second of each other
15var KEYPRESS_MAXIMUM_DELAY = 1000;
16
17// consolidate mouse movements if they occur within .5 seconds of each other
18var MOUSEMOVE_MAXIMUM_DELAY = 500;
19
20// absolute longest wait between commands
21// anything longer gets chopped to 10
22var MAXIMUM_DELAY = 10000;
23
24// stack of commands recorded from dojo.connects
25var commands = [];
26
27// number to write next to test name
28// goes up after each recording
29var testNumber = 0;
30
31// time user started test
32var startTime = null;
33
34// time since last user input
35// robot commands work on deltas
36var prevTime = null;
37
38var start = function(){
39        // summary:
40        //      Starts recording the user's input.
41        //
42        alert("Started recording.");
43        commands = [];
44        startTime = new Date();
45        prevTime = new Date();
46}
47
48var addCommand = function(name, args){
49        // summary:
50        //      Add a command to the stack.
51        //
52        // name:
53        //      doh.robot function to call.
54        //
55        // args:
56        //      arguments array to pass to the doh.robot
57        //
58
59        // omit start/stop record
60        if(startTime == null
61                || name=="doh.robot.keyPress"
62                && args[0]==dojo.keys.ENTER
63                && eval("("+args[2]+")").ctrl
64                && eval("("+args[2]+")").alt){ return; }
65        var dt = Math.max(Math.min(Math.round((new Date()).getTime() - prevTime.getTime()),MAXIMUM_DELAY),1);
66        // add in dt
67        // is usually args[1] but there are exceptions
68        if(name == "doh.robot.mouseMove"){
69                args[2]=dt;
70        }else{
71                args[1]=dt;
72        }
73        commands.push({name:name,args:args});
74        prevTime = new Date();
75}
76
77var _optimize = function(){
78        // make the stack more human-readable and remove any odditites
79        var c = commands;
80
81        // INITIAL OPTIMIZATIONS
82        // remove starting ENTER press
83        if(c[0].name == "doh.robot.keyPress"
84                && (c[0].args[0] == dojo.keys.ENTER || c[0].args[0] == 77)){
85                c.splice(0,1);
86        }
87        // remove ending CTRL + ALT keypresses in IE
88        for(var i = c.length-1; (i >= c.length-2) && (i>=0); i-- ){
89                if(c[i].name == "doh.robot.keyPress"
90                        && c[i].args[0]==dojo.keys.ALT || c[i].args[0]==dojo.keys.CTRL){
91                        c.splice(i,1);
92                }
93        }
94        // ITERATIVE OPTIMIZATIONS
95        for(i = 0; i<c.length; i++){
96                var next, nextdt;
97                if(c[i+1]
98                        && c[i].name=="doh.robot.mouseMove"
99                        && c[i+1].name==c[i].name
100                        && c[i+1].args[2]<MOUSEMOVE_MAXIMUM_DELAY){
101                        // mouse movement optimization
102                        // if the movement is temporally close, collapse it
103                        // example: mouseMove(a,b,delay 5)+mouseMove(x,y,delay 10)+mousePress(delay 1) becomes mouseMove(x,y,delay 5)+mousePress(delay 11)
104                        // expected pattern:
105                        //      c[i]: mouseMove
106                        //      c[i+1]: mouseMove
107                        //      ...
108                        //      c[i+n-1]: last mouseMove
109                        //      c[i+n]: something else
110                        // result:
111                        //      c[i]: last mouseMove
112                        //      c[i+1]: something else
113                        // 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
114
115                        next = c[i+1];
116                        nextdt = 0;
117                        while(next
118                                && next.name==c[i].name
119                                && next.args[2]<MOUSEMOVE_MAXIMUM_DELAY){
120                                // cut out next
121                                c.splice(i + 1,1);
122                                // add next's delay to the total
123                                nextdt += next.args[2];
124                                // move to next mouse position
125                                c[i].args[0]=next.args[0];
126                                c[i].args[1]=next.args[1];
127                                next = c[i+1];
128                        }
129                        // make the total delay the duration
130                        c[i].args[3]=nextdt;
131                }else if(c[i+1]
132                        && c[i].name=="doh.robot.mouseWheel"
133                        && c[i+1].name==c[i].name
134                        && c[i+1].args[1]<MOUSEMOVE_MAXIMUM_DELAY){
135                        // mouse wheel optimization
136                        // if the movement is temporally close, collapse it
137                        // example: mouseWheel(1,delay 5)+mouseWheel(-2,delay 10) becomes mouseWheel(-1,delay 5, speed 10)
138                        // expected pattern:
139                        //      c[i]: mouseWheel
140
141                        //      c[i+1]: mouseWheel
142                        //      ...
143                        //      c[i+n-1]: last mouseWheel
144                        //      c[i+n]: something else
145                        // result:
146                        //      c[i]: mouseWheel
147                        //      c[i+1]: something else
148
149                        next = c[i+1];
150                        nextdt = 0;
151                        while(next
152                                && next.name==c[i].name
153                                && next.args[1]<MOUSEMOVE_MAXIMUM_DELAY){
154                                // cut out next
155                                c.splice(i + 1, 1);
156                                // add next's delay to the total
157                                nextdt += next.args[1];
158                                // add in wheel amount
159                                c[i].args[0]+=next.args[0];
160                                next = c[i+1];
161                        }
162                        // make the total delay the duration
163                        c[i].args[2]=nextdt;
164                }else if(c[i + 2]
165                        && c[i].name=="doh.robot.mouseMoveAt"
166                        && c[i+2].name=="doh.robot.scrollIntoView"){
167                        // swap scrollIntoView of widget and mouseMoveAt
168                        // the recorder traps scrollIntoView after the mouse click registers, but in playback, it is better to go the other way
169                        // expected pattern:
170                        //      c[i]: mouseMoveAt
171                        //      c[i+1]: mousePress
172                        //      c[i+2]: scrollIntoView
173                        // result:
174                        //      c[i]: scrollIntoView
175                        //      c[i+1]: mouseMoveAt
176                        //      c[i+2]: mousePress
177                        var temp = c.splice(i+2,1)[0];
178                        c.splice(i,0,temp);
179                }else if(c[i + 1]
180                        && c[i].name=="doh.robot.mousePress"
181                        && c[i+1].name=="doh.robot.mouseRelease"
182                        && c[i].args[0]==c[i+1].args[0]){
183                        // convert mousePress+mouseRelease to mouseClick
184                        // expected pattern:
185                        //      c[i]: mousePress
186                        //      c[i+1]: mouseRelease
187                        //      mouse buttons are the same
188                        c[i].name = "doh.robot.mouseClick";
189                        // delete extra mouseRelease
190                        c.splice(i + 1,1);
191                        // if this was already a mouse click, get rid of the next (dup) one
192                        if(c[i+1] && c[i+1].name == "doh.robot.mouseClick" && c[i].args[0] == c[i+1].args[0]){
193                                c.splice(i + 1, 1);
194                        }
195                }else if(c[i + 1]
196                        && c[i - 1]
197                        && c[i - 1].name=="doh.robot.mouseMoveAt"
198                        && c[i].name=="doh.robot.mousePress"
199                        && c[i+1].name=="doh.robot.mouseMove"){
200                        // convert mouseMoveAt+mousePress+mouseMove to mouseMoveAt+mousePress+mouseMoveAt+mouseMove
201                        // this is to kick off dojo.dnd by moving the mouse 1 px
202                        // expected pattern:
203                        //      c[i-1]: mouseMoveAt
204                        //      c[i]: mousePress
205                        //      c[i+1]: mouseMove
206
207                        // insert new mouseMoveAt, 1px to the right
208                        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]]};
209                        c.splice(i+1,0,cmd);
210                }else if(c[i + 1]
211                        && ((c[i].name=="doh.robot.keyPress"
212                                && typeof c[i].args[0] =="string")
213                                || c[i].name=="doh.robot.typeKeys")
214                        && c[i+1].name=="doh.robot.keyPress"
215                        && typeof c[i+1].args[0] =="string"
216                        && c[i+1].args[1]<=KEYPRESS_MAXIMUM_DELAY
217                        && !eval("("+c[i].args[2]+")").ctrl
218                        && !eval("("+c[i].args[2]+")").alt
219                        && !eval("("+c[i+1].args[2]+")").ctrl
220                        && !eval("("+c[i+1].args[2]+")").alt){
221                        // convert keyPress+keyPress+... to typeKeys
222                        // expected pattern:
223                        //      c[i]: keyPress(a)
224                        //      c[i+1]: keyPress(b)
225                        //      ...
226                        //      c[i+n-1]: last keyPress(z)
227                        //      c[i+n]: something else
228                        // result:
229                        //      c[i]: typeKeys(ab...z)
230                        //      c[i+1]: something else
231                        // note: does not convert alt or ctrl keypresses, and does not convert non-character keypresses like enter
232                        c[i].name = "doh.robot.typeKeys";
233                        c[i].args.splice(3,1);
234                        next = c[i+1];
235                        var typeTime = 0;
236                        while(next
237                                && next.name == "doh.robot.keyPress"
238                                && typeof next.args[0] =="string"
239                                && next.args[1]<=KEYPRESS_MAXIMUM_DELAY
240                                && !eval("("+next.args[2]+")").ctrl
241                                && !eval("("+next.args[2]+")").alt){
242                                c.splice(i + 1,1);
243                                c[i].args[0] += next.args[0];
244                                typeTime += next.args[1];
245                                next = c[i+1];
246                        }
247                        // set the duration to the total type time
248                        c[i].args[2] = typeTime;
249                        // wrap string in quotes
250                        c[i].args[0] = "'"+c[i].args[0]+"'";
251                }else if(c[i].name == "doh.robot.keyPress"){
252                        // take care of standalone keypresses
253                        // characters should be wrapped in quotes.
254                        // non-characters should be replaced with their corresponding dojo.keys constant
255                        if(typeof c[i].args[0] == "string"){
256                                // one keypress of a character by itself should be wrapped in quotes
257                                c[i].args[0] = "'"+c[i].args[0]+"'";
258                        }else{
259                                if(c[i].args[0]==0){
260                                        // toss null keypresses
261                                        c.splice(i,1);
262                                }else{
263                                        // look up dojo.keys.constant if possible
264                                        for(var j in dojo.keys){
265                                                if(dojo.keys[j] == c[i].args[0]){
266                                                        c[i].args[0] = "dojo.keys."+j;
267                                                        break;
268                                                }
269                                        }
270                                }
271                        }
272                }
273        }
274}
275
276var toggle = function(){
277        // summary:
278        //      Toggles recording the user's input.
279        //      Hotkey: CTRL- ALT-ENTER
280        //
281        if(!startTime){ start(); }
282        else{ stop(); }
283}
284
285var stop = function(){
286        // summary:
287        //      Stops recording the user's input,
288        //      and displays the generated code.
289        //
290
291        var dt = Math.round((new Date()).getTime() - startTime.getTime());
292        startTime = null;
293        _optimize();
294        var c = commands;
295        console.log("Stop called. Commands: " + c.length);
296        if(c.length){
297                var s = "doh.register('dojox.robot.AutoGeneratedTestGroup',{\n";
298                s += "     name: 'autotest" + (testNumber++)+"',\n";
299                s += "     timeout: " + (dt+2000)+",\n";
300                s += "     runTest: function(){\n";
301                s += "          var d = new doh.Deferred();\n";
302                for(var i = 0; i<c.length; i++){
303                        s += "          "+c[i].name+"(";
304                        for(var j = 0; j<c[i].args.length; j++){
305                                var arg = c[i].args[j];
306                                s += arg;
307                                if(j != c[i].args.length-1){ s += ", "; }
308                        }
309                        s += ");\n";
310                }
311                s += "          doh.robot.sequence(function(){\n";
312                s += "               if(/*Your condition here*/){\n";
313                s += "                    d.callback(true);\n";
314                s += "               }else{\n";
315                s += "                    d.errback(new Error('We got a failure'));\n";
316                s += "               }\n";
317                s += "          }, 1000);\n";
318                s += "          return d;\n";
319                s += "     }\n";
320                s += "});\n";
321                var div = document.createElement('div');
322                div.id="dojox.robot.recorder";
323                div.style.backgroundColor = "white";
324                div.style.position = "absolute";
325                var scroll = {y: (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0),
326                x: (window.pageXOffset || (window["dojo"]?dojo._fixIeBiDiScrollLeft(document.documentElement.scrollLeft):undefined) || document.body.scrollLeft || 0)};
327                div.style.left = scroll.x+"px";
328                div.style.top = scroll.y+"px";
329                var h1 = document.createElement('h1');
330                h1.innerHTML = "Your code:";
331                div.appendChild(h1);
332                var pre = document.createElement('pre');
333                if(pre.innerText !== undefined){
334                        pre.innerText = s;
335                }else{
336                        pre.textContent = s;
337                }
338                div.appendChild(pre);
339                var button = document.createElement('button');
340                button.innerHTML = "Close";
341                var connect = dojo.connect(button,'onmouseup',function(e){
342                        dojo.stopEvent(e);
343                        document.body.removeChild(div);
344                        dojo.disconnect(connect);
345                });
346                div.appendChild(button);
347                document.body.appendChild(div);
348                commands = [];
349        }
350}
351
352var getSelector = function(node){
353        // Selenium/Windmill recorders have a concept of a "selector."
354        // The idea is that recorders need some reference to an element that persists even after a page refresh.
355        // For elements with ids, this is easy; just use the id.
356        // For other elements, we have to be more sly.
357
358        if(typeof node =="string"){
359                // it's already an id to be interpreted by dojo.byId
360                return "'" + node+"'";
361        }else if(node.id){
362                // it has an id
363                return "'" + node.id+"'";
364        }else{
365                // TODO: need a generic selector, like CSS3/dojo.query, for the default return value
366                // for now, just do getElementsByTagName
367                var nodes = document.getElementsByTagName(node.nodeName);
368                var i;
369                for(i = 0; i<nodes.length; i++){
370                        if(nodes[i] == node){
371                                break;
372                        }
373                }
374                // wrap in a function to defer evaluation
375                return "function(){ return document.getElementsByTagName('" + node.nodeName+"')["+i+"]; }";
376        }
377}
378
379var getMouseButtonObject = function(b){
380        // convert native event to doh.robot API
381        return "{left:" + (b==0)+", middle:" + (b==1)+", right:" + (b==2)+"}";
382}
383
384
385var getModifierObject = function(e){
386        // convert native event to doh.robot API
387        return "{'shift':" + (e.shiftKey)+", 'ctrl':" + (e.ctrlKey)+", 'alt':" + (e.altKey)+"}";
388}
389
390// dojo.connects
391
392dojo.connect(document,"onkeydown",function(e){
393        // the CTRL- ALT-ENTER hotkey to activate the recorder
394        //console.log(e.keyCode + ", " + e.ctrlKey+", " + e.altKey);
395        if((e.keyCode == dojo.keys.ENTER || e.keyCode==77) && e.ctrlKey && e.altKey){
396                dojo.stopEvent(e);
397                toggle();
398        }
399});
400
401var lastEvent = {type:""};
402
403var onmousedown = function(e){
404        // handler for mouse down
405        if(!e || lastEvent.type==e.type && lastEvent.button==e.button){ return; }
406        lastEvent={type:e.type, button:e.button};
407        var selector = getSelector(e.target);
408        var coords = dojo.coords(e.target);
409        addCommand("doh.robot.mouseMoveAt",[selector, 0, 100, e.clientX - coords.x, e.clientY-coords.y]);
410        addCommand("doh.robot.mousePress",[getMouseButtonObject(e.button-(dojo.isIE?1:0)), 0]);
411};
412
413var onclick = function(e){
414        // handler for mouse up
415        if(!e || lastEvent.type==e.type && lastEvent.button==e.button){ return; }
416        lastEvent={type:e.type, button:e.button};
417        var selector = getSelector(e.target);
418        var coords = dojo.coords(e.target);
419        addCommand("doh.robot.mouseClick",[getMouseButtonObject(e.button-(dojo.isIE?1:0)), 0]);
420};
421
422var onmouseup = function(e){
423        // handler for mouse up
424        if(!e || lastEvent.type==e.type && lastEvent.button==e.button){ return; }
425        lastEvent={type:e.type, button:e.button};
426        var selector = getSelector(e.target);
427        var coords = dojo.coords(e.target);
428        addCommand("doh.robot.mouseRelease",[getMouseButtonObject(e.button-(dojo.isIE?1:0)), 0]);
429};
430
431var onmousemove = function(e){
432        // handler for mouse move
433        if(!e || lastEvent.type==e.type && lastEvent.pageX==e.pageX && lastEvent.pageY==e.pageY){ return; }
434        lastEvent={type:e.type, pageX:e.pageX, pageY:e.pageY};
435        addCommand("doh.robot.mouseMove",[e.pageX, e.pageY, 0, 100, true]);
436};
437
438var onmousewheel = function(e){
439        // handler for mouse move
440        if(!e || lastEvent.type==e.type && lastEvent.pageX==e.pageX && lastEvent.pageY==e.pageY){ return; }
441        lastEvent={type:e.type, detail:(e.detail ? (e.detail) : (-e.wheelDelta / 120))};
442        addCommand("doh.robot.mouseWheel",[lastEvent.detail]);
443};
444
445var onkeypress = function(e){
446        // handler for key press
447        if(!e || lastEvent.type==e.type && (lastEvent.charCode == e.charCode && lastEvent.keyCode == e.keyCode)){ return; }
448        lastEvent={type:e.type, charCode:e.charCode, keyCode:e.keyCode};
449        addCommand("doh.robot.keyPress",[e.charOrCode==dojo.keys.SPACE?' ':e.charOrCode, 0, getModifierObject(e)]);
450};
451
452var onkeyup = function(e){
453        if(!e || lastEvent.type==e.type && (lastEvent.charCode == e.charCode && lastEvent.keyCode == e.keyCode)){ return; }
454        lastEvent={type:e.type, charCode:e.charCode, keyCode:e.keyCode};
455}
456
457// trap all native elements' events
458dojo.connect(document,"onmousedown",onmousedown);
459dojo.connect(document,"onmouseup",onmouseup);
460dojo.connect(document,"onclick",onclick);
461dojo.connect(document,"onkeypress",onkeypress);
462dojo.connect(document,"onkeyup",onkeyup);
463dojo.connect(document,"onmousemove",onmousemove);
464dojo.connect(document,!dojo.isMozilla ? "onmousewheel" : 'DOMMouseScroll',onmousewheel);
465
466dojo.addOnLoad(function(){
467        // get scrollIntoView for good measure
468        // catch: dojo.window might not be loaded (yet?) so addonload
469        if(dojo.window){
470                dojo.connect(dojo.window,"scrollIntoView",function(node){
471                        addCommand("doh.robot.scrollIntoView",[getSelector(node)]);
472                });
473        }
474});
475
476// Get Dojo widget events too!
477dojo.connect(dojo, "connect",
478        function(/*dijit._Widget*/ widget, /*String*/ event, /*Function*/ f){
479                // kill recursion
480                // check for private variable _mine to make sure this isn't a recursive loop
481                if(widget && (!f || !f._mine)){
482                        var hitchedf = null;
483                        if(event.toLowerCase() == "onmousedown"){
484                                hitchedf = dojo.hitch(this,onmousedown);
485                        }else if(event.toLowerCase() == (!dojo.isMozilla ? "onmousewheel" : 'dommousescroll')){
486                                hitchedf = dojo.hitch(this,onmousewheel);
487                        }else if(event.toLowerCase() == "onclick"){
488                                hitchedf = dojo.hitch(this,onclick);
489                        }else if(event.toLowerCase() == "onmouseup"){
490                                hitchedf = dojo.hitch(this,onmouseup);
491                        }else if(event.toLowerCase() == "onkeypress"){
492                                hitchedf = dojo.hitch(this,onkeypress);
493                        }else if(event.toLowerCase() == "onkeyup"){
494                                hitchedf = dojo.hitch(this,onkeyup);
495                        }
496                        if(hitchedf == null){ return; }
497                        hitchedf._mine = true;
498                        dojo.connect(widget,event,hitchedf);
499                }
500        });
501})();
Note: See TracBrowser for help on using the repository browser.