tor-browser

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

DragChildContextBase.sys.mjs (11526B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 const kTestDataTransferType = "x-moz-datatransfer-test";
      7 const kTestDataTransferData = "Dragged Test Data";
      8 
      9 // Common base of DragSourceChildContext and DragTargetChildContext
     10 export class DragChildContextBase {
     11  // The name of the subtype of this object.
     12  subtypeName = "";
     13 
     14  // Map of counts of events (indexed by event type) that are expected before the next checkExpected
     15  // NB: A second expected array is maintained on the document object.  This is because the source and
     16  // target of the drag may be in the same document, in which case source-document events
     17  // will obviously appear on the target-document and vice-versa.
     18  expected = {};
     19 
     20  // Array of all of the relevantEvents received on dragElement.
     21  events = [];
     22 
     23  // (Document) event handler, which is the method with 'this' bound.
     24  eventHandler = null;
     25  documentEventHandler = null;
     26 
     27  // Array of all of the relevantEvents received on the document.
     28  // We expect this to be the same list as the list of events that
     29  // dragElement should receive, unless the other element involved
     30  // in the drag is in the same document.  @see expected.
     31  documentEvents = [];
     32 
     33  // The window
     34  dragWindow = null;
     35 
     36  // The element being dragged from or to.  Set as parameter to initialize.
     37  dragElement = null;
     38 
     39  // Position of element in client coords.
     40  clientPos = null;
     41 
     42  // Position of element in screen coords
     43  screenPos = null;
     44 
     45  // Was there a drag session before the test started?
     46  alreadyHadSession = false;
     47 
     48  // Array of monitored event types.  Set as parameter to initialize.
     49  relevantEvents = [];
     50 
     51  // Label added to diagnostic output to identify the specific drag.
     52  // Set as parameter to initialize.
     53  contextLabel = null;
     54 
     55  // Should an exception be thrown when an incorrect event is received.
     56  // Useful for debugging.  Set as parameter to initialize.
     57  throwOnExtraMessage = false;
     58 
     59  // Should events other than dragstart and drop have access to the
     60  // dataTransfer?  Set as parameter to initialize.
     61  expectProtectedDataTransferAccess = false;
     62 
     63  window = null;
     64 
     65  dragService = null;
     66 
     67  expect(aEvType) {
     68    this.expected[aEvType] += 1;
     69    this.dragWindow.document.expected[aEvType] += 1;
     70  }
     71 
     72  async checkExpected() {
     73    for (let ev of this.relevantEvents) {
     74      this.is(
     75        this.events[ev].length,
     76        this.expected[ev],
     77        `\telement received proper number of ${ev} events.`
     78      );
     79      this.is(
     80        this.documentEvents[ev].length,
     81        this.dragWindow.document.expected[ev],
     82        `\tdocument received proper number of ${ev} events.`
     83      );
     84    }
     85  }
     86 
     87  checkHasDrag(aShouldHaveDrag = true) {
     88    this.info(
     89      `${this.subtypeName} had pre-existing drag: ${this.alreadyHadSession}`
     90    );
     91    this.ok(
     92      !!this.dragService.getCurrentSession(this.dragWindow) ==
     93        aShouldHaveDrag || this.alreadyHadSession,
     94      `Has ${!aShouldHaveDrag ? "no " : ""}drag session`
     95    );
     96  }
     97 
     98  checkSessionHasAction() {
     99    this.checkHasDrag();
    100    if (this.alreadyHadSession) {
    101      return;
    102    }
    103    this.ok(
    104      this.dragService.getCurrentSession(this.dragWindow).dragAction !==
    105        Ci.nsIDragService.DRAGDROP_ACTION_NONE,
    106      "Drag session has valid action"
    107    );
    108  }
    109 
    110  // Adapted from EventUtils
    111  nodeIsFlattenedTreeDescendantOf(aPossibleDescendant, aPossibleAncestor) {
    112    do {
    113      if (aPossibleDescendant == aPossibleAncestor) {
    114        return true;
    115      }
    116      aPossibleDescendant = aPossibleDescendant.flattenedTreeParentNode;
    117    } while (aPossibleDescendant);
    118    return false;
    119  }
    120 
    121  // Adapted from EventUtils
    122  getInclusiveFlattenedTreeParentElement(aNode) {
    123    for (
    124      let inclusiveAncestor = aNode;
    125      inclusiveAncestor;
    126      inclusiveAncestor = this.getFlattenedTreeParentNode(inclusiveAncestor)
    127    ) {
    128      if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) {
    129        return inclusiveAncestor;
    130      }
    131    }
    132    return null;
    133  }
    134 
    135  constructor(aSubtypeName, aDragWindow, aParams) {
    136    this.subtypeName = aSubtypeName;
    137    this.dragWindow = aDragWindow;
    138    this.dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
    139      Ci.nsIDragService
    140    );
    141 
    142    Object.assign(this, aParams);
    143 
    144    this.info = msg => {
    145      aParams.info(`[${this.contextLabel}|${this.subtypeName}]| ${msg}`);
    146    };
    147    this.ok = (cond, msg) => {
    148      aParams.ok(cond, `[${this.contextLabel}|${this.subtypeName}]| ${msg}`);
    149    };
    150    this.is = (v1, v2, msg) => {
    151      aParams.is(v1, v2, `[${this.contextLabel}|${this.subtypeName}]| ${msg}`);
    152    };
    153 
    154    this.alreadyHadSession = !!this.dragService.getCurrentSession(
    155      this.dragWindow
    156    );
    157 
    158    this.initializeElementInfo(this.dragElementId);
    159 
    160    // Register for events on both the drag element AND the document so we can
    161    // detect that the right events were/were not sent at the right time.
    162    this.registerForRelevantEvents();
    163  }
    164 
    165  initializeElementInfo(aDragElementId) {
    166    this.dragElement = this.dragWindow.document.getElementById(aDragElementId);
    167    if (!this.dragElement) {
    168      for (let shadowRoot of this.dragWindow.document.getConnectedShadowRoots()) {
    169        if (shadowRoot.isUAWidget()) {
    170          continue;
    171        }
    172 
    173        this.dragElement = shadowRoot.getElementById(aDragElementId);
    174        if (this.dragElement) {
    175          break;
    176        }
    177      }
    178      this.ok(this.dragElement, "dragElement found in shadow DOM");
    179    } else {
    180      this.ok(this.dragElement, "dragElement found");
    181    }
    182    let rect = this.dragElement.getBoundingClientRect();
    183    let rectStr =
    184      `left: ${rect.left}, top: ${rect.top}, ` +
    185      `right: ${rect.right}, bottom: ${rect.bottom}`;
    186    this.info(`getBoundingClientRect(): ${rectStr}`);
    187    const scale = this.dragWindow.devicePixelRatio;
    188    this.clientPos = [
    189      this.dragElement.offsetLeft * scale,
    190      this.dragElement.offsetTop * scale,
    191    ];
    192    this.screenPos = [
    193      (this.dragWindow.mozInnerScreenX + this.dragElement.offsetLeft) * scale,
    194      (this.dragWindow.mozInnerScreenY + this.dragElement.offsetTop) * scale,
    195    ];
    196  }
    197 
    198  // Checks that the event was expected and, if so, adds the event to
    199  // the list of events of that type.
    200  eventHandlerFn(aEv) {
    201    this.events[aEv.type].push(aEv);
    202    this.info(`Element received ${aEv.type}`);
    203 
    204    // In order to properly test the dataTransfer, we need to try to access the
    205    // dataTransfer under the principal of the web page.  Otherwise, we will run
    206    // under the system principal and dataTransfer access will always be given.
    207    let sandbox = this.dragWindow.SpecialPowers.unwrap(
    208      Cu.Sandbox(this.dragWindow.document.nodePrincipal)
    209    );
    210 
    211    sandbox.is = this.is;
    212    sandbox.kTestDataTransferType = kTestDataTransferType;
    213    sandbox.kTestDataTransferData = kTestDataTransferData;
    214    sandbox.aEv = aEv;
    215 
    216    let getFromDataTransfer = Cu.evalInSandbox(
    217      "(" +
    218        function () {
    219          return aEv.dataTransfer.getData(kTestDataTransferType);
    220        } +
    221        ")",
    222      sandbox
    223    );
    224    let setInDataTransfer = Cu.evalInSandbox(
    225      "(" +
    226        function () {
    227          return aEv.dataTransfer.setData(
    228            kTestDataTransferType,
    229            kTestDataTransferData
    230          );
    231        } +
    232        ")",
    233      sandbox
    234    );
    235    let clearDataTransfer = Cu.evalInSandbox(
    236      "(" +
    237        function () {
    238          return aEv.dataTransfer.setData(kTestDataTransferType, "");
    239        } +
    240        ")",
    241      sandbox
    242    );
    243 
    244    try {
    245      if (aEv.type == "dragstart") {
    246        // Add some additional data to the DataTransfer so we can look for it
    247        // as we get later events.
    248        this.is(
    249          getFromDataTransfer(),
    250          "",
    251          `[${aEv.type}]| DataTransfer didn't have kTestDataTransferType`
    252        );
    253        setInDataTransfer();
    254        this.is(
    255          getFromDataTransfer(),
    256          kTestDataTransferData,
    257          `[${aEv.type}]| Successfully added kTestDataTransferType to DataTransfer`
    258        );
    259      } else if (aEv.type == "drop") {
    260        this.is(
    261          getFromDataTransfer(),
    262          kTestDataTransferData,
    263          `[${aEv.type}]| Successfully read from DataTransfer`
    264        );
    265        try {
    266          clearDataTransfer();
    267          this.ok(false, "Writing to DataTransfer throws an exception");
    268        } catch (ex) {
    269          this.ok(true, "Got exception: " + ex);
    270        }
    271        this.is(
    272          getFromDataTransfer(),
    273          kTestDataTransferData,
    274          `[${aEv.type}]| Properly failed to write to DataTransfer`
    275        );
    276      } else if (
    277        aEv.type == "dragenter" ||
    278        aEv.type == "dragover" ||
    279        aEv.type == "dragleave" ||
    280        aEv.type == "dragend"
    281      ) {
    282        this.is(
    283          getFromDataTransfer(),
    284          this.expectProtectedDataTransferAccess ? kTestDataTransferData : "",
    285          `[${aEv.type}]| ${
    286            this.expectProtectedDataTransferAccess
    287              ? "Successfully"
    288              : "Unsuccessfully"
    289          } read from DataTransfer`
    290        );
    291      }
    292    } catch (ex) {
    293      this.ok(false, "Handler did not throw an uncaught exception: " + ex);
    294    }
    295 
    296    if (
    297      this.throwOnExtraMessage &&
    298      this.events[aEv.type].length > this.expected[aEv.type]
    299    ) {
    300      throw new Error(
    301        `[${this.contextLabel}|${this.subtypeName}] Received unexpected ${
    302          aEv.type
    303        } | received ${this.events[aEv.type].length} > expected ${
    304          this.expected[aEv.type]
    305        } | event: ${aEv}`
    306      );
    307    }
    308  }
    309 
    310  documentEventHandlerFn(aEv) {
    311    this.documentEvents[aEv.type].push(aEv);
    312    this.info(`Document received ${aEv.type}`);
    313    if (
    314      this.throwOnExtraMessage &&
    315      this.documentEvents[aEv.type].length >
    316        this.dragWindow.document.expected[aEv.type]
    317    ) {
    318      throw new Error(
    319        `[${this.contextLabel}|${
    320          this.subtypeName
    321        }] Document received unexpected ${aEv.type} | received ${
    322          this.documentEvents[aEv.type].length
    323        } > expected ${
    324          this.dragWindow.document.expected[aEv.type]
    325        } | event: ${aEv}`
    326      );
    327    }
    328  }
    329 
    330  registerForRelevantEvents() {
    331    this.eventHandler = this.eventHandlerFn.bind(this);
    332    this.documentEventHandler = this.documentEventHandlerFn.bind(this);
    333    if (!this.dragWindow.document.expected) {
    334      this.dragWindow.document.expected = [];
    335    }
    336    for (let ev of this.relevantEvents) {
    337      this.events[ev] = [];
    338      this.documentEvents[ev] = [];
    339      this.expected[ev] = 0;
    340      // See the comment on the declaration of this.expected for the reason
    341      // why we define the expected array for document events on the document
    342      // itself.
    343      this.dragWindow.document.expected[ev] = 0;
    344      this.dragElement.addEventListener(ev, this.eventHandler);
    345      this.dragWindow.document.addEventListener(ev, this.documentEventHandler);
    346    }
    347  }
    348 
    349  getElementPositions() {
    350    this.info(`clientpos: ${this.clientPos}, screenPos: ${this.screenPos}`);
    351    return { clientPos: this.clientPos, screenPos: this.screenPos };
    352  }
    353 
    354  async cleanup() {
    355    for (let ev of this.relevantEvents) {
    356      this.dragElement.removeEventListener(ev, this.eventHandler);
    357      this.dragWindow.document.removeEventListener(
    358        ev,
    359        this.documentEventHandler
    360      );
    361    }
    362  }
    363 }