tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);