BrowserTestUtilsChild.sys.mjs (11105B)
1 /* vim: set ts=2 sw=2 sts=2 et tw=80: */ 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 lazy = {}; 7 ChromeUtils.defineESModuleGetters(lazy, { 8 E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", 9 }); 10 11 class BrowserTestUtilsChildObserver { 12 constructor() { 13 this.currentObserverStatus = ""; 14 this.observerItems = []; 15 } 16 17 startObservingTopics(aTopics) { 18 for (let topic of aTopics) { 19 Services.obs.addObserver(this, topic); 20 this.observerItems.push({ topic }); 21 } 22 } 23 24 stopObservingTopics(aTopics) { 25 if (aTopics) { 26 for (let topic of aTopics) { 27 let index = this.observerItems.findIndex(item => item.topic == topic); 28 if (index >= 0) { 29 Services.obs.removeObserver(this, topic); 30 this.observerItems.splice(index, 1); 31 } 32 } 33 } else { 34 for (let topic of this.observerItems) { 35 Services.obs.removeObserver(this, topic); 36 } 37 this.observerItems = []; 38 } 39 40 if (this.currentObserverStatus) { 41 let error = new Error(this.currentObserverStatus); 42 this.currentObserverStatus = ""; 43 throw error; 44 } 45 } 46 47 observeTopic(topic, count, filterFn, callbackResolver) { 48 // If the topic is in the list already, assume that it came from a 49 // startObservingTopics call. If it isn't in the list already, assume 50 // that it isn't within a start/stop set and the observer has to be 51 // removed afterwards. 52 let removeObserver = false; 53 let index = this.observerItems.findIndex(item => item.topic == topic); 54 if (index == -1) { 55 removeObserver = true; 56 this.startObservingTopics([topic]); 57 } 58 59 for (let item of this.observerItems) { 60 if (item.topic == topic) { 61 item.count = count || 1; 62 item.filterFn = filterFn; 63 item.promiseResolver = () => { 64 if (removeObserver) { 65 this.stopObservingTopics([topic]); 66 } 67 callbackResolver(); 68 }; 69 break; 70 } 71 } 72 } 73 74 observe(aSubject, aTopic, aData) { 75 for (let item of this.observerItems) { 76 if (item.topic != aTopic) { 77 continue; 78 } 79 if (item.filterFn && !item.filterFn(aSubject, aTopic, aData)) { 80 break; 81 } 82 83 if (--item.count >= 0) { 84 if (item.count == 0 && item.promiseResolver) { 85 item.promiseResolver(); 86 } 87 return; 88 } 89 } 90 91 // Otherwise, if the observer doesn't match, fail. 92 console.log( 93 "Failed: Observer topic " + aTopic + " not expected in content process" 94 ); 95 this.currentObserverStatus += 96 "Topic " + aTopic + " not expected in content process\n"; 97 } 98 } 99 100 BrowserTestUtilsChildObserver.prototype.QueryInterface = ChromeUtils.generateQI( 101 ["nsIObserver", "nsISupportsWeakReference"] 102 ); 103 104 export class BrowserTestUtilsChild extends JSWindowActorChild { 105 actorCreated() { 106 this._EventUtils = null; 107 } 108 109 get EventUtils() { 110 if (!this._EventUtils) { 111 // Set up a dummy environment so that EventUtils works. We need to be careful to 112 // pass a window object into each EventUtils method we call rather than having 113 // it rely on the |window| global. 114 let win = this.contentWindow; 115 let EventUtils = { 116 get KeyboardEvent() { 117 return win.KeyboardEvent; 118 }, 119 // EventUtils' `sendChar` function relies on the navigator to synthetize events. 120 get navigator() { 121 return win.navigator; 122 }, 123 }; 124 125 EventUtils.window = {}; 126 EventUtils.parent = EventUtils.window; 127 EventUtils._EU_Ci = Ci; 128 EventUtils._EU_Cc = Cc; 129 130 Services.scriptloader.loadSubScript( 131 "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", 132 EventUtils 133 ); 134 135 this._EventUtils = EventUtils; 136 } 137 138 return this._EventUtils; 139 } 140 141 receiveMessage(aMessage) { 142 switch (aMessage.name) { 143 case "Test:SynthesizeMouse": { 144 return this.synthesizeMouse(aMessage.data, this.contentWindow); 145 } 146 147 case "Test:SynthesizeTouch": { 148 return this.synthesizeTouch(aMessage.data, this.contentWindow); 149 } 150 151 case "Test:SendChar": { 152 return this.EventUtils.sendChar(aMessage.data.char, this.contentWindow); 153 } 154 155 case "Test:SynthesizeKey": 156 this.EventUtils.synthesizeKey( 157 aMessage.data.key, 158 aMessage.data.event || {}, 159 this.contentWindow 160 ); 161 break; 162 163 case "Test:SynthesizeComposition": { 164 return this.EventUtils.synthesizeComposition( 165 aMessage.data.event, 166 this.contentWindow 167 ); 168 } 169 170 case "Test:SynthesizeCompositionChange": 171 this.EventUtils.synthesizeCompositionChange( 172 aMessage.data.event, 173 this.contentWindow 174 ); 175 break; 176 177 case "BrowserTestUtils:StartObservingTopics": { 178 this.observer = new BrowserTestUtilsChildObserver(); 179 this.observer.startObservingTopics(aMessage.data.topics); 180 break; 181 } 182 183 case "BrowserTestUtils:StopObservingTopics": { 184 if (this.observer) { 185 this.observer.stopObservingTopics(aMessage.data.topics); 186 this.observer = null; 187 } 188 break; 189 } 190 191 case "BrowserTestUtils:ObserveTopic": { 192 return new Promise(resolve => { 193 let filterFn; 194 if (aMessage.data.filterFunctionSource) { 195 // eslint-disable-next-line mozilla/reject-globalThis-modification 196 let sb = Cu.Sandbox(globalThis, { 197 sandboxPrototype: { 198 __proto__: globalThis, 199 content: this.contentWindow, 200 }, 201 }); 202 filterFn = Cu.evalInSandbox( 203 `(() => (${aMessage.data.filterFunctionSource}))()`, 204 sb 205 ); 206 } 207 208 let observer = this.observer || new BrowserTestUtilsChildObserver(); 209 observer.observeTopic( 210 aMessage.data.topic, 211 aMessage.data.count, 212 filterFn, 213 resolve 214 ); 215 }); 216 } 217 218 case "BrowserTestUtils:CrashFrame": { 219 // This is to intentionally crash the frame. 220 // We crash by using js-ctypes. The crash 221 // should happen immediately 222 // upon loading this frame script. 223 224 const { ctypes } = ChromeUtils.importESModule( 225 "resource://gre/modules/ctypes.sys.mjs" 226 ); 227 228 let dies = function () { 229 dump("\nEt tu, Brute?\n"); 230 ChromeUtils.privateNoteIntentionalCrash(); 231 232 try { 233 // Annotate test failure to allow callers to separate intentional 234 // crashes from unintentional crashes. 235 Services.appinfo.annotateCrashReport("TestKey", "CrashFrame"); 236 } catch (e) { 237 dump(`Failed to annotate crash in CrashFrame: ${e}\n`); 238 } 239 240 switch (aMessage.data.crashType) { 241 case "CRASH_OOM": { 242 let debug = Cc["@mozilla.org/xpcom/debug;1"].getService( 243 Ci.nsIDebug2 244 ); 245 debug.crashWithOOM(); 246 break; 247 } 248 case "CRASH_SYSCALL": { 249 if (Services.appinfo.OS == "Linux") { 250 let libc = ctypes.open("libc.so.6"); 251 let chroot = libc.declare( 252 "chroot", 253 ctypes.default_abi, 254 ctypes.int, 255 ctypes.char.ptr 256 ); 257 chroot("/"); 258 } 259 break; 260 } 261 case "CRASH_INVALID_POINTER_DEREF": // Fallthrough 262 default: { 263 // Dereference a bad pointer. 264 let zero = new ctypes.intptr_t(8); 265 let badptr = ctypes.cast( 266 zero, 267 ctypes.PointerType(ctypes.int32_t) 268 ); 269 badptr.contents; 270 } 271 } 272 }; 273 274 if (aMessage.data.asyncCrash) { 275 let { setTimeout } = ChromeUtils.importESModule( 276 "resource://gre/modules/Timer.sys.mjs" 277 ); 278 // Get out of the stack. 279 setTimeout(dies, 0); 280 } else { 281 dies(); 282 } 283 } 284 } 285 286 return undefined; 287 } 288 289 handleEvent(aEvent) { 290 switch (aEvent.type) { 291 case "DOMContentLoaded": 292 case "load": { 293 this.sendAsyncMessage(aEvent.type, { 294 internalURL: aEvent.target.documentURI, 295 visibleURL: aEvent.target.location 296 ? aEvent.target.location.href 297 : null, 298 }); 299 break; 300 } 301 } 302 } 303 304 #getTarget(data, window) { 305 let { target, targetFn } = data; 306 if (typeof target == "string") { 307 return this.document.querySelector(target); 308 } 309 if (typeof targetFn == "string") { 310 let sb = Cu.Sandbox(window, { sandboxPrototype: window }); 311 return Cu.evalInSandbox(`(${targetFn})()`, sb); 312 } 313 return null; 314 } 315 316 synthesizeMouse(data, window) { 317 let target = this.#getTarget(data, window); 318 319 let left = data.x; 320 let top = data.y; 321 if (target) { 322 if (target.ownerDocument !== this.document) { 323 // Account for nodes found in iframes. 324 let cur = target; 325 do { 326 // eslint-disable-next-line mozilla/use-ownerGlobal 327 let frame = cur.ownerDocument.defaultView.frameElement; 328 let rect = frame.getBoundingClientRect(); 329 330 left += rect.left; 331 top += rect.top; 332 333 cur = frame; 334 } while (cur && cur.ownerDocument !== this.document); 335 336 // node must be in this document tree. 337 if (!cur) { 338 throw new Error("target must be in the main document tree"); 339 } 340 } 341 342 let rect = target.getBoundingClientRect(); 343 left += rect.left; 344 top += rect.top; 345 346 if (data.event.centered) { 347 left += rect.width / 2; 348 top += rect.height / 2; 349 } 350 } 351 352 let result; 353 354 lazy.E10SUtils.wrapHandlingUserInput(window, data.handlingUserInput, () => { 355 if (data.event && data.event.wheel) { 356 this.EventUtils.synthesizeWheelAtPoint(left, top, data.event, window); 357 } else { 358 result = this.EventUtils.synthesizeMouseAtPoint( 359 left, 360 top, 361 data.event, 362 window 363 ); 364 } 365 }); 366 367 return result; 368 } 369 370 synthesizeTouch(data, window) { 371 let target = this.#getTarget(data, window); 372 373 if (target) { 374 if (target.ownerDocument !== this.document) { 375 // Account for nodes found in iframes. 376 let cur = target; 377 do { 378 cur = cur.ownerGlobal.frameElement; 379 } while (cur && cur.ownerDocument !== this.document); 380 381 // node must be in this document tree. 382 if (!cur) { 383 throw new Error("target must be in the main document tree"); 384 } 385 } 386 } 387 388 return this.EventUtils.synthesizeTouch( 389 target, 390 data.x, 391 data.y, 392 data.event, 393 window 394 ); 395 } 396 }