tor-browser

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

promisified-events.js (11541B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 // This is loaded by head.js, so has the same globals, hence we import the
      8 // globals from there.
      9 /* import-globals-from common.js */
     10 
     11 /* exported EVENT_ANNOUNCEMENT, EVENT_ALERT, EVENT_REORDER, EVENT_SCROLLING,
     12            EVENT_SCROLLING_END, EVENT_SHOW, EVENT_TEXT_INSERTED,
     13            EVENT_TEXT_REMOVED, EVENT_DOCUMENT_LOAD_COMPLETE, EVENT_HIDE,
     14            EVENT_TEXT_ATTRIBUTE_CHANGED, EVENT_TEXT_CARET_MOVED, EVENT_SELECTION,
     15            EVENT_DESCRIPTION_CHANGE, EVENT_NAME_CHANGE, EVENT_STATE_CHANGE,
     16            EVENT_VALUE_CHANGE, EVENT_TEXT_VALUE_CHANGE, EVENT_FOCUS,
     17            EVENT_DOCUMENT_RELOAD, EVENT_VIRTUALCURSOR_CHANGED, EVENT_ALERT,
     18            EVENT_OBJECT_ATTRIBUTE_CHANGED, EVENT_MENUPOPUP_START, EVENT_MENUPOPUP_END, EVENT_ERRORMESSAGE_CHANGED,
     19            UnexpectedEvents, waitForEvent,
     20            waitForEvents, waitForOrderedEvents, waitForStateChange,
     21            stateChangeEventArgs */
     22 
     23 const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT;
     24 const EVENT_DOCUMENT_LOAD_COMPLETE =
     25  nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE;
     26 const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE;
     27 const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER;
     28 const EVENT_SCROLLING = nsIAccessibleEvent.EVENT_SCROLLING;
     29 const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START;
     30 const EVENT_SCROLLING_END = nsIAccessibleEvent.EVENT_SCROLLING_END;
     31 const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION;
     32 const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN;
     33 const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW;
     34 const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE;
     35 const EVENT_TEXT_ATTRIBUTE_CHANGED =
     36  nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED;
     37 const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED;
     38 const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED;
     39 const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED;
     40 const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE;
     41 const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE;
     42 const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE;
     43 const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE;
     44 const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS;
     45 const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD;
     46 const EVENT_VIRTUALCURSOR_CHANGED =
     47  nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED;
     48 const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT;
     49 const EVENT_TEXT_SELECTION_CHANGED =
     50  nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED;
     51 const EVENT_LIVE_REGION_ADDED = nsIAccessibleEvent.EVENT_LIVE_REGION_ADDED;
     52 const EVENT_LIVE_REGION_REMOVED = nsIAccessibleEvent.EVENT_LIVE_REGION_REMOVED;
     53 const EVENT_OBJECT_ATTRIBUTE_CHANGED =
     54  nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED;
     55 const EVENT_INNER_REORDER = nsIAccessibleEvent.EVENT_INNER_REORDER;
     56 const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START;
     57 const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END;
     58 const EVENT_ERRORMESSAGE_CHANGED =
     59  nsIAccessibleEvent.EVENT_ERRORMESSAGE_CHANGED;
     60 
     61 const EventsLogger = {
     62  enabled: false,
     63 
     64  log(msg) {
     65    if (this.enabled) {
     66      info(msg);
     67    }
     68  },
     69 };
     70 
     71 /**
     72 * Describe an event in string format.
     73 *
     74 * @param  {nsIAccessibleEvent}  event  event to strigify
     75 */
     76 function eventToString(event) {
     77  let type = eventTypeToString(event.eventType);
     78  let info = `Event type: ${type}`;
     79 
     80  if (event instanceof nsIAccessibleStateChangeEvent) {
     81    let stateStr = statesToString(
     82      event.isExtraState ? 0 : event.state,
     83      event.isExtraState ? event.state : 0
     84    );
     85    info += `, state: ${stateStr}, is enabled: ${event.isEnabled}`;
     86  } else if (event instanceof nsIAccessibleTextChangeEvent) {
     87    let tcType = event.isInserted ? "inserted" : "removed";
     88    info += `, start: ${event.start}, length: ${event.length}, ${tcType} text: ${event.modifiedText}`;
     89  }
     90 
     91  info += `. Target: ${prettyName(event.accessible)}`;
     92  return info;
     93 }
     94 
     95 function matchEvent(event, matchCriteria) {
     96  if (!matchCriteria) {
     97    return true;
     98  }
     99 
    100  let acc = event.accessible;
    101  switch (typeof matchCriteria) {
    102    case "string": {
    103      let id = getAccessibleDOMNodeID(acc);
    104      if (id === matchCriteria) {
    105        EventsLogger.log(`Event matches DOMNode id: ${id}`);
    106        return true;
    107      }
    108      break;
    109    }
    110    case "function":
    111      if (matchCriteria(event)) {
    112        EventsLogger.log(
    113          `Lambda function matches event: ${eventToString(event)}`
    114        );
    115        return true;
    116      }
    117      break;
    118    default:
    119      if (matchCriteria instanceof nsIAccessible) {
    120        if (acc === matchCriteria) {
    121          EventsLogger.log(`Event matches accessible: ${prettyName(acc)}`);
    122          return true;
    123        }
    124      } else if (event.DOMNode == matchCriteria) {
    125        EventsLogger.log(
    126          `Event matches DOM node: ${prettyName(event.DOMNode)}`
    127        );
    128        return true;
    129      }
    130  }
    131 
    132  return false;
    133 }
    134 
    135 /**
    136 * A helper function that returns a promise that resolves when an accessible
    137 * event of the given type with the given target (defined by its id or
    138 * accessible) is observed.
    139 *
    140 * @param  {number}                eventType        expected accessible event
    141 *                                                  type
    142 * @param  {string | nsIAccessible | Function}  matchCriteria  expected content
    143 *                                                         element id
    144 *                                                         for the event
    145 * @param  {string}                message          Message to prepend to logging.
    146 * @return {Promise}                                promise that resolves to an
    147 *                                                  event
    148 */
    149 function waitForEvent(eventType, matchCriteria, message) {
    150  return new Promise(resolve => {
    151    let eventObserver = {
    152      observe(subject, topic) {
    153        if (topic !== "accessible-event") {
    154          return;
    155        }
    156 
    157        let event = subject.QueryInterface(nsIAccessibleEvent);
    158        if (EventsLogger.enabled) {
    159          // Avoid calling eventToString if the EventsLogger isn't enabled in order
    160          // to avoid an intermittent crash (bug 1307645).
    161          EventsLogger.log(eventToString(event));
    162        }
    163 
    164        // If event type does not match expected type, skip the event.
    165        if (event.eventType !== eventType) {
    166          return;
    167        }
    168 
    169        if (matchEvent(event, matchCriteria)) {
    170          EventsLogger.log(
    171            `Correct event type: ${eventTypeToString(eventType)}`
    172          );
    173          Services.obs.removeObserver(this, "accessible-event");
    174          ok(
    175            true,
    176            `${message ? message + ": " : ""}Received ${eventTypeToString(
    177              eventType
    178            )} event`
    179          );
    180          resolve(event);
    181        }
    182      },
    183    };
    184    Services.obs.addObserver(eventObserver, "accessible-event");
    185  });
    186 }
    187 
    188 class UnexpectedEvents {
    189  constructor(unexpected) {
    190    if (unexpected.length) {
    191      this.unexpected = unexpected;
    192      Services.obs.addObserver(this, "accessible-event");
    193    }
    194  }
    195 
    196  observe(subject, topic) {
    197    if (topic !== "accessible-event") {
    198      return;
    199    }
    200 
    201    let event = subject.QueryInterface(nsIAccessibleEvent);
    202 
    203    let unexpectedEvent = this.unexpected.find(
    204      ([etype, criteria]) =>
    205        etype === event.eventType && matchEvent(event, criteria)
    206    );
    207 
    208    if (unexpectedEvent) {
    209      ok(false, `Got unexpected event: ${eventToString(event)}`);
    210    }
    211  }
    212 
    213  stop() {
    214    if (this.unexpected) {
    215      Services.obs.removeObserver(this, "accessible-event");
    216    }
    217  }
    218 }
    219 
    220 /**
    221 * A helper function that waits for a sequence of accessible events in
    222 * specified order.
    223 *
    224 * @param {Array}   events          a list of events to wait (same format as
    225 *                                   waitForEvent arguments)
    226 * @param {string}  message         Message to prepend to logging.
    227 * @param {boolean} ordered         Events need to be received in given order.
    228 * @param {object}  invokerOrWindow a local window or a special content invoker
    229 *                                   it takes a list of arguments and a task
    230 *                                   function.
    231 */
    232 async function waitForEvents(
    233  events,
    234  message,
    235  ordered = false,
    236  invokerOrWindow = null
    237 ) {
    238  let expected = events.expected || events;
    239  // Next expected event index.
    240  let currentIdx = 0;
    241 
    242  let unexpectedListener = events.unexpected
    243    ? new UnexpectedEvents(events.unexpected)
    244    : null;
    245 
    246  let results = await Promise.all(
    247    expected.map((evt, idx) => {
    248      const [eventType, matchCriteria] = evt;
    249      return waitForEvent(eventType, matchCriteria, message).then(result => {
    250        return [result, idx == currentIdx++];
    251      });
    252    })
    253  );
    254 
    255  if (unexpectedListener) {
    256    let flushQueue = async win => {
    257      // Flush all notifications or queued a11y events.
    258      win.windowUtils.advanceTimeAndRefresh(100);
    259 
    260      // Flush all DOM async events.
    261      await new Promise(r => win.setTimeout(r, 0));
    262 
    263      // Flush all notifications or queued a11y events resulting from async DOM events.
    264      win.windowUtils.advanceTimeAndRefresh(100);
    265 
    266      // Flush all notifications or a11y events that may have been queued in the last tick.
    267      win.windowUtils.advanceTimeAndRefresh(100);
    268 
    269      // Return refresh to normal.
    270      win.windowUtils.restoreNormalRefresh();
    271    };
    272 
    273    if (invokerOrWindow instanceof Function) {
    274      await invokerOrWindow([flushQueue.toString()], async _flushQueue => {
    275        // eslint-disable-next-line no-eval, no-undef
    276        await eval(_flushQueue)(content);
    277      });
    278    } else {
    279      await flushQueue(invokerOrWindow ? invokerOrWindow : window);
    280    }
    281 
    282    unexpectedListener.stop();
    283  }
    284 
    285  if (ordered) {
    286    ok(
    287      results.every(([, isOrdered]) => isOrdered),
    288      `${message ? message + ": " : ""}Correct event order`
    289    );
    290  }
    291 
    292  return results.map(([event]) => event);
    293 }
    294 
    295 function waitForOrderedEvents(events, message) {
    296  return waitForEvents(events, message, true);
    297 }
    298 
    299 function stateChangeEventArgs(id, state, isEnabled, isExtra = false) {
    300  return [
    301    EVENT_STATE_CHANGE,
    302    e => {
    303      e.QueryInterface(nsIAccessibleStateChangeEvent);
    304      return (
    305        e.state == state &&
    306        e.isExtraState == isExtra &&
    307        isEnabled == e.isEnabled &&
    308        (typeof id == "string"
    309          ? id == getAccessibleDOMNodeID(e.accessible)
    310          : getAccessible(id) == e.accessible)
    311      );
    312    },
    313  ];
    314 }
    315 
    316 function waitForStateChange(id, state, isEnabled, isExtra = false) {
    317  return waitForEvent(...stateChangeEventArgs(id, state, isEnabled, isExtra));
    318 }
    319 
    320 ////////////////////////////////////////////////////////////////////////////////
    321 // Utility functions ported from events.js.
    322 
    323 /**
    324 * This function selects all text in the passed-in element if it has an editor,
    325 * before setting focus to it. This simulates behavio with the keyboard when
    326 * tabbing to the element. This does explicitly what synthFocus did implicitly.
    327 * This should be called only if you really want this behavior.
    328 *
    329 * @param  {string}  id  The element ID to focus
    330 */
    331 function selectAllTextAndFocus(id) {
    332  const elem = getNode(id);
    333  if (elem.editor) {
    334    elem.selectionStart = elem.selectionEnd = elem.value.length;
    335  }
    336 
    337  elem.focus();
    338 }