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 }