eventrecorder.js (13816B)
1 // interface EventRecorder { 2 // static void start(); 3 // static void stop(); 4 // static void clearRecords(); 5 // static sequence<EventRecord> getRecords(); 6 // static void configure(EventRecorderOptions options); 7 // }; 8 // * getRecords 9 // * returns an array of EventRecord objects; the array represents the sequence of events captured at anytime after the last clear() 10 // call, between when the recorder was started and stopped (including multiple start/stop pairs) 11 // * configure 12 // * sets options that should apply to the recorder. If the recorder has any existing records, than this API throws an exception. 13 // * start 14 // * starts/un-pauses the recorder 15 // * stop 16 // * stops/pauses the recorder 17 // * clear 18 // * purges all recorded records 19 20 // ---------------------- 21 22 // dictionary EventRecorderOptions { 23 // sequence<SupportedEventTypes> mergeEventTypes; 24 // ObjectNamedMap objectMap; 25 // }; 26 // * mergeEventTypes 27 // * a list of event types that should be consolidated into one record when all of the following conditions are true: 28 // 1) The events are of the same type and follow each other chronologically 29 // 2) The events' currentTarget is the same 30 // * The default is an empty list (no event types are merged). 31 // * objectMap 32 // * Sets up a series 33 34 // dictionary ObjectNamedMap { 35 // //<keys will be 'targetTestID' names, with values of the objects which they label> 36 // }; 37 // * targetTestID = the string identifier that the associated target object should be known as (for purposes of unique identification. This 38 // need not be the same as the Node's id attribute if it has one. If no 'targetTestID' string mapping is provided via this 39 // map, but is encountered later when recording specific events, a generic targetTestID of 'UNKNOWN_OBJECT' is used. 40 41 // ---------------------- 42 43 // dictionary EventRecord { 44 // unsigned long chronologicalOrder; 45 // unsigned long sequentialOccurrences; 46 // sequence<EventRecord>? nestedEvents; 47 // DOMString interfaceType; 48 // EventRecordDetails event; 49 // }; 50 // * chronologicalOrder 51 // * Since some events may be dispatched re-entrantly (e.g., while existing events are being dispatched), and others may be merged 52 // given the 'mergeEventTypes' option in the EventRecorder, this value is the actual chronological order that the event fired 53 // * sequentialOccurrences 54 // * If this event was fired multiple times in a row (see the 'mergeEventTypes' option), this value is the count of occurrences. 55 // A value of 1 means this was the only occurrence of this event (that no events were merged with it). A value greater than 1 56 // indicates that the event occurred that many times in a row. 57 // * nestedEvents 58 // * The holds all the events that were sequentially dispatched synchronously while the current event was still being dispatched 59 // (e.g., between the time that this event listener was triggered and when it returned). 60 // * Has the value null if no nested events were recorded during the invocation of this listener. 61 // * interfaceType 62 // * The string indicating which Event object (or derived Event object type) the recorded event object instance is based on. 63 // * event 64 // * Access to the recorded event properties for the event instance (not the actual event instance itself). A snapshot of the 65 // enumerable properties of the event object instance at the moment the listener was first triggered. 66 67 // ---------------------- 68 69 // dictionary EventRecordDetails { 70 // //<recorded property names with their values for all enumerable properties of the event object instance> 71 // }; 72 // * EventRecordDetails 73 // * For records with 'sequentialOccurrences' > 1, only the first occurence is recorded (subsequent event details are dropped). 74 // * Object reference values (e.g., event.target, event.currentTarget, etc.) are replaced with their mapped 'targetTestID' string. 75 // If no 'targetTestID' string mapping is available for a particular object, the value 'UNKNOWN_OBJECT' is returned. 76 77 // ---------------------- 78 79 // partial interface Node { 80 // void addRecordedEventListener(SupportedEventTypes type, EventListener? handler, optional boolean capturePhase = false); 81 // void removeRecordedEventListener(SupportedEventTypes type, EventListener? handler, optional boolean capturePhase = false); 82 // }; 83 // 84 // enum SupportedEventTypes = { 85 // "mousemove", 86 // etc... 87 // }; 88 // * addRecordedEventListener 89 // * handler = pass null if you want only a default recording of the event (and don't need any other special handling). Otherwise, 90 // the handler will be invoked normally as part of the event's dispatch. 91 // * <other params> are the same as those defined on addEventListener/removeEventListenter APIs (see DOM4) 92 // * Use this API *instead of* addEventListener to record your events for testing purposes. 93 94 (function EventRecorderScope(global) { 95 "use strict"; 96 97 if (global.EventRecorder) 98 return; // Already initialized. 99 100 // WeakMap polyfill 101 if (!global.WeakMap) { 102 throw new Error("EventRecorder depends on WeakMap! Please polyfill for completeness to run in this user agent!"); 103 } 104 105 // Globally applicable variables 106 var allRecords = []; 107 var recording = false; 108 var rawOrder = 1; 109 var mergeTypesTruthMap = {}; // format of { eventType: true, ... } 110 var eventsInScope = []; // Tracks synchronous event dispatches 111 var handlerMap = new WeakMap(); // Keeps original handlers (so that they can be used to un-register for events. 112 113 // Find all Event Object Constructors on the global and add them to the map along with their name (sans 'Event') 114 var eventConstructorsNameMap = new WeakMap(); // format of key: hostObject, value: alias to use. 115 var regex = /[A-Z][A-Za-z0-9]+Event$/; 116 Object.getOwnPropertyNames(global).forEach(function (propName) { 117 if (regex.test(propName)) 118 eventConstructorsNameMap.set(global[propName], propName); 119 }); 120 var knownObjectsMap = eventConstructorsNameMap; 121 122 Object.defineProperty(global, "EventRecorder", { 123 writable: true, 124 configurable: true, 125 value: Object.create(null, { 126 start: { 127 enumerable: true, configurable: true, writable: true, value: function start() { recording = true; } 128 }, 129 stop: { 130 enumerable: true, configurable: true, writable: true, value: function stop() { recording = false; } 131 }, 132 clearRecords: { 133 enumerable: true, configurable: true, writable: true, value: function clearRecords() { 134 rawOrder = 1; 135 allRecords = []; 136 } 137 }, 138 getRecords: { 139 enumerable: true, configurable: true, writable: true, value: function getRecords() { return allRecords; } 140 }, 141 checkRecords: { 142 enumerable: true, configurable: true, writable: true, value: function checkRecords(expected) { 143 if (expected.length < allRecords.length) { 144 return false; 145 } 146 var j = 0; 147 for (var i = 0; i < expected.length; ++i) { 148 if (j >= allRecords.length) { 149 if (expected[i].optional) { 150 continue; 151 } 152 return false; 153 } 154 if (expected[i].type == allRecords[j].event.type && expected[i].target == allRecords[j].event.currentTarget) { 155 ++j; 156 continue; 157 } 158 if (expected[i].optional) { 159 continue; 160 } 161 return false; 162 } 163 return true; 164 } 165 }, 166 configure: { 167 enumerable: true, configurable: true, writable: true, value: function configure(options) { 168 if (allRecords.length > 0) 169 throw new Error("Wrong time to call me: EventRecorder.configure must only be called when no recorded events are present. Try 'clearRecords' first."); 170 171 // Un-configure existing options by calling again with no options set... 172 mergeTypesTruthMap = {}; 173 knownObjectsMap = eventConstructorsNameMap; 174 175 if (!(options instanceof Object)) 176 return; 177 // Sanitize the passed object (tease-out getter functions) 178 var sanitizedOptions = {}; 179 for (var x in options) { 180 sanitizedOptions[x] = options[x]; 181 } 182 if (sanitizedOptions.mergeEventTypes && Array.isArray(sanitizedOptions.mergeEventTypes)) { 183 sanitizedOptions.mergeEventTypes.forEach(function (eventType) { 184 if (typeof eventType == "string") 185 mergeTypesTruthMap[eventType] = true; 186 }); 187 } 188 if (sanitizedOptions.objectMap && (sanitizedOptions.objectMap instanceof Object)) { 189 for (var y in sanitizedOptions.objectMap) { 190 knownObjectsMap.set(sanitizedOptions.objectMap[y], y); 191 } 192 } 193 } 194 }, 195 addEventListenersForNodes: { 196 enumerable: true, configurable: true, writable: true, value: function addEventListenersForNodes(events, nodes, handler) { 197 for (var i = 0; i < nodes.length; ++i) { 198 for (var j = 0; j < events.length; ++j) { 199 nodes[i].addRecordedEventListener(events[j], handler); 200 } 201 } 202 } 203 } 204 }) 205 }); 206 207 function EventRecord(rawEvent) { 208 this.chronologicalOrder = rawOrder++; 209 this.sequentialOccurrences = 1; 210 this.nestedEvents = null; // potentially a [] 211 this.interfaceType = knownObjectsMap.get(rawEvent.constructor); 212 if (!this.interfaceType) // In case (somehow) this event's constructor is not named something with an 'Event' suffix... 213 this.interfaceType = rawEvent.constructor.toString(); 214 this.event = new CloneObjectLike(rawEvent); 215 } 216 217 // Only enumerable props including prototype-chain (non-recursive), w/no functions. 218 function CloneObjectLike(object) { 219 for (var prop in object) { 220 var val = object[prop]; 221 if (Array.isArray(val)) 222 this[prop] = CloneArray(val); 223 else if (typeof val == "function") 224 continue; 225 else if ((typeof val == "object") && (val != null)) { 226 this[prop] = knownObjectsMap.get(val); 227 if (this[prop] === undefined) 228 this[prop] = "UNKNOWN_OBJECT (" + val.toString() + ")"; 229 } 230 else 231 this[prop] = val; 232 } 233 } 234 235 function CloneArray(array) { 236 var dup = []; 237 for (var i = 0, len = array.length; i < len; i++) { 238 var val = array[i] 239 if (typeof val == "undefined") 240 throw new Error("Ugg. Sparce arrays are not supported. Sorry!"); 241 else if (Array.isArray(val)) 242 dup[i] = "UNKNOWN_ARRAY"; 243 else if (typeof val == "function") 244 dup[i] = "UNKNOWN_FUNCTION"; 245 else if ((typeof val == "object") && (val != null)) { 246 dup[i] = knownObjectsMap.get(val); 247 if (dup[i] === undefined) 248 dup[i] = "UNKNOWN_OBJECT (" + val.toString() + ")"; 249 } 250 else 251 dup[i] = val; 252 } 253 return dup; 254 } 255 256 function generateRecordedEventHandlerWithCallback(callback) { 257 return function(e) { 258 if (recording) { 259 // Setup the scope for any synchronous events 260 eventsInScope.push(recordEvent(e)); 261 callback.call(this, e); 262 eventsInScope.pop(); 263 } 264 } 265 } 266 267 function recordedEventHandler(e) { 268 if (recording) 269 recordEvent(e); 270 } 271 272 function recordEvent(e) { 273 var record = new EventRecord(e); 274 var recordList = allRecords; 275 // Adjust which sequential list to use depending on scope 276 if (eventsInScope.length > 0) { 277 recordList = eventsInScope[eventsInScope.length - 1].nestedEvents; 278 if (recordList == null) // This top-of-stack event record hasn't had any nested events yet. 279 recordList = eventsInScope[eventsInScope.length - 1].nestedEvents = []; 280 } 281 if (mergeTypesTruthMap[e.type] && (recordList.length > 0)) { 282 var tail = recordList[recordList.length-1]; 283 // Same type and currentTarget? 284 if ((tail.event.type == record.event.type) && (tail.event.currentTarget == record.event.currentTarget)) { 285 tail.sequentialOccurrences++; 286 return; 287 } 288 } 289 recordList.push(record); 290 return record; 291 } 292 293 Object.defineProperties(Node.prototype, { 294 addRecordedEventListener: { 295 enumerable: true, writable: true, configurable: true, 296 value: function addRecordedEventListener(type, handler, capture) { 297 if (handler == null) 298 this.addEventListener(type, recordedEventHandler, capture); 299 else { 300 var subvertedHandler = generateRecordedEventHandlerWithCallback(handler); 301 handlerMap.set(handler, subvertedHandler); 302 this.addEventListener(type, subvertedHandler, capture); 303 } 304 } 305 }, 306 removeRecordedEventListener: { 307 enumerable: true, writable: true, configurable: true, 308 value: function addRecordedEventListener(type, handler, capture) { 309 var alternateHandlerUsed = handlerMap.get(handler); 310 this.removeEventListenter(type, alternateHandlerUsed ? alternateHandlerUsed : recordedEventHandler, capture); 311 } 312 } 313 }); 314 315 })(window);