gecko-dev/testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.jsm
2019-10-29 16:23:03 +00:00

348 lines
9.3 KiB
JavaScript

/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["BrowserTestUtilsChild"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
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(
[Ci.nsIObserver, Ci.nsISupportsWeakReference]
);
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 and dereferencing
// a bad pointer. The crash should happen immediately
// upon loading this frame script.
const { ctypes } = ChromeUtils.import(
"resource://gre/modules/ctypes.jsm"
);
let dies = function() {
ChromeUtils.privateNoteIntentionalCrash();
let zero = new ctypes.intptr_t(8);
let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
badptr.contents;
};
dump("\nEt tu, Brute?\n");
dies();
}
}
return undefined;
}
handleEvent(aEvent) {
switch (aEvent.type) {
case "DOMContentLoaded":
case "load": {
this.sendAsyncMessage(aEvent.type, {
internalURL: aEvent.target.documentURI,
visibleURL: aEvent.target.location.href,
});
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 {
let frame = cur.ownerGlobal.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;
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
);
}
}