diff options
Diffstat (limited to 'testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs')
-rw-r--r-- | testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs | 399 |
1 files changed, 399 insertions, 0 deletions
diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs b/testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs new file mode 100644 index 0000000000..9d63388471 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs @@ -0,0 +1,399 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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 lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", +}); + +class BrowserTestUtilsChildObserver { + constructor() { + this.currentObserverStatus = ""; + this.observerItems = []; + } + + startObservingTopics(aTopics) { + for (let topic of aTopics) { + Services.obs.addObserver(this, topic); + this.observerItems.push({ topic }); + } + } + + stopObservingTopics(aTopics) { + if (aTopics) { + for (let topic of aTopics) { + let index = this.observerItems.findIndex(item => item.topic == topic); + if (index >= 0) { + Services.obs.removeObserver(this, topic); + this.observerItems.splice(index, 1); + } + } + } else { + for (let topic of this.observerItems) { + Services.obs.removeObserver(this, topic); + } + this.observerItems = []; + } + + if (this.currentObserverStatus) { + let error = new Error(this.currentObserverStatus); + this.currentObserverStatus = ""; + throw error; + } + } + + observeTopic(topic, count, filterFn, callbackResolver) { + // If the topic is in the list already, assume that it came from a + // startObservingTopics call. If it isn't in the list already, assume + // that it isn't within a start/stop set and the observer has to be + // removed afterwards. + let removeObserver = false; + let index = this.observerItems.findIndex(item => item.topic == topic); + if (index == -1) { + removeObserver = true; + this.startObservingTopics([topic]); + } + + for (let item of this.observerItems) { + if (item.topic == topic) { + item.count = count || 1; + item.filterFn = filterFn; + item.promiseResolver = () => { + if (removeObserver) { + this.stopObservingTopics([topic]); + } + callbackResolver(); + }; + break; + } + } + } + + observe(aSubject, aTopic, aData) { + for (let item of this.observerItems) { + if (item.topic != aTopic) { + continue; + } + if (item.filterFn && !item.filterFn(aSubject, aTopic, aData)) { + break; + } + + if (--item.count >= 0) { + if (item.count == 0 && item.promiseResolver) { + item.promiseResolver(); + } + return; + } + } + + // Otherwise, if the observer doesn't match, fail. + console.log( + "Failed: Observer topic " + aTopic + " not expected in content process" + ); + this.currentObserverStatus += + "Topic " + aTopic + " not expected in content process\n"; + } +} + +BrowserTestUtilsChildObserver.prototype.QueryInterface = ChromeUtils.generateQI( + ["nsIObserver", "nsISupportsWeakReference"] +); + +export class BrowserTestUtilsChild extends JSWindowActorChild { + actorCreated() { + this._EventUtils = null; + } + + get EventUtils() { + if (!this._EventUtils) { + // Set up a dummy environment so that EventUtils works. We need to be careful to + // pass a window object into each EventUtils method we call rather than having + // it rely on the |window| global. + let win = this.contentWindow; + let EventUtils = { + get KeyboardEvent() { + return win.KeyboardEvent; + }, + // EventUtils' `sendChar` function relies on the navigator to synthetize events. + get navigator() { + return win.navigator; + }, + }; + + EventUtils.window = {}; + EventUtils.parent = EventUtils.window; + EventUtils._EU_Ci = Ci; + EventUtils._EU_Cc = Cc; + + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils + ); + + this._EventUtils = EventUtils; + } + + return this._EventUtils; + } + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "Test:SynthesizeMouse": { + return this.synthesizeMouse(aMessage.data, this.contentWindow); + } + + case "Test:SynthesizeTouch": { + return this.synthesizeTouch(aMessage.data, this.contentWindow); + } + + case "Test:SendChar": { + return this.EventUtils.sendChar(aMessage.data.char, this.contentWindow); + } + + case "Test:SynthesizeKey": + this.EventUtils.synthesizeKey( + aMessage.data.key, + aMessage.data.event || {}, + this.contentWindow + ); + break; + + case "Test:SynthesizeComposition": { + return this.EventUtils.synthesizeComposition( + aMessage.data.event, + this.contentWindow + ); + } + + case "Test:SynthesizeCompositionChange": + this.EventUtils.synthesizeCompositionChange( + aMessage.data.event, + this.contentWindow + ); + break; + + case "BrowserTestUtils:StartObservingTopics": { + this.observer = new BrowserTestUtilsChildObserver(); + this.observer.startObservingTopics(aMessage.data.topics); + break; + } + + case "BrowserTestUtils:StopObservingTopics": { + if (this.observer) { + this.observer.stopObservingTopics(aMessage.data.topics); + this.observer = null; + } + break; + } + + case "BrowserTestUtils:ObserveTopic": { + return new Promise(resolve => { + let filterFn; + if (aMessage.data.filterFunctionSource) { + /* eslint-disable-next-line no-eval */ + filterFn = eval( + `(() => (${aMessage.data.filterFunctionSource}))()` + ); + } + + let observer = this.observer || new BrowserTestUtilsChildObserver(); + observer.observeTopic( + aMessage.data.topic, + aMessage.data.count, + filterFn, + resolve + ); + }); + } + + case "BrowserTestUtils:CrashFrame": { + // This is to intentionally crash the frame. + // We crash by using js-ctypes. The crash + // should happen immediately + // upon loading this frame script. + + const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + let dies = function () { + dump("\nEt tu, Brute?\n"); + ChromeUtils.privateNoteIntentionalCrash(); + + try { + // Annotate test failure to allow callers to separate intentional + // crashes from unintentional crashes. + Services.appinfo.annotateCrashReport("TestKey", "CrashFrame"); + } catch (e) { + dump(`Failed to annotate crash in CrashFrame: ${e}\n`); + } + + switch (aMessage.data.crashType) { + case "CRASH_OOM": { + let debug = Cc["@mozilla.org/xpcom/debug;1"].getService( + Ci.nsIDebug2 + ); + debug.crashWithOOM(); + break; + } + case "CRASH_SYSCALL": { + if (Services.appinfo.OS == "Linux") { + let libc = ctypes.open("libc.so.6"); + let chroot = libc.declare( + "chroot", + ctypes.default_abi, + ctypes.int, + ctypes.char.ptr + ); + chroot("/"); + } + break; + } + case "CRASH_INVALID_POINTER_DEREF": // Fallthrough + default: { + // Dereference a bad pointer. + let zero = new ctypes.intptr_t(8); + let badptr = ctypes.cast( + zero, + ctypes.PointerType(ctypes.int32_t) + ); + badptr.contents; + } + } + }; + + if (aMessage.data.asyncCrash) { + let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" + ); + // Get out of the stack. + setTimeout(dies, 0); + } else { + dies(); + } + } + } + + return undefined; + } + + handleEvent(aEvent) { + switch (aEvent.type) { + case "DOMContentLoaded": + case "load": { + this.sendAsyncMessage(aEvent.type, { + internalURL: aEvent.target.documentURI, + visibleURL: aEvent.target.location + ? aEvent.target.location.href + : null, + }); + break; + } + } + } + + synthesizeMouse(data, window) { + let target = data.target; + if (typeof target == "string") { + target = this.document.querySelector(target); + } else if (typeof data.targetFn == "string") { + let runnablestr = ` + (() => { + return (${data.targetFn}); + })();`; + /* eslint-disable no-eval */ + target = eval(runnablestr)(); + /* eslint-enable no-eval */ + } + + let left = data.x; + let top = data.y; + if (target) { + if (target.ownerDocument !== this.document) { + // Account for nodes found in iframes. + let cur = target; + do { + // eslint-disable-next-line mozilla/use-ownerGlobal + let frame = cur.ownerDocument.defaultView.frameElement; + let rect = frame.getBoundingClientRect(); + + left += rect.left; + top += rect.top; + + cur = frame; + } while (cur && cur.ownerDocument !== this.document); + + // node must be in this document tree. + if (!cur) { + throw new Error("target must be in the main document tree"); + } + } + + let rect = target.getBoundingClientRect(); + left += rect.left; + top += rect.top; + + if (data.event.centered) { + left += rect.width / 2; + top += rect.height / 2; + } + } + + let result; + + lazy.E10SUtils.wrapHandlingUserInput(window, data.handlingUserInput, () => { + if (data.event && data.event.wheel) { + this.EventUtils.synthesizeWheelAtPoint(left, top, data.event, window); + } else { + result = this.EventUtils.synthesizeMouseAtPoint( + left, + top, + data.event, + window + ); + } + }); + + return result; + } + + synthesizeTouch(data, window) { + let target = data.target; + if (typeof target == "string") { + target = this.document.querySelector(target); + } else if (typeof data.targetFn == "string") { + let runnablestr = ` + (() => { + return (${data.targetFn}); + })();`; + /* eslint-disable no-eval */ + target = eval(runnablestr)(); + /* eslint-enable no-eval */ + } + + if (target) { + if (target.ownerDocument !== this.document) { + // Account for nodes found in iframes. + let cur = target; + do { + cur = cur.ownerGlobal.frameElement; + } while (cur && cur.ownerDocument !== this.document); + + // node must be in this document tree. + if (!cur) { + throw new Error("target must be in the main document tree"); + } + } + } + + return this.EventUtils.synthesizeTouch( + target, + data.x, + data.y, + data.event, + window + ); + } +} |