399 lines
11 KiB
JavaScript
399 lines
11 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/. */
|
|
|
|
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
|
|
);
|
|
}
|
|
}
|