/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const kTestDataTransferType = "x-moz-datatransfer-test"; const kTestDataTransferData = "Dragged Test Data"; // Common base of DragSourceChildContext and DragTargetChildContext export class DragChildContextBase { // The name of the subtype of this object. subtypeName = ""; // Map of counts of events (indexed by event type) that are expected before the next checkExpected // NB: A second expected array is maintained on the document object. This is because the source and // target of the drag may be in the same document, in which case source-document events // will obviously appear on the target-document and vice-versa. expected = {}; // Array of all of the relevantEvents received on dragElement. events = []; // (Document) event handler, which is the method with 'this' bound. eventHandler = null; documentEventHandler = null; // Array of all of the relevantEvents received on the document. // We expect this to be the same list as the list of events that // dragElement should receive, unless the other element involved // in the drag is in the same document. @see expected. documentEvents = []; // The window dragWindow = null; // The element being dragged from or to. Set as parameter to initialize. dragElement = null; // Position of element in client coords. clientPos = null; // Position of element in screen coords screenPos = null; // Was there a drag session before the test started? alreadyHadSession = false; // Array of monitored event types. Set as parameter to initialize. relevantEvents = []; // Label added to diagnostic output to identify the specific drag. // Set as parameter to initialize. contextLabel = null; // Should an exception be thrown when an incorrect event is received. // Useful for debugging. Set as parameter to initialize. throwOnExtraMessage = false; // Should events other than dragstart and drop have access to the // dataTransfer? Set as parameter to initialize. expectProtectedDataTransferAccess = false; window = null; dragService = null; expect(aEvType) { this.expected[aEvType] += 1; this.dragWindow.document.expected[aEvType] += 1; } async checkExpected() { for (let ev of this.relevantEvents) { this.is( this.events[ev].length, this.expected[ev], `\telement received proper number of ${ev} events.` ); this.is( this.documentEvents[ev].length, this.dragWindow.document.expected[ev], `\tdocument received proper number of ${ev} events.` ); } } checkHasDrag(aShouldHaveDrag = true) { this.info( `${this.subtypeName} had pre-existing drag: ${this.alreadyHadSession}` ); this.ok( !!this.dragService.getCurrentSession(this.dragWindow) == aShouldHaveDrag || this.alreadyHadSession, `Has ${!aShouldHaveDrag ? "no " : ""}drag session` ); } checkSessionHasAction() { this.checkHasDrag(); if (this.alreadyHadSession) { return; } this.ok( this.dragService.getCurrentSession(this.dragWindow).dragAction !== Ci.nsIDragService.DRAGDROP_ACTION_NONE, "Drag session has valid action" ); } // Adapted from EventUtils nodeIsFlattenedTreeDescendantOf(aPossibleDescendant, aPossibleAncestor) { do { if (aPossibleDescendant == aPossibleAncestor) { return true; } aPossibleDescendant = aPossibleDescendant.flattenedTreeParentNode; } while (aPossibleDescendant); return false; } // Adapted from EventUtils getInclusiveFlattenedTreeParentElement(aNode) { for ( let inclusiveAncestor = aNode; inclusiveAncestor; inclusiveAncestor = this.getFlattenedTreeParentNode(inclusiveAncestor) ) { if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) { return inclusiveAncestor; } } return null; } constructor(aSubtypeName, aDragWindow, aParams) { this.subtypeName = aSubtypeName; this.dragWindow = aDragWindow; this.dragService = Cc["@mozilla.org/widget/dragservice;1"].getService( Ci.nsIDragService ); Object.assign(this, aParams); this.info = msg => { aParams.info(`[${this.contextLabel}|${this.subtypeName}]| ${msg}`); }; this.ok = (cond, msg) => { aParams.ok(cond, `[${this.contextLabel}|${this.subtypeName}]| ${msg}`); }; this.is = (v1, v2, msg) => { aParams.is(v1, v2, `[${this.contextLabel}|${this.subtypeName}]| ${msg}`); }; this.alreadyHadSession = !!this.dragService.getCurrentSession( this.dragWindow ); this.initializeElementInfo(this.dragElementId); // Register for events on both the drag element AND the document so we can // detect that the right events were/were not sent at the right time. this.registerForRelevantEvents(); } initializeElementInfo(aDragElementId) { this.dragElement = this.dragWindow.document.getElementById(aDragElementId); if (!this.dragElement) { for (let shadowRoot of this.dragWindow.document.getConnectedShadowRoots()) { if (shadowRoot.isUAWidget()) { continue; } this.dragElement = shadowRoot.getElementById(aDragElementId); if (this.dragElement) { break; } } this.ok(this.dragElement, "dragElement found in shadow DOM"); } else { this.ok(this.dragElement, "dragElement found"); } let rect = this.dragElement.getBoundingClientRect(); let rectStr = `left: ${rect.left}, top: ${rect.top}, ` + `right: ${rect.right}, bottom: ${rect.bottom}`; this.info(`getBoundingClientRect(): ${rectStr}`); const scale = this.dragWindow.devicePixelRatio; this.clientPos = [ this.dragElement.offsetLeft * scale, this.dragElement.offsetTop * scale, ]; this.screenPos = [ (this.dragWindow.mozInnerScreenX + this.dragElement.offsetLeft) * scale, (this.dragWindow.mozInnerScreenY + this.dragElement.offsetTop) * scale, ]; } // Checks that the event was expected and, if so, adds the event to // the list of events of that type. eventHandlerFn(aEv) { this.events[aEv.type].push(aEv); this.info(`Element received ${aEv.type}`); // In order to properly test the dataTransfer, we need to try to access the // dataTransfer under the principal of the web page. Otherwise, we will run // under the system principal and dataTransfer access will always be given. let sandbox = this.dragWindow.SpecialPowers.unwrap( Cu.Sandbox(this.dragWindow.document.nodePrincipal) ); sandbox.is = this.is; sandbox.kTestDataTransferType = kTestDataTransferType; sandbox.kTestDataTransferData = kTestDataTransferData; sandbox.aEv = aEv; let getFromDataTransfer = Cu.evalInSandbox( "(" + function () { return aEv.dataTransfer.getData(kTestDataTransferType); } + ")", sandbox ); let setInDataTransfer = Cu.evalInSandbox( "(" + function () { return aEv.dataTransfer.setData( kTestDataTransferType, kTestDataTransferData ); } + ")", sandbox ); let clearDataTransfer = Cu.evalInSandbox( "(" + function () { return aEv.dataTransfer.setData(kTestDataTransferType, ""); } + ")", sandbox ); try { if (aEv.type == "dragstart") { // Add some additional data to the DataTransfer so we can look for it // as we get later events. this.is( getFromDataTransfer(), "", `[${aEv.type}]| DataTransfer didn't have kTestDataTransferType` ); setInDataTransfer(); this.is( getFromDataTransfer(), kTestDataTransferData, `[${aEv.type}]| Successfully added kTestDataTransferType to DataTransfer` ); } else if (aEv.type == "drop") { this.is( getFromDataTransfer(), kTestDataTransferData, `[${aEv.type}]| Successfully read from DataTransfer` ); try { clearDataTransfer(); this.ok(false, "Writing to DataTransfer throws an exception"); } catch (ex) { this.ok(true, "Got exception: " + ex); } this.is( getFromDataTransfer(), kTestDataTransferData, `[${aEv.type}]| Properly failed to write to DataTransfer` ); } else if ( aEv.type == "dragenter" || aEv.type == "dragover" || aEv.type == "dragleave" || aEv.type == "dragend" ) { this.is( getFromDataTransfer(), this.expectProtectedDataTransferAccess ? kTestDataTransferData : "", `[${aEv.type}]| ${ this.expectProtectedDataTransferAccess ? "Successfully" : "Unsuccessfully" } read from DataTransfer` ); } } catch (ex) { this.ok(false, "Handler did not throw an uncaught exception: " + ex); } if ( this.throwOnExtraMessage && this.events[aEv.type].length > this.expected[aEv.type] ) { throw new Error( `[${this.contextLabel}|${this.subtypeName}] Received unexpected ${ aEv.type } | received ${this.events[aEv.type].length} > expected ${ this.expected[aEv.type] } | event: ${aEv}` ); } } documentEventHandlerFn(aEv) { this.documentEvents[aEv.type].push(aEv); this.info(`Document received ${aEv.type}`); if ( this.throwOnExtraMessage && this.documentEvents[aEv.type].length > this.dragWindow.document.expected[aEv.type] ) { throw new Error( `[${this.contextLabel}|${ this.subtypeName }] Document received unexpected ${aEv.type} | received ${ this.documentEvents[aEv.type].length } > expected ${ this.dragWindow.document.expected[aEv.type] } | event: ${aEv}` ); } } registerForRelevantEvents() { this.eventHandler = this.eventHandlerFn.bind(this); this.documentEventHandler = this.documentEventHandlerFn.bind(this); if (!this.dragWindow.document.expected) { this.dragWindow.document.expected = []; } for (let ev of this.relevantEvents) { this.events[ev] = []; this.documentEvents[ev] = []; this.expected[ev] = 0; // See the comment on the declaration of this.expected for the reason // why we define the expected array for document events on the document // itself. this.dragWindow.document.expected[ev] = 0; this.dragElement.addEventListener(ev, this.eventHandler); this.dragWindow.document.addEventListener(ev, this.documentEventHandler); } } getElementPositions() { this.info(`clientpos: ${this.clientPos}, screenPos: ${this.screenPos}`); return { clientPos: this.clientPos, screenPos: this.screenPos }; } async cleanup() { for (let ev of this.relevantEvents) { this.dragElement.removeEventListener(ev, this.eventHandler); this.dragWindow.document.removeEventListener( ev, this.documentEventHandler ); } } }