Source code

Revision control

Copy as Markdown

Other Tools

/* 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(
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
);
}
}