OLD | NEW |
(Empty) | |
| 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 sequenc
e of events captured at anytime after the last clear() |
| 10 // call, between when the recorder was started and stopped (includin
g multiple start/stop pairs) |
| 11 // * configure |
| 12 // * sets options that should apply to the recorder. If the recorder has any e
xisting 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 al
l 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 the
y label> |
| 36 // }; |
| 37 // * targetTestID = the string identifier that the associated target object sh
ould 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 even
ts, 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 ev
ents are being dispatched), and others may be merged |
| 52 // given the 'mergeEventTypes' option in the EventRecorder, this value is th
e 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 ev
ents 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 i
t returned). |
| 60 // * Has the value null if no nested events were recorded during the invocatio
n of this listener. |
| 61 // * interfaceType |
| 62 // * The string indicating which Event object (or derived Event object type) t
he recorded event object instance is based on. |
| 63 // * event |
| 64 // * Access to the recorded event properties for the event instance (not the a
ctual event instance itself). A snapshot of the |
| 65 // enumerable properties of the event object instance at the moment the list
ener 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.) a
re 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 // [NoInterfaceObject] |
| 80 // interface EventRecorderRegistration { |
| 81 // void addRecordedEventListener(SupportedEventTypes type, EventListener? han
dler, optional boolean capturePhase = false); |
| 82 // void removeRecordedEventListener(SupportedEventTypes type, EventListener?
handler, optional boolean capturePhase = false); |
| 83 // }; |
| 84 // Node implements EventRecorderRegistration; |
| 85 // |
| 86 // enum SupportedEventTypes = { |
| 87 // "mousemove", |
| 88 // etc... |
| 89 // }; |
| 90 // * addRecordedEventListener |
| 91 // * handler = pass null if you want only a default recording of the even
t (and don't need any other special handling). Otherwise, |
| 92 // the handler will be invoked normally as part of the event'
s dispatch. |
| 93 // * <other params> are the same as those defined on addEventListener/removeEv
entListenter APIs (see DOM4) |
| 94 // * Use this API *instead of* addEventListener to record your events for test
ing purposes. |
| 95 |
| 96 (function EventRecorderScope(global) { |
| 97 "use strict"; |
| 98 |
| 99 if (global.EventRecorder) |
| 100 return; // Already initialized. |
| 101 |
| 102 // WeakMap polyfill |
| 103 if (!global.WeakMap) { |
| 104 throw new Error("EventRecorder depends on WeakMap! Please polyfill for com
pleteness to run in this user agent!"); |
| 105 } |
| 106 |
| 107 // Globally applicable variables |
| 108 var allRecords = []; |
| 109 var recording = false; |
| 110 var rawOrder = 1; |
| 111 var mergeTypesTruthMap = {}; // format of { eventType: true, ... } |
| 112 var eventsInScope = []; // Tracks synchronous event dispatches |
| 113 var handlerMap = new WeakMap(); // Keeps original handlers (so that they can
be used to un-register for events. |
| 114 |
| 115 // Find all Event Object Constructors on the global and add them to the map a
long with their name (sans 'Event') |
| 116 var eventConstructorsNameMap = new WeakMap(); // format of key: hostObject, v
alue: alias to use. |
| 117 var regex = /[A-Z][A-Za-z0-9]+Event$/; |
| 118 Object.getOwnPropertyNames(global).forEach(function (propName) { |
| 119 if (regex.test(propName)) |
| 120 eventConstructorsNameMap.set(global[propName], propName); |
| 121 }); |
| 122 var knownObjectsMap = eventConstructorsNameMap; |
| 123 |
| 124 Object.defineProperty(global, "EventRecorder", { |
| 125 writable: true, |
| 126 configurable: true, |
| 127 value: Object.create(null, { |
| 128 start: { |
| 129 enumerable: true, configurable: true, writable: true, value: functio
n start() { recording = true; } |
| 130 }, |
| 131 stop: { |
| 132 enumerable: true, configurable: true, writable: true, value: functio
n stop() { recording = false; } |
| 133 }, |
| 134 clearRecords: { |
| 135 enumerable: true, configurable: true, writable: true, value: functio
n clearRecords() { |
| 136 rawOrder = 1; |
| 137 allRecords = []; |
| 138 } |
| 139 }, |
| 140 getRecords: { |
| 141 enumerable: true, configurable: true, writable: true, value: functio
n getRecords() { return allRecords; } |
| 142 }, |
| 143 configure: { |
| 144 enumerable: true, configurable: true, writable: true, value: functio
n configure(options) { |
| 145 if (allRecords.length > 0) |
| 146 throw new Error("Wrong time to call me: EventRecorder.configur
e must only be called when no recorded events are present. Try 'clearRecords' fi
rst."); |
| 147 |
| 148 // Un-configure existing options by calling again with no options
set... |
| 149 mergeTypesTruthMap = {}; |
| 150 knownObjectsMap = eventConstructorsNameMap; |
| 151 |
| 152 if (!(options instanceof Object)) |
| 153 return; |
| 154 // Sanitize the passed object (tease-out getter functions) |
| 155 var sanitizedOptions = {}; |
| 156 for (var x in options) { |
| 157 sanitizedOptions[x] = options[x]; |
| 158 } |
| 159 if (sanitizedOptions.mergeEventTypes && Array.isArray(sanitizedOp
tions.mergeEventTypes)) { |
| 160 sanitizedOptions.mergeEventTypes.forEach(function (eventType)
{ |
| 161 if (typeof eventType == "string") |
| 162 mergeTypesTruthMap[eventType] = true; |
| 163 }); |
| 164 } |
| 165 if (sanitizedOptions.objectMap && (sanitizedOptions.objectMap ins
tanceof Object)) { |
| 166 for (var y in sanitizedOptions.objectMap) { |
| 167 knownObjectsMap.set(sanitizedOptions.objectMap[y], y); |
| 168 } |
| 169 } |
| 170 } |
| 171 } |
| 172 }) |
| 173 }); |
| 174 |
| 175 function EventRecord(rawEvent) { |
| 176 this.chronologicalOrder = rawOrder++; |
| 177 this.sequentialOccurrences = 1; |
| 178 this.nestedEvents = null; // potentially a [] |
| 179 this.interfaceType = knownObjectsMap.get(rawEvent.constructor); |
| 180 if (!this.interfaceType) // In case (somehow) this event's constructor is
not named something with an 'Event' suffix... |
| 181 this.interfaceType = rawEvent.constructor.toString(); |
| 182 this.event = new CloneObjectLike(rawEvent); |
| 183 } |
| 184 |
| 185 // Only enumerable props including prototype-chain (non-recursive), w/no func
tions. |
| 186 function CloneObjectLike(object) { |
| 187 for (var prop in object) { |
| 188 var val = object[prop]; |
| 189 if (Array.isArray(val)) |
| 190 this[prop] = CloneArray(val); |
| 191 else if (typeof val == "function") |
| 192 continue; |
| 193 else if ((typeof val == "object") && (val != null)) { |
| 194 this[prop] = knownObjectsMap.get(val); |
| 195 if (this[prop] === undefined) |
| 196 this[prop] = "UNKNOWN_OBJECT (" + val.toString() + ")"; |
| 197 } |
| 198 else |
| 199 this[prop] = val; |
| 200 } |
| 201 } |
| 202 |
| 203 function CloneArray(array) { |
| 204 var dup = []; |
| 205 for (var i = 0, len = array.length; i < len; i++) { |
| 206 var val = array[i] |
| 207 if (typeof val == "undefined") |
| 208 throw new Error("Ugg. Sparce arrays are not supported. Sorry!"); |
| 209 else if (Array.isArray(val)) |
| 210 dup[i] = "UNKNOWN_ARRAY"; |
| 211 else if (typeof val == "function") |
| 212 dup[i] = "UNKNOWN_FUNCTION"; |
| 213 else if ((typeof val == "object") && (val != null)) { |
| 214 dup[i] = knownObjectsMap.get(val); |
| 215 if (dup[i] === undefined) |
| 216 dup[i] = "UNKNOWN_OBJECT (" + val.toString() + ")"; |
| 217 } |
| 218 else |
| 219 dup[i] = val; |
| 220 } |
| 221 return dup; |
| 222 } |
| 223 |
| 224 function generateRecordedEventHandlerWithCallback(callback) { |
| 225 return function(e) { |
| 226 if (recording) { |
| 227 // Setup the scope for any synchronous events |
| 228 eventsInScope.push(recordEvent(e)); |
| 229 callback.call(this, e); |
| 230 eventsInScope.pop(); |
| 231 } |
| 232 } |
| 233 } |
| 234 |
| 235 function recordedEventHandler(e) { |
| 236 if (recording) |
| 237 recordEvent(e); |
| 238 } |
| 239 |
| 240 function recordEvent(e) { |
| 241 var record = new EventRecord(e); |
| 242 var recordList = allRecords; |
| 243 // Adjust which sequential list to use depending on scope |
| 244 if (eventsInScope.length > 0) { |
| 245 recordList = eventsInScope[eventsInScope.length - 1].nestedEvents; |
| 246 if (recordList == null) // This top-of-stack event record hasn't had an
y nested events yet. |
| 247 recordList = eventsInScope[eventsInScope.length - 1].nestedEvents =
[]; |
| 248 } |
| 249 if (mergeTypesTruthMap[e.type] && (recordList.length > 0)) { |
| 250 var tail = recordList[recordList.length-1]; |
| 251 // Same type and currentTarget? |
| 252 if ((tail.event.type == record.event.type) && (tail.event.currentTarget
== record.event.currentTarget)) { |
| 253 tail.sequentialOccurrences++; |
| 254 return; |
| 255 } |
| 256 } |
| 257 recordList.push(record); |
| 258 return record; |
| 259 } |
| 260 |
| 261 Object.defineProperties(Node.prototype, { |
| 262 addRecordedEventListener: { |
| 263 enumerable: true, writable: true, configurable: true, |
| 264 value: function addRecordedEventListener(type, handler, capture) { |
| 265 if (handler == null) |
| 266 this.addEventListener(type, recordedEventHandler, capture); |
| 267 else { |
| 268 var subvertedHandler = generateRecordedEventHandlerWithCallback(h
andler); |
| 269 handlerMap.set(handler, subvertedHandler); |
| 270 this.addEventListener(type, subvertedHandler, capture); |
| 271 } |
| 272 } |
| 273 }, |
| 274 removeRecordedEventListener: { |
| 275 enumerable: true, writable: true, configurable: true, |
| 276 value: function addRecordedEventListener(type, handler, capture) { |
| 277 var alternateHandlerUsed = handlerMap.get(handler); |
| 278 this.removeEventListenter(type, alternateHandlerUsed ? alternateHand
lerUsed : recordedEventHandler, capture); |
| 279 } |
| 280 } |
| 281 }); |
| 282 |
| 283 })(window); |
OLD | NEW |