summaryrefslogtreecommitdiffstats
path: root/testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs')
-rw-r--r--testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs399
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
+ );
+ }
+}