├── .gitattributes ├── .gitignore ├── README.md ├── gulp ├── index.js ├── tasks │ ├── browserify-css-selector.js │ ├── browserify-ext-selector.js │ ├── browserify-ext.js │ ├── browserify.js │ ├── build.js │ ├── default.js │ └── http.js └── util │ └── scriptFilter.js ├── gulpfile.js ├── package.json ├── src ├── app-css-selector.js ├── app-ext-selector.js ├── app-ext.js ├── app.js ├── code-generator-css.js ├── code-generator-ext.js ├── css-selector-factory.js ├── custom-event.js ├── dom.js ├── event-coding-map.js ├── event-coordinates.js ├── events-to-record.js ├── ext-component-query-factory.js ├── object-generator-ext-selector.js └── recorder.js ├── test ├── frame.html ├── index.html └── ui-recorder.js ├── ui-recorder-css-selector.js ├── ui-recorder-ext-selector.js ├── ui-recorder-ext.js └── ui-recorder.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | eggs 141 | parts 142 | bin 143 | var 144 | sdist 145 | develop-eggs 146 | .installed.cfg 147 | 148 | # Installer logs 149 | pip-log.txt 150 | 151 | # Unit test / coverage reports 152 | .coverage 153 | .tox 154 | 155 | #Translations 156 | *.mo 157 | 158 | #Mr Developer 159 | .mr.developer.cfg 160 | 161 | # Mac crap 162 | .DS_Store 163 | 164 | # Node.js 165 | node_modules/ 166 | 167 | # Webstorm 168 | .idea/ 169 | 170 | # Packaging 171 | app-built/ 172 | built/ 173 | build/ 174 | 175 | # Angular JS 176 | **/bower_components/ 177 | 178 | ## Test Server database files 179 | db-files/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI Recorder 2 | 3 | This repository contains the source code of a browser event recorder, which supports capturing all user interaction in a browser and generating code for the captured interactions. It's simple and flexible, and it can easily be cusotmized to capture any browser events and to generate any kind of test automation code. It aims to become a generic browser event recorder that can be used to develop any kind of UI recorders such as [Selenium IDE](http://docs.seleniumhq.org/projects/ide/). 4 | 5 | ## Overview 6 | 7 | The basic concept of the UI Recorder is extremely simple. It adds event listeners to the browser's window and frames so that it can capture the specific events and then generate code based on the events. 8 | 9 | ## Demo 10 | 11 | Click on [this link](http://yguan.github.io/repos/ui-recorder/) to go to the test page, open the browser console, click somewhere on the test page, and you should see the recorded code shown in the console. You can run `recorder.getRecordedCode()` to get the recorded code. 12 | 13 | ## Development 14 | 15 | #### Overview of Folder Structure 16 | 17 | * `src` contains the pre-build files of the UI Recorder. 18 | * `test` contains the files for testing the UI recorder. 19 | * `gulp` contains the gulp task files. 20 | 21 | #### Anatomy of the UI Recorder 22 | 23 | Here is the overview for the files under `src` folder: 24 | 25 | * `app.js` is where the UI Recorder is initialized, and it is the entry point for gulp to build ui-recorder.js. 26 | * `recorder.js` is the core of the UI Recorder. It provides interface to interact with the recorder and glues all the specific components, such as code generation. 27 | * `elements-to-listen.js` specifies what elements to add listeners to. 28 | * `events-to-record.js` specifies what events to listen. 29 | * `code-generator.js` specifies how code is generated for an event. 30 | * `event-coding-map.js` specifies what function name is used for an event for code generation. 31 | 32 | #### Set up The Local Environment 33 | 34 | Here are the steps: 35 | 36 | * Install `gulp` globally if you haven't done so. 37 | * Run `npm install`. 38 | * Run `gulp` to build the `ui-recorder.js`. 39 | * Run `gulp http` to start a http server at port `9000`. 40 | * Point your browser to `localhost:9000/index.html`. 41 | * Open browser's console. 42 | * Click some elements on the page and you should see the output from the recorder, and you can debug `ui-recorder.js`. 43 | 44 | ## Usage 45 | 46 | For now, you have to manually copy and paste to get the recorder running, and here are the steps: 47 | 48 | Here are the steps; 49 | 50 | * Open a browser and go to the site that you want to record user interactions. 51 | * Open the browser's console (F12). 52 | * Copy the code from [ui-recorder.js](https://raw.githubusercontent.com/yguan/ui-recorder/master/ui-recorder.js), paste it to the console, and run it. 53 | * To start the recorder, run `recorder.record()` from the console. 54 | * To stop the recorder, run `recorder.stop()` from the console. 55 | * To start the recorder again, run `recorder.record()`. 56 | * To get the recorded code, run `recorder.getRecordedCode()`. 57 | * To clear the recorded code, run `recorder.clearRecordedCode()`. 58 | 59 | ## Todo 60 | 61 | Here is a todo list to make UI Recorder better, and everyone is welcome to contribute to it: 62 | 63 | * Create a Chrome extension to bypass the manual steps to run the recorder. 64 | * Figure out a plugin architecture to make code generation easier to integrate. 65 | 66 | ## License 67 | 68 | [MIT](http://opensource.org/licenses/MIT) -------------------------------------------------------------------------------- /gulp/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var onlyScripts = require('./util/scriptFilter'); 3 | var tasks = fs.readdirSync('./gulp/tasks/').filter(onlyScripts); 4 | 5 | tasks.forEach(function(task) { 6 | require('./tasks/' + task); 7 | }); -------------------------------------------------------------------------------- /gulp/tasks/browserify-css-selector.js: -------------------------------------------------------------------------------- 1 | /* browserify task 2 | --------------- 3 | Bundle javascripty things with browserify! 4 | 5 | If the watch task is running, this uses watchify instead 6 | of browserify for faster bundling using caching. 7 | */ 8 | 9 | var browserify = require('browserify'); 10 | var gulp = require('gulp'); 11 | var source = require('vinyl-source-stream'); 12 | 13 | gulp.task('browserify-css-selector', function() { 14 | return browserify('./src/app-css-selector.js') 15 | .bundle() 16 | //Pass desired output filename to vinyl-source-stream 17 | .pipe(source('ui-recorder-css-selector.js')) 18 | // Start piping stream to tasks! 19 | .pipe(gulp.dest('.')); 20 | }); 21 | -------------------------------------------------------------------------------- /gulp/tasks/browserify-ext-selector.js: -------------------------------------------------------------------------------- 1 | /* browserify task 2 | --------------- 3 | Bundle javascripty things with browserify! 4 | 5 | If the watch task is running, this uses watchify instead 6 | of browserify for faster bundling using caching. 7 | */ 8 | 9 | var browserify = require('browserify'); 10 | var gulp = require('gulp'); 11 | var source = require('vinyl-source-stream'); 12 | 13 | gulp.task('browserify-ext-selector', function() { 14 | return browserify('./src/app-ext-selector.js') 15 | .bundle() 16 | //Pass desired output filename to vinyl-source-stream 17 | .pipe(source('ui-recorder-ext-selector.js')) 18 | // Start piping stream to tasks! 19 | .pipe(gulp.dest('.')); 20 | }); 21 | -------------------------------------------------------------------------------- /gulp/tasks/browserify-ext.js: -------------------------------------------------------------------------------- 1 | /* browserify task 2 | --------------- 3 | Bundle javascripty things with browserify! 4 | 5 | If the watch task is running, this uses watchify instead 6 | of browserify for faster bundling using caching. 7 | */ 8 | 9 | var browserify = require('browserify'); 10 | var gulp = require('gulp'); 11 | var source = require('vinyl-source-stream'); 12 | 13 | gulp.task('browserify-ext', function() { 14 | return browserify('./src/app-ext.js') 15 | .bundle() 16 | //Pass desired output filename to vinyl-source-stream 17 | .pipe(source('ui-recorder-ext.js')) 18 | // Start piping stream to tasks! 19 | .pipe(gulp.dest('.')); 20 | }); 21 | -------------------------------------------------------------------------------- /gulp/tasks/browserify.js: -------------------------------------------------------------------------------- 1 | /* browserify task 2 | --------------- 3 | Bundle javascripty things with browserify! 4 | 5 | If the watch task is running, this uses watchify instead 6 | of browserify for faster bundling using caching. 7 | */ 8 | 9 | var browserify = require('browserify'); 10 | var gulp = require('gulp'); 11 | var source = require('vinyl-source-stream'); 12 | 13 | gulp.task('browserify', function() { 14 | return browserify('./src/app.js') 15 | .bundle() 16 | //Pass desired output filename to vinyl-source-stream 17 | .pipe(source('ui-recorder.js')) 18 | // Start piping stream to tasks! 19 | .pipe(gulp.dest('.')) 20 | .pipe(gulp.dest('./test/')); 21 | }); 22 | -------------------------------------------------------------------------------- /gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | gulp.task('build', ['browserify', 'browserify-ext', 'browserify-ext-selector', 'browserify-css-selector']); 4 | -------------------------------------------------------------------------------- /gulp/tasks/default.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | gulp.task('default', ['build']); 4 | -------------------------------------------------------------------------------- /gulp/tasks/http.js: -------------------------------------------------------------------------------- 1 | /* browserify task 2 | --------------- 3 | Bundle javascripty things with browserify! 4 | 5 | If the watch task is running, this uses watchify instead 6 | of browserify for faster bundling using caching. 7 | */ 8 | 9 | var gulp = require('gulp'); 10 | var http = require('http'); 11 | var connect = require('connect'); 12 | 13 | gulp.task('http', function(){ 14 | 15 | var app = connect().use(connect.static('./test')); 16 | http.createServer(app).listen(9000); 17 | }); -------------------------------------------------------------------------------- /gulp/util/scriptFilter.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | // Filters out non .coffee and .js files. Prevents 4 | // accidental inclusion of possible hidden files 5 | module.exports = function(name) { 6 | return /(\.(js|coffee)$)/i.test(path.extname(name)); 7 | }; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // copy from https://github.com/greypants/gulp-starter 2 | // and here is the introduction http://viget.com/extend/gulp-browserify-starter-faq 3 | /* 4 | gulpfile.js 5 | =========== 6 | Rather than manage one giant configuration file responsible 7 | for creating multiple tasks, each task has been broken out into 8 | its own file in gulp/tasks. Any file in that folder gets automatically 9 | required by the loop in ./gulp/index.js (required below). 10 | 11 | To add a new task, simply add a new task file to gulp/tasks. 12 | */ 13 | 14 | require('./gulp'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-recorder", 3 | "version": "0.0.1", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/yguan/ui-recorder.git" 7 | }, 8 | "description": "Simple library for recording browser events", 9 | "author": "Yong Guan", 10 | "dependencies": { 11 | "gulp": "~3.8.0", 12 | "browserify": "~3.36.0", 13 | "vinyl-source-stream": "~0.1.1", 14 | "connect": "2.19.6" 15 | }, 16 | "devDependencies": { 17 | }, 18 | "engines": { 19 | "node": ">=0.10.0" 20 | }, 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /src/app-css-selector.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var recorder = require('./recorder'); 5 | var eventsToRecord = require('./events-to-record'); 6 | var cssSelectorFactory = require('./css-selector-factory'); 7 | var eventCoordinators = require('./event-coordinates'); 8 | 9 | function generateCode(evt) { 10 | var cssSelector = cssSelectorFactory.getSelector(evt.target), 11 | coordinates = eventCoordinators.getClientCoordinates(evt); 12 | return evt.type + ' \'' + cssSelector + '\' ' + JSON.stringify(coordinates); 13 | } 14 | 15 | recorder.init({ 16 | generateCode: generateCode, 17 | eventsToRecord: eventsToRecord 18 | }); 19 | window.recorderCss = recorder; 20 | module.exports = recorder; -------------------------------------------------------------------------------- /src/app-ext-selector.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var recorder = require('./recorder'); 5 | var eventsToRecord = require('./events-to-record'); 6 | var codeGenerator = require('./object-generator-ext-selector'); 7 | 8 | recorder.init({ 9 | generateObject: codeGenerator.generateObject, 10 | eventsToRecord: eventsToRecord 11 | }); 12 | window.recorderES = recorder; 13 | module.exports = recorder; -------------------------------------------------------------------------------- /src/app-ext.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var recorder = require('./recorder'); 5 | var eventsToRecord = require('./events-to-record'); 6 | var codeGenerator = require('./code-generator-ext'); 7 | 8 | recorder.init({ 9 | generateCode: codeGenerator.generateCode, 10 | eventsToRecord: eventsToRecord 11 | }); 12 | window.recorderExt = recorder; 13 | module.exports = recorder; -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var recorder = require('./recorder'); 5 | var eventsToRecord = require('./events-to-record'); 6 | var codeGenerator = require('./code-generator-css'); 7 | 8 | recorder.init({ 9 | generateCode: codeGenerator.generateCode, 10 | eventsToRecord: eventsToRecord 11 | }); 12 | window.recorder = recorder; 13 | module.exports = recorder; -------------------------------------------------------------------------------- /src/code-generator-css.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var eventCodingMap = require('./event-coding-map'), 5 | cssSelectorFactory = require('./css-selector-factory'), 6 | eventCoordinators = require('./event-coordinates'); 7 | 8 | function generateCode(evt) { 9 | var cssSelector = cssSelectorFactory.getSelector(evt.target), 10 | code = eventCodingMap.getEventCode(evt), 11 | coordinates = eventCoordinators.getClientCoordinates(evt); 12 | 13 | if (code) { 14 | return code + '(\'' + cssSelector + '\', ' + JSON.stringify(coordinates) + ')'; 15 | } 16 | 17 | return evt.type + ' \'' + cssSelector + '\' ' + JSON.stringify(coordinates); 18 | } 19 | 20 | module.exports = { 21 | generateCode: generateCode 22 | }; -------------------------------------------------------------------------------- /src/code-generator-ext.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var eventCodingMap = require('./event-coding-map'), 5 | extComponentQueryFactory = require('./ext-component-query-factory'); 6 | 7 | function generateCode(evt) { 8 | var query = JSON.stringify(extComponentQueryFactory.getQuery(evt.target)), 9 | code = eventCodingMap.getEventCode(evt); 10 | 11 | if (code) { 12 | return code + '(\'' + query + '\')'; 13 | } 14 | 15 | return evt.type + ' \'' + query + '\''; 16 | } 17 | 18 | module.exports = { 19 | generateCode: generateCode 20 | }; -------------------------------------------------------------------------------- /src/css-selector-factory.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var dom = require('./dom'); 5 | 6 | function getIdOrCls(el) { 7 | if (el.id) { 8 | return '#' + el.id; 9 | } else if (el.classList && el.classList.length > 0) { 10 | return '.' + el.className.split(' ').join('.'); 11 | } 12 | return ''; 13 | } 14 | 15 | function getCssSelector(el) { 16 | var selectorList = ['', ''], 17 | selector, 18 | parentEl; 19 | 20 | selectorList[1] = getIdOrCls(el); 21 | 22 | if (el.id) { 23 | return selectorList[1]; 24 | } 25 | 26 | if (selectorList[1].length === 0) { 27 | selector = el.nodeName; 28 | 29 | if (selector === 'A') { 30 | selector += ':contains(' + el.textContent + ')' 31 | } 32 | selectorList[1] = selector; 33 | } 34 | 35 | parentEl = dom.up(el, function (element) { 36 | return getIdOrCls(element).length > 0; 37 | }); 38 | 39 | selectorList[0] = getIdOrCls(parentEl); 40 | 41 | return selectorList.join(' ').trim(); 42 | } 43 | 44 | module.exports = { 45 | getSelector: getCssSelector 46 | }; -------------------------------------------------------------------------------- /src/custom-event.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | function isEnterText(evt) { 5 | var element = evt.target; 6 | return (element.type === 'text' || element.type === 'textarea') && evt.type === 'keyup'; 7 | } 8 | 9 | function getCustomEventType(evt) { 10 | if (isEnterText(evt)) { 11 | return 'enterText'; 12 | } 13 | } 14 | 15 | module.exports = { 16 | getType: getCustomEventType 17 | }; -------------------------------------------------------------------------------- /src/dom.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | function up(el, stopCondition) { 5 | var target = el; 6 | 7 | while (target.parentNode) { 8 | target = target.parentNode; 9 | if (stopCondition(target)) { 10 | break; 11 | } 12 | } 13 | return target; 14 | } 15 | 16 | module.exports = { 17 | up: up 18 | }; -------------------------------------------------------------------------------- /src/event-coding-map.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var customEvent = require('./custom-event'), 5 | codingMap = { 6 | click: '.waitAndClick', 7 | enterText: '.typeValue' // this is a non-existing event to represent type in values to a textbox or textarea 8 | }; 9 | 10 | function getEventCode(evt) { 11 | var code = codingMap[evt.type]; 12 | 13 | if (code) { 14 | return code; 15 | } 16 | 17 | // handle non-existing events 18 | return codingMap[customEvent.getType(evt)]; 19 | } 20 | 21 | module.exports = { 22 | getEventCode: getEventCode 23 | }; -------------------------------------------------------------------------------- /src/event-coordinates.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var eventsWithCoordinates = { 5 | mouseup: true, 6 | mousedown: true, 7 | mousemove: true, 8 | mouseover: true 9 | }; 10 | 11 | function getClientCoordinates(evt) { 12 | if (!eventsWithCoordinates[evt.type]) { 13 | return ''; 14 | } 15 | 16 | return { 17 | x: evt.clientX, 18 | y: evt.clientY 19 | }; 20 | } 21 | 22 | module.exports = { 23 | getClientCoordinates: getClientCoordinates 24 | }; -------------------------------------------------------------------------------- /src/events-to-record.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | module.exports = [ 5 | 'click', 6 | // 'focus', 7 | // 'blur', 8 | 'dblclick', 9 | 'change', 10 | 'keyup', 11 | // 'keydown', 12 | // 'keypress', 13 | 'mousedown', 14 | // 'mousemove', 15 | // 'mouseout', 16 | // 'mouseover', 17 | // 'mouseup', 18 | 'resize', 19 | // 'scroll', 20 | 'select', 21 | 'submit', 22 | 'load', 23 | 'unload' 24 | ]; 25 | 26 | //var events = [ 27 | // abort, 28 | // afterprint, 29 | // beforeprint, 30 | // beforeunload, 31 | // blur, 32 | // canplay, 33 | // canplaythrough, 34 | // change, 35 | // click, 36 | // contextmenu, 37 | // copy, 38 | // cuechange, 39 | // cut, 40 | // dblclick, 41 | // DOMContentLoaded, 42 | // drag, 43 | // dragend, 44 | // dragenter, 45 | // dragleave, 46 | // dragover, 47 | // dragstart, 48 | // drop, 49 | // durationchange, 50 | // emptied, 51 | // ended, 52 | // error, 53 | // focus, 54 | // focusin, 55 | // focusout, 56 | // formchange, 57 | // forminput, 58 | // hashchange, 59 | // input, 60 | // invalid, 61 | // keydown, 62 | // keypress, 63 | // keyup, 64 | // load, 65 | // loadeddata, 66 | // loadedmetadata, 67 | // loadstart, 68 | // message, 69 | // mousedown, 70 | // mouseenter, 71 | // mouseleave, 72 | // mousemove, 73 | // mouseout, 74 | // mouseover, 75 | // mouseup, 76 | // mousewheel, 77 | // offline, 78 | // online, 79 | // pagehide, 80 | // pageshow, 81 | // paste, 82 | // pause, 83 | // play, 84 | // playing, 85 | // popstate, 86 | // progress, 87 | // ratechange, 88 | // readystatechange, 89 | // redo, 90 | // reset, 91 | // resize, 92 | // scroll, 93 | // seeked, 94 | // seeking, 95 | // select, 96 | // show, 97 | // stalled, 98 | // storage, 99 | // submit, 100 | // suspend, 101 | // timeupdate, 102 | // undo, 103 | // unload, 104 | // volumechange, 105 | // waiting 106 | //]; -------------------------------------------------------------------------------- /src/ext-component-query-factory.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module,Ext */ 3 | 4 | function getComponent(el) { 5 | var cmp, 6 | target = el; 7 | 8 | while (target) { 9 | cmp = Ext.getCmp(target.id); 10 | 11 | if (cmp) { 12 | return cmp; 13 | } 14 | 15 | target = target.parentNode; 16 | } 17 | 18 | return null; 19 | } 20 | 21 | function getQuery(el) { 22 | var cmp = getComponent(el), 23 | query; 24 | 25 | if (!cmp) { 26 | return 'No component query available.'; 27 | } 28 | 29 | // Depend on the ExtJS app, either itemId or cls may be the right one to use. 30 | // Use cmp.cls with cmp.getXType() to create 'someXtype[cls=someClass]' 31 | query = { 32 | itemId: cmp.getItemId(), 33 | cls: cmp.cls, 34 | xtype: cmp.getXType(), 35 | el: { 36 | name: el.nodeName, 37 | className: el.className 38 | } 39 | }; 40 | 41 | return query; 42 | } 43 | 44 | module.exports = { 45 | getQuery: getQuery 46 | }; -------------------------------------------------------------------------------- /src/object-generator-ext-selector.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var extComponentQueryFactory = require('./ext-component-query-factory'); 5 | 6 | function generateObject(evt) { 7 | var query = extComponentQueryFactory.getQuery(evt.target); 8 | query.event = evt.type; 9 | 10 | return query; 11 | } 12 | 13 | module.exports = { 14 | generateObject: generateObject 15 | }; -------------------------------------------------------------------------------- /src/recorder.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | /*global $,define,require,module */ 3 | 4 | var recordedCode = '', 5 | generateCode, 6 | generateObject, 7 | eventsToRecord, 8 | windowToListen; 9 | 10 | function init(config) { 11 | generateCode = config.generateCode; 12 | generateObject = config.generateObject; 13 | eventsToRecord = config.eventsToRecord; 14 | } 15 | 16 | function setWindowToListen(windowElement) { 17 | windowToListen = windowElement; 18 | } 19 | 20 | // Each frame is a window 21 | function getAllFrames(windowElement, allFrames) { 22 | allFrames.push(windowElement.frames); 23 | for (var i = 0; i < windowElement.frames.length; i++) { 24 | getAllFrames(windowElement.frames[i], allFrames); 25 | } 26 | return allFrames; 27 | } 28 | 29 | function getElementsToListen(windowElement) { 30 | return getAllFrames(windowElement, []); 31 | } 32 | 33 | function bind(el, eventType, handler) { 34 | if (el.addEventListener) { // DOM Level 2 browsers 35 | el.addEventListener(eventType, handler, false); 36 | } else if (el.attachEvent) { // IE <= 8 37 | el.attachEvent('on' + eventType, handler); 38 | } else { // ancient browsers 39 | el['on' + eventType] = handler; 40 | } 41 | } 42 | 43 | function unbind(el, eventType, handler) { 44 | if (el.removeEventListener) { 45 | el.removeEventListener(eventType, handler, false); 46 | } else if (el.detachEvent) { 47 | el.detachEvent("on" + eventType, handler); 48 | } else { 49 | el["on" + eventType] = null; 50 | } 51 | } 52 | 53 | function manageSingleElementEvents(element, action, events, handler) { 54 | var eventIndex = 0, 55 | eventCount = events.length; 56 | 57 | for (; eventIndex < eventCount; eventIndex++) { 58 | action(element, events[eventIndex], handler); 59 | } 60 | } 61 | 62 | function manageEvents(elements, action, events, handler) { 63 | var elementIndex = 0, 64 | elementCount = elements.length; 65 | 66 | for (; elementIndex < elementCount; elementIndex++) { 67 | // Have to attach events with some delay between iframes. Otherwise, iframe events are not captured 68 | setTimeout(manageSingleElementEvents, 50, elements[elementIndex], action, events, handler); 69 | } 70 | } 71 | 72 | function recordEvent(e) { 73 | var code; 74 | 75 | if (generateObject) { 76 | console.recorderLog(JSON.stringify(generateObject(e), true, 2)); 77 | } 78 | if (generateCode) { 79 | code = generateCode(e); 80 | 81 | recordedCode = recordedCode + code + '\n'; 82 | console.recorderLog(code); 83 | } 84 | } 85 | 86 | function record() { 87 | var elementsToListen = getElementsToListen(windowToListen || window); 88 | console.recorderLog = console.log; // hijack the console.log so that only recorded code will be shown 89 | console.log = function () {}; 90 | manageEvents(elementsToListen, bind, eventsToRecord, recordEvent); 91 | } 92 | 93 | function stop() { 94 | var elementsToListen = getElementsToListen(windowToListen || window); 95 | if (console.recorderLog) { 96 | console.log = console.recorderLog; 97 | } 98 | manageEvents(elementsToListen, unbind, eventsToRecord, recordEvent); 99 | } 100 | 101 | function getRecordedCode() { 102 | return recordedCode; 103 | } 104 | 105 | function clearRecordedCode() { 106 | return recordedCode = ''; 107 | } 108 | 109 | module.exports = { 110 | init: init, 111 | setWindowToListen: setWindowToListen, 112 | record: record, 113 | stop: stop, 114 | getRecordedCode: getRecordedCode, 115 | clearRecordedCode: clearRecordedCode 116 | }; 117 | -------------------------------------------------------------------------------- /test/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Iframe 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UI Recorder 6 | 7 | 8 |
9 | link 10 | 11 | label 12 | 13 |
14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/ui-recorder.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { 48 | return '.' + el.className.split(' ').join('.'); 49 | } 50 | return ''; 51 | } 52 | 53 | function getCssSelector(el) { 54 | var selectorList = ['', ''], 55 | selector, 56 | parentEl; 57 | 58 | selectorList[1] = getIdOrCls(el); 59 | 60 | if (el.id) { 61 | return selectorList[1]; 62 | } 63 | 64 | if (selectorList[1].length === 0) { 65 | selector = el.nodeName; 66 | 67 | if (selector === 'A') { 68 | selector += ':contains(' + el.textContent + ')' 69 | } 70 | selectorList[1] = selector; 71 | } 72 | 73 | parentEl = dom.up(el, function (element) { 74 | return getIdOrCls(element).length > 0; 75 | }); 76 | 77 | selectorList[0] = getIdOrCls(parentEl); 78 | 79 | return selectorList.join(' ').trim(); 80 | } 81 | 82 | module.exports = { 83 | getSelector: getCssSelector 84 | }; 85 | },{"./dom":5}],4:[function(require,module,exports){ 86 | /*jslint nomen: true*/ 87 | /*global $,define,require,module */ 88 | 89 | function isEnterText(evt) { 90 | var element = evt.target; 91 | return (element.type === 'text' || element.type === 'textarea') && evt.type === 'keyup'; 92 | } 93 | 94 | function getCustomEventType(evt) { 95 | if (isEnterText(evt)) { 96 | return 'enterText'; 97 | } 98 | } 99 | 100 | module.exports = { 101 | getType: getCustomEventType 102 | }; 103 | },{}],5:[function(require,module,exports){ 104 | /*jslint nomen: true*/ 105 | /*global $,define,require,module */ 106 | 107 | function up(el, stopCondition) { 108 | var target = el; 109 | 110 | while (target.parentNode) { 111 | target = target.parentNode; 112 | if (stopCondition(target)) { 113 | break; 114 | } 115 | } 116 | return target; 117 | } 118 | 119 | module.exports = { 120 | up: up 121 | }; 122 | },{}],6:[function(require,module,exports){ 123 | /*jslint nomen: true*/ 124 | /*global $,define,require,module */ 125 | 126 | var customEvent = require('./custom-event'), 127 | codingMap = { 128 | click: '.waitAndClick', 129 | enterText: '.typeValue' // this is a non-existing event to represent type in values to a textbox or textarea 130 | }; 131 | 132 | function getEventCode(evt) { 133 | var code = codingMap[evt.type]; 134 | 135 | if (code) { 136 | return code; 137 | } 138 | 139 | // handle non-existing events 140 | return codingMap[customEvent.getType(evt)]; 141 | } 142 | 143 | module.exports = { 144 | getEventCode: getEventCode 145 | }; 146 | },{"./custom-event":4}],7:[function(require,module,exports){ 147 | /*jslint nomen: true*/ 148 | /*global $,define,require,module */ 149 | 150 | var eventsWithCoordinates = { 151 | mouseup: true, 152 | mousedown: true, 153 | mousemove: true, 154 | mouseover: true 155 | }; 156 | 157 | function getClientCoordinates(evt) { 158 | if (!eventsWithCoordinates[evt.type]) { 159 | return ''; 160 | } 161 | 162 | return { 163 | x: evt.clientX, 164 | y: evt.clientY 165 | }; 166 | } 167 | 168 | module.exports = { 169 | getClientCoordinates: getClientCoordinates 170 | }; 171 | },{}],8:[function(require,module,exports){ 172 | /*jslint nomen: true*/ 173 | /*global $,define,require,module */ 174 | 175 | module.exports = [ 176 | 'click', 177 | // 'focus', 178 | // 'blur', 179 | 'dblclick', 180 | 'change', 181 | 'keyup', 182 | // 'keydown', 183 | // 'keypress', 184 | 'mousedown', 185 | // 'mousemove', 186 | // 'mouseout', 187 | // 'mouseover', 188 | // 'mouseup', 189 | 'resize', 190 | // 'scroll', 191 | 'select', 192 | 'submit', 193 | 'load', 194 | 'unload' 195 | ]; 196 | 197 | //var events = [ 198 | // abort, 199 | // afterprint, 200 | // beforeprint, 201 | // beforeunload, 202 | // blur, 203 | // canplay, 204 | // canplaythrough, 205 | // change, 206 | // click, 207 | // contextmenu, 208 | // copy, 209 | // cuechange, 210 | // cut, 211 | // dblclick, 212 | // DOMContentLoaded, 213 | // drag, 214 | // dragend, 215 | // dragenter, 216 | // dragleave, 217 | // dragover, 218 | // dragstart, 219 | // drop, 220 | // durationchange, 221 | // emptied, 222 | // ended, 223 | // error, 224 | // focus, 225 | // focusin, 226 | // focusout, 227 | // formchange, 228 | // forminput, 229 | // hashchange, 230 | // input, 231 | // invalid, 232 | // keydown, 233 | // keypress, 234 | // keyup, 235 | // load, 236 | // loadeddata, 237 | // loadedmetadata, 238 | // loadstart, 239 | // message, 240 | // mousedown, 241 | // mouseenter, 242 | // mouseleave, 243 | // mousemove, 244 | // mouseout, 245 | // mouseover, 246 | // mouseup, 247 | // mousewheel, 248 | // offline, 249 | // online, 250 | // pagehide, 251 | // pageshow, 252 | // paste, 253 | // pause, 254 | // play, 255 | // playing, 256 | // popstate, 257 | // progress, 258 | // ratechange, 259 | // readystatechange, 260 | // redo, 261 | // reset, 262 | // resize, 263 | // scroll, 264 | // seeked, 265 | // seeking, 266 | // select, 267 | // show, 268 | // stalled, 269 | // storage, 270 | // submit, 271 | // suspend, 272 | // timeupdate, 273 | // undo, 274 | // unload, 275 | // volumechange, 276 | // waiting 277 | //]; 278 | },{}],9:[function(require,module,exports){ 279 | /*jslint nomen: true*/ 280 | /*global $,define,require,module */ 281 | 282 | var recordedCode = '', 283 | generateCode, 284 | generateObject, 285 | eventsToRecord, 286 | windowToListen; 287 | 288 | function init(config) { 289 | generateCode = config.generateCode; 290 | generateObject = config.generateObject; 291 | eventsToRecord = config.eventsToRecord; 292 | } 293 | 294 | function setWindowToListen(windowElement) { 295 | windowToListen = windowElement; 296 | } 297 | 298 | // Each frame is a window 299 | function getAllFrames(windowElement, allFrames) { 300 | allFrames.push(windowElement.frames); 301 | for (var i = 0; i < windowElement.frames.length; i++) { 302 | getAllFrames(windowElement.frames[i], allFrames); 303 | } 304 | return allFrames; 305 | } 306 | 307 | function getElementsToListen(windowElement) { 308 | return getAllFrames(windowElement, []); 309 | } 310 | 311 | function bind(el, eventType, handler) { 312 | if (el.addEventListener) { // DOM Level 2 browsers 313 | el.addEventListener(eventType, handler, false); 314 | } else if (el.attachEvent) { // IE <= 8 315 | el.attachEvent('on' + eventType, handler); 316 | } else { // ancient browsers 317 | el['on' + eventType] = handler; 318 | } 319 | } 320 | 321 | function unbind(el, eventType, handler) { 322 | if (el.removeEventListener) { 323 | el.removeEventListener(eventType, handler, false); 324 | } else if (el.detachEvent) { 325 | el.detachEvent("on" + eventType, handler); 326 | } else { 327 | el["on" + eventType] = null; 328 | } 329 | } 330 | 331 | function manageSingleElementEvents(element, action, events, handler) { 332 | var eventIndex = 0, 333 | eventCount = events.length; 334 | 335 | for (; eventIndex < eventCount; eventIndex++) { 336 | action(element, events[eventIndex], handler); 337 | } 338 | } 339 | 340 | function manageEvents(elements, action, events, handler) { 341 | var elementIndex = 0, 342 | elementCount = elements.length; 343 | 344 | for (; elementIndex < elementCount; elementIndex++) { 345 | // Have to attach events with some delay between iframes. Otherwise, iframe events are not captured 346 | setTimeout(manageSingleElementEvents, 50, elements[elementIndex], action, events, handler); 347 | } 348 | } 349 | 350 | function recordEvent(e) { 351 | var code; 352 | 353 | if (generateObject) { 354 | console.recorderLog(JSON.stringify(generateObject(e), true, 2)); 355 | } 356 | if (generateCode) { 357 | code = generateCode(e); 358 | 359 | recordedCode = recordedCode + code + '\n'; 360 | console.recorderLog(code); 361 | } 362 | } 363 | 364 | function record() { 365 | var elementsToListen = getElementsToListen(windowToListen || window); 366 | console.recorderLog = console.log; // hijack the console.log so that only recorded code will be shown 367 | console.log = function () {}; 368 | manageEvents(elementsToListen, bind, eventsToRecord, recordEvent); 369 | } 370 | 371 | function stop() { 372 | var elementsToListen = getElementsToListen(windowToListen || window); 373 | if (console.recorderLog) { 374 | console.log = console.recorderLog; 375 | } 376 | manageEvents(elementsToListen, unbind, eventsToRecord, recordEvent); 377 | } 378 | 379 | function getRecordedCode() { 380 | return recordedCode; 381 | } 382 | 383 | function clearRecordedCode() { 384 | return recordedCode = ''; 385 | } 386 | 387 | module.exports = { 388 | init: init, 389 | setWindowToListen: setWindowToListen, 390 | record: record, 391 | stop: stop, 392 | getRecordedCode: getRecordedCode, 393 | clearRecordedCode: clearRecordedCode 394 | }; 395 | 396 | },{}]},{},[1]) -------------------------------------------------------------------------------- /ui-recorder-css-selector.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { 32 | return '.' + el.className.split(' ').join('.'); 33 | } 34 | return ''; 35 | } 36 | 37 | function getCssSelector(el) { 38 | var selectorList = ['', ''], 39 | selector, 40 | parentEl; 41 | 42 | selectorList[1] = getIdOrCls(el); 43 | 44 | if (el.id) { 45 | return selectorList[1]; 46 | } 47 | 48 | if (selectorList[1].length === 0) { 49 | selector = el.nodeName; 50 | 51 | if (selector === 'A') { 52 | selector += ':contains(' + el.textContent + ')' 53 | } 54 | selectorList[1] = selector; 55 | } 56 | 57 | parentEl = dom.up(el, function (element) { 58 | return getIdOrCls(element).length > 0; 59 | }); 60 | 61 | selectorList[0] = getIdOrCls(parentEl); 62 | 63 | return selectorList.join(' ').trim(); 64 | } 65 | 66 | module.exports = { 67 | getSelector: getCssSelector 68 | }; 69 | },{"./dom":3}],3:[function(require,module,exports){ 70 | /*jslint nomen: true*/ 71 | /*global $,define,require,module */ 72 | 73 | function up(el, stopCondition) { 74 | var target = el; 75 | 76 | while (target.parentNode) { 77 | target = target.parentNode; 78 | if (stopCondition(target)) { 79 | break; 80 | } 81 | } 82 | return target; 83 | } 84 | 85 | module.exports = { 86 | up: up 87 | }; 88 | },{}],4:[function(require,module,exports){ 89 | /*jslint nomen: true*/ 90 | /*global $,define,require,module */ 91 | 92 | var eventsWithCoordinates = { 93 | mouseup: true, 94 | mousedown: true, 95 | mousemove: true, 96 | mouseover: true 97 | }; 98 | 99 | function getClientCoordinates(evt) { 100 | if (!eventsWithCoordinates[evt.type]) { 101 | return ''; 102 | } 103 | 104 | return { 105 | x: evt.clientX, 106 | y: evt.clientY 107 | }; 108 | } 109 | 110 | module.exports = { 111 | getClientCoordinates: getClientCoordinates 112 | }; 113 | },{}],5:[function(require,module,exports){ 114 | /*jslint nomen: true*/ 115 | /*global $,define,require,module */ 116 | 117 | module.exports = [ 118 | 'click', 119 | // 'focus', 120 | // 'blur', 121 | 'dblclick', 122 | 'change', 123 | 'keyup', 124 | // 'keydown', 125 | // 'keypress', 126 | 'mousedown', 127 | // 'mousemove', 128 | // 'mouseout', 129 | // 'mouseover', 130 | // 'mouseup', 131 | 'resize', 132 | // 'scroll', 133 | 'select', 134 | 'submit', 135 | 'load', 136 | 'unload' 137 | ]; 138 | 139 | //var events = [ 140 | // abort, 141 | // afterprint, 142 | // beforeprint, 143 | // beforeunload, 144 | // blur, 145 | // canplay, 146 | // canplaythrough, 147 | // change, 148 | // click, 149 | // contextmenu, 150 | // copy, 151 | // cuechange, 152 | // cut, 153 | // dblclick, 154 | // DOMContentLoaded, 155 | // drag, 156 | // dragend, 157 | // dragenter, 158 | // dragleave, 159 | // dragover, 160 | // dragstart, 161 | // drop, 162 | // durationchange, 163 | // emptied, 164 | // ended, 165 | // error, 166 | // focus, 167 | // focusin, 168 | // focusout, 169 | // formchange, 170 | // forminput, 171 | // hashchange, 172 | // input, 173 | // invalid, 174 | // keydown, 175 | // keypress, 176 | // keyup, 177 | // load, 178 | // loadeddata, 179 | // loadedmetadata, 180 | // loadstart, 181 | // message, 182 | // mousedown, 183 | // mouseenter, 184 | // mouseleave, 185 | // mousemove, 186 | // mouseout, 187 | // mouseover, 188 | // mouseup, 189 | // mousewheel, 190 | // offline, 191 | // online, 192 | // pagehide, 193 | // pageshow, 194 | // paste, 195 | // pause, 196 | // play, 197 | // playing, 198 | // popstate, 199 | // progress, 200 | // ratechange, 201 | // readystatechange, 202 | // redo, 203 | // reset, 204 | // resize, 205 | // scroll, 206 | // seeked, 207 | // seeking, 208 | // select, 209 | // show, 210 | // stalled, 211 | // storage, 212 | // submit, 213 | // suspend, 214 | // timeupdate, 215 | // undo, 216 | // unload, 217 | // volumechange, 218 | // waiting 219 | //]; 220 | },{}],6:[function(require,module,exports){ 221 | /*jslint nomen: true*/ 222 | /*global $,define,require,module */ 223 | 224 | var recordedCode = '', 225 | generateCode, 226 | generateObject, 227 | eventsToRecord, 228 | windowToListen; 229 | 230 | function init(config) { 231 | generateCode = config.generateCode; 232 | generateObject = config.generateObject; 233 | eventsToRecord = config.eventsToRecord; 234 | } 235 | 236 | function setWindowToListen(windowElement) { 237 | windowToListen = windowElement; 238 | } 239 | 240 | // Each frame is a window 241 | function getAllFrames(windowElement, allFrames) { 242 | allFrames.push(windowElement.frames); 243 | for (var i = 0; i < windowElement.frames.length; i++) { 244 | getAllFrames(windowElement.frames[i], allFrames); 245 | } 246 | return allFrames; 247 | } 248 | 249 | function getElementsToListen(windowElement) { 250 | return getAllFrames(windowElement, []); 251 | } 252 | 253 | function bind(el, eventType, handler) { 254 | if (el.addEventListener) { // DOM Level 2 browsers 255 | el.addEventListener(eventType, handler, false); 256 | } else if (el.attachEvent) { // IE <= 8 257 | el.attachEvent('on' + eventType, handler); 258 | } else { // ancient browsers 259 | el['on' + eventType] = handler; 260 | } 261 | } 262 | 263 | function unbind(el, eventType, handler) { 264 | if (el.removeEventListener) { 265 | el.removeEventListener(eventType, handler, false); 266 | } else if (el.detachEvent) { 267 | el.detachEvent("on" + eventType, handler); 268 | } else { 269 | el["on" + eventType] = null; 270 | } 271 | } 272 | 273 | function manageSingleElementEvents(element, action, events, handler) { 274 | var eventIndex = 0, 275 | eventCount = events.length; 276 | 277 | for (; eventIndex < eventCount; eventIndex++) { 278 | action(element, events[eventIndex], handler); 279 | } 280 | } 281 | 282 | function manageEvents(elements, action, events, handler) { 283 | var elementIndex = 0, 284 | elementCount = elements.length; 285 | 286 | for (; elementIndex < elementCount; elementIndex++) { 287 | // Have to attach events with some delay between iframes. Otherwise, iframe events are not captured 288 | setTimeout(manageSingleElementEvents, 50, elements[elementIndex], action, events, handler); 289 | } 290 | } 291 | 292 | function recordEvent(e) { 293 | var code; 294 | 295 | if (generateObject) { 296 | console.recorderLog(JSON.stringify(generateObject(e), true, 2)); 297 | } 298 | if (generateCode) { 299 | code = generateCode(e); 300 | 301 | recordedCode = recordedCode + code + '\n'; 302 | console.recorderLog(code); 303 | } 304 | } 305 | 306 | function record() { 307 | var elementsToListen = getElementsToListen(windowToListen || window); 308 | console.recorderLog = console.log; // hijack the console.log so that only recorded code will be shown 309 | console.log = function () {}; 310 | manageEvents(elementsToListen, bind, eventsToRecord, recordEvent); 311 | } 312 | 313 | function stop() { 314 | var elementsToListen = getElementsToListen(windowToListen || window); 315 | if (console.recorderLog) { 316 | console.log = console.recorderLog; 317 | } 318 | manageEvents(elementsToListen, unbind, eventsToRecord, recordEvent); 319 | } 320 | 321 | function getRecordedCode() { 322 | return recordedCode; 323 | } 324 | 325 | function clearRecordedCode() { 326 | return recordedCode = ''; 327 | } 328 | 329 | module.exports = { 330 | init: init, 331 | setWindowToListen: setWindowToListen, 332 | record: record, 333 | stop: stop, 334 | getRecordedCode: getRecordedCode, 335 | clearRecordedCode: clearRecordedCode 336 | }; 337 | 338 | },{}]},{},[1]) -------------------------------------------------------------------------------- /ui-recorder-ext-selector.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { 48 | return '.' + el.className.split(' ').join('.'); 49 | } 50 | return ''; 51 | } 52 | 53 | function getCssSelector(el) { 54 | var selectorList = ['', ''], 55 | selector, 56 | parentEl; 57 | 58 | selectorList[1] = getIdOrCls(el); 59 | 60 | if (el.id) { 61 | return selectorList[1]; 62 | } 63 | 64 | if (selectorList[1].length === 0) { 65 | selector = el.nodeName; 66 | 67 | if (selector === 'A') { 68 | selector += ':contains(' + el.textContent + ')' 69 | } 70 | selectorList[1] = selector; 71 | } 72 | 73 | parentEl = dom.up(el, function (element) { 74 | return getIdOrCls(element).length > 0; 75 | }); 76 | 77 | selectorList[0] = getIdOrCls(parentEl); 78 | 79 | return selectorList.join(' ').trim(); 80 | } 81 | 82 | module.exports = { 83 | getSelector: getCssSelector 84 | }; 85 | },{"./dom":5}],4:[function(require,module,exports){ 86 | /*jslint nomen: true*/ 87 | /*global $,define,require,module */ 88 | 89 | function isEnterText(evt) { 90 | var element = evt.target; 91 | return (element.type === 'text' || element.type === 'textarea') && evt.type === 'keyup'; 92 | } 93 | 94 | function getCustomEventType(evt) { 95 | if (isEnterText(evt)) { 96 | return 'enterText'; 97 | } 98 | } 99 | 100 | module.exports = { 101 | getType: getCustomEventType 102 | }; 103 | },{}],5:[function(require,module,exports){ 104 | /*jslint nomen: true*/ 105 | /*global $,define,require,module */ 106 | 107 | function up(el, stopCondition) { 108 | var target = el; 109 | 110 | while (target.parentNode) { 111 | target = target.parentNode; 112 | if (stopCondition(target)) { 113 | break; 114 | } 115 | } 116 | return target; 117 | } 118 | 119 | module.exports = { 120 | up: up 121 | }; 122 | },{}],6:[function(require,module,exports){ 123 | /*jslint nomen: true*/ 124 | /*global $,define,require,module */ 125 | 126 | var customEvent = require('./custom-event'), 127 | codingMap = { 128 | click: '.waitAndClick', 129 | enterText: '.typeValue' // this is a non-existing event to represent type in values to a textbox or textarea 130 | }; 131 | 132 | function getEventCode(evt) { 133 | var code = codingMap[evt.type]; 134 | 135 | if (code) { 136 | return code; 137 | } 138 | 139 | // handle non-existing events 140 | return codingMap[customEvent.getType(evt)]; 141 | } 142 | 143 | module.exports = { 144 | getEventCode: getEventCode 145 | }; 146 | },{"./custom-event":4}],7:[function(require,module,exports){ 147 | /*jslint nomen: true*/ 148 | /*global $,define,require,module */ 149 | 150 | var eventsWithCoordinates = { 151 | mouseup: true, 152 | mousedown: true, 153 | mousemove: true, 154 | mouseover: true 155 | }; 156 | 157 | function getClientCoordinates(evt) { 158 | if (!eventsWithCoordinates[evt.type]) { 159 | return ''; 160 | } 161 | 162 | return { 163 | x: evt.clientX, 164 | y: evt.clientY 165 | }; 166 | } 167 | 168 | module.exports = { 169 | getClientCoordinates: getClientCoordinates 170 | }; 171 | },{}],8:[function(require,module,exports){ 172 | /*jslint nomen: true*/ 173 | /*global $,define,require,module */ 174 | 175 | module.exports = [ 176 | 'click', 177 | // 'focus', 178 | // 'blur', 179 | 'dblclick', 180 | 'change', 181 | 'keyup', 182 | // 'keydown', 183 | // 'keypress', 184 | 'mousedown', 185 | // 'mousemove', 186 | // 'mouseout', 187 | // 'mouseover', 188 | // 'mouseup', 189 | 'resize', 190 | // 'scroll', 191 | 'select', 192 | 'submit', 193 | 'load', 194 | 'unload' 195 | ]; 196 | 197 | //var events = [ 198 | // abort, 199 | // afterprint, 200 | // beforeprint, 201 | // beforeunload, 202 | // blur, 203 | // canplay, 204 | // canplaythrough, 205 | // change, 206 | // click, 207 | // contextmenu, 208 | // copy, 209 | // cuechange, 210 | // cut, 211 | // dblclick, 212 | // DOMContentLoaded, 213 | // drag, 214 | // dragend, 215 | // dragenter, 216 | // dragleave, 217 | // dragover, 218 | // dragstart, 219 | // drop, 220 | // durationchange, 221 | // emptied, 222 | // ended, 223 | // error, 224 | // focus, 225 | // focusin, 226 | // focusout, 227 | // formchange, 228 | // forminput, 229 | // hashchange, 230 | // input, 231 | // invalid, 232 | // keydown, 233 | // keypress, 234 | // keyup, 235 | // load, 236 | // loadeddata, 237 | // loadedmetadata, 238 | // loadstart, 239 | // message, 240 | // mousedown, 241 | // mouseenter, 242 | // mouseleave, 243 | // mousemove, 244 | // mouseout, 245 | // mouseover, 246 | // mouseup, 247 | // mousewheel, 248 | // offline, 249 | // online, 250 | // pagehide, 251 | // pageshow, 252 | // paste, 253 | // pause, 254 | // play, 255 | // playing, 256 | // popstate, 257 | // progress, 258 | // ratechange, 259 | // readystatechange, 260 | // redo, 261 | // reset, 262 | // resize, 263 | // scroll, 264 | // seeked, 265 | // seeking, 266 | // select, 267 | // show, 268 | // stalled, 269 | // storage, 270 | // submit, 271 | // suspend, 272 | // timeupdate, 273 | // undo, 274 | // unload, 275 | // volumechange, 276 | // waiting 277 | //]; 278 | },{}],9:[function(require,module,exports){ 279 | /*jslint nomen: true*/ 280 | /*global $,define,require,module */ 281 | 282 | var recordedCode = '', 283 | generateCode, 284 | generateObject, 285 | eventsToRecord, 286 | windowToListen; 287 | 288 | function init(config) { 289 | generateCode = config.generateCode; 290 | generateObject = config.generateObject; 291 | eventsToRecord = config.eventsToRecord; 292 | } 293 | 294 | function setWindowToListen(windowElement) { 295 | windowToListen = windowElement; 296 | } 297 | 298 | // Each frame is a window 299 | function getAllFrames(windowElement, allFrames) { 300 | allFrames.push(windowElement.frames); 301 | for (var i = 0; i < windowElement.frames.length; i++) { 302 | getAllFrames(windowElement.frames[i], allFrames); 303 | } 304 | return allFrames; 305 | } 306 | 307 | function getElementsToListen(windowElement) { 308 | return getAllFrames(windowElement, []); 309 | } 310 | 311 | function bind(el, eventType, handler) { 312 | if (el.addEventListener) { // DOM Level 2 browsers 313 | el.addEventListener(eventType, handler, false); 314 | } else if (el.attachEvent) { // IE <= 8 315 | el.attachEvent('on' + eventType, handler); 316 | } else { // ancient browsers 317 | el['on' + eventType] = handler; 318 | } 319 | } 320 | 321 | function unbind(el, eventType, handler) { 322 | if (el.removeEventListener) { 323 | el.removeEventListener(eventType, handler, false); 324 | } else if (el.detachEvent) { 325 | el.detachEvent("on" + eventType, handler); 326 | } else { 327 | el["on" + eventType] = null; 328 | } 329 | } 330 | 331 | function manageSingleElementEvents(element, action, events, handler) { 332 | var eventIndex = 0, 333 | eventCount = events.length; 334 | 335 | for (; eventIndex < eventCount; eventIndex++) { 336 | action(element, events[eventIndex], handler); 337 | } 338 | } 339 | 340 | function manageEvents(elements, action, events, handler) { 341 | var elementIndex = 0, 342 | elementCount = elements.length; 343 | 344 | for (; elementIndex < elementCount; elementIndex++) { 345 | // Have to attach events with some delay between iframes. Otherwise, iframe events are not captured 346 | setTimeout(manageSingleElementEvents, 50, elements[elementIndex], action, events, handler); 347 | } 348 | } 349 | 350 | function recordEvent(e) { 351 | var code; 352 | 353 | if (generateObject) { 354 | console.recorderLog(JSON.stringify(generateObject(e), true, 2)); 355 | } 356 | if (generateCode) { 357 | code = generateCode(e); 358 | 359 | recordedCode = recordedCode + code + '\n'; 360 | console.recorderLog(code); 361 | } 362 | } 363 | 364 | function record() { 365 | var elementsToListen = getElementsToListen(windowToListen || window); 366 | console.recorderLog = console.log; // hijack the console.log so that only recorded code will be shown 367 | console.log = function () {}; 368 | manageEvents(elementsToListen, bind, eventsToRecord, recordEvent); 369 | } 370 | 371 | function stop() { 372 | var elementsToListen = getElementsToListen(windowToListen || window); 373 | if (console.recorderLog) { 374 | console.log = console.recorderLog; 375 | } 376 | manageEvents(elementsToListen, unbind, eventsToRecord, recordEvent); 377 | } 378 | 379 | function getRecordedCode() { 380 | return recordedCode; 381 | } 382 | 383 | function clearRecordedCode() { 384 | return recordedCode = ''; 385 | } 386 | 387 | module.exports = { 388 | init: init, 389 | setWindowToListen: setWindowToListen, 390 | record: record, 391 | stop: stop, 392 | getRecordedCode: getRecordedCode, 393 | clearRecordedCode: clearRecordedCode 394 | }; 395 | 396 | },{}]},{},[1]) --------------------------------------------------------------------------------