Bug 1614738, convert ContentSearch to be a JSWindowActor, r=adw

Differential Revision: https://phabricator.services.mozilla.com/D68237

--HG--
rename : browser/modules/ContentSearch.jsm => browser/actors/ContentSearchParent.jsm
extra : moz-landing-system : lando
This commit is contained in:
Neil Deakin 2020-04-11 23:10:45 +00:00
parent 76b0d3e957
commit 1351306d5f
11 changed files with 294 additions and 297 deletions

View File

@ -6,24 +6,19 @@
var EXPORTED_SYMBOLS = ["ContentSearchChild"];
const { ActorChild } = ChromeUtils.import(
"resource://gre/modules/ActorChild.jsm"
);
class ContentSearchChild extends ActorChild {
class ContentSearchChild extends JSWindowActorChild {
handleEvent(event) {
this._sendMsg(event.detail.type, event.detail.data);
// The event gets translated into a message that
// is then sent to the parent.
if (event.type == "ContentSearchClient") {
this.sendAsyncMessage(event.detail.type, event.detail.data);
}
}
receiveMessage(msg) {
this._fireEvent(msg.data.type, msg.data.data);
}
_sendMsg(type, data = null) {
this.mm.sendAsyncMessage("ContentSearch", {
type,
data,
});
// The message gets translated into an event that
// is then sent to the content.
this._fireEvent(msg.name, msg.data);
}
_fireEvent(type, data = null) {
@ -34,10 +29,10 @@ class ContentSearchChild extends ActorChild {
data,
},
},
this.content
this.contentWindow
);
this.content.dispatchEvent(
new this.content.CustomEvent("ContentSearchService", event)
this.contentWindow.dispatchEvent(
new this.contentWindow.CustomEvent("ContentSearchService", event)
);
}
}

View File

@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["ContentSearch"];
var EXPORTED_SYMBOLS = ["ContentSearchParent", "ContentSearch"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
@ -28,18 +28,13 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/SearchSuggestionController.jsm"
);
const INBOUND_MESSAGE = "ContentSearch";
const OUTBOUND_MESSAGE = INBOUND_MESSAGE;
const MAX_LOCAL_SUGGESTIONS = 3;
const MAX_SUGGESTIONS = 6;
// Set of all ContentSearch actors, used to broadcast messages to all of them.
let gContentSearchActors = new Set();
/**
* ContentSearch receives messages named INBOUND_MESSAGE and sends messages
* named OUTBOUND_MESSAGE. The data of each message is expected to look like
* { type, data }. type is the message's type (or subtype if you consider the
* type of the message itself to be INBOUND_MESSAGE), and data is data that is
* specific to the type.
*
* Inbound messages have the following types:
*
* AddFormHistoryEntry
@ -95,7 +90,9 @@ const MAX_SUGGESTIONS = 6;
* data: null
*/
var ContentSearch = {
let ContentSearch = {
initialized: false,
// Inbound events are queued and processed in FIFO order instead of handling
// them immediately, which would result in non-FIFO responses due to the
// asynchrononicity added by converting image data URIs to ArrayBuffers.
@ -114,13 +111,17 @@ var ContentSearch = {
_currentSuggestion: null,
init() {
Services.obs.addObserver(this, "browser-search-engine-modified");
Services.obs.addObserver(this, "browser-search-service");
Services.obs.addObserver(this, "shutdown-leaks-before-check");
Services.prefs.addObserver("browser.search.hiddenOneOffs", this);
this._stringBundle = Services.strings.createBundle(
"chrome://global/locale/autocomplete.properties"
);
if (!this.initialized) {
Services.obs.addObserver(this, "browser-search-engine-modified");
Services.obs.addObserver(this, "browser-search-service");
Services.obs.addObserver(this, "shutdown-leaks-before-check");
Services.prefs.addObserver("browser.search.hiddenOneOffs", this);
this._stringBundle = Services.strings.createBundle(
"chrome://global/locale/autocomplete.properties"
);
this.initialized = true;
}
},
get searchSuggestionUIStrings() {
@ -147,6 +148,10 @@ var ContentSearch = {
},
destroy() {
if (!this.initialized) {
return new Promise();
}
if (this._destroyedPromise) {
return this._destroyedPromise;
}
@ -160,48 +165,6 @@ var ContentSearch = {
return this._destroyedPromise;
},
/**
* Focuses the search input in the page with the given message manager.
* @param messageManager
* The MessageManager object of the selected browser.
*/
focusInput(messageManager) {
messageManager.sendAsyncMessage(OUTBOUND_MESSAGE, {
type: "FocusInput",
});
},
// Listeners and observers are added in BrowserGlue.jsm
receiveMessage(msg) {
// Add a temporary event handler that exists only while the message is in
// the event queue. If the message's source docshell changes browsers in
// the meantime, then we need to update msg.target. event.detail will be
// the docshell's new parent <xul:browser> element.
msg.handleEvent = event => {
let browserData = this._suggestionMap.get(msg.target);
if (browserData) {
this._suggestionMap.delete(msg.target);
this._suggestionMap.set(event.detail, browserData);
}
msg.target.removeEventListener("SwapDocShells", msg, true);
msg.target = event.detail;
msg.target.addEventListener("SwapDocShells", msg, true);
};
msg.target.addEventListener("SwapDocShells", msg, true);
// Search requests cause cancellation of all Suggestion requests from the
// same browser.
if (msg.data.type === "Search") {
this._cancelSuggestions(msg);
}
this._eventQueue.push({
type: "Message",
data: msg,
});
this._processEventQueue();
},
observe(subj, topic, data) {
switch (topic) {
case "browser-search-service":
@ -226,8 +189,8 @@ var ContentSearch = {
}
},
removeFormHistoryEntry(msg, entry) {
let browserData = this._suggestionDataForBrowser(msg.target);
removeFormHistoryEntry(browser, entry) {
let browserData = this._suggestionDataForBrowser(browser);
if (browserData && browserData.previousFormHistoryResult) {
let { previousFormHistoryResult } = browserData;
for (let i = 0; i < previousFormHistoryResult.matchCount; i++) {
@ -239,7 +202,7 @@ var ContentSearch = {
}
},
performSearch(msg, data) {
performSearch(browser, data) {
this._ensureDataHasProperties(data, [
"engineName",
"searchString",
@ -252,7 +215,6 @@ var ContentSearch = {
"",
data.searchPurpose
);
let browser = msg.target;
let win = browser.ownerGlobal;
if (!win) {
// The browser may have been closed between the time its content sent the
@ -269,7 +231,7 @@ var ContentSearch = {
if (where === "current") {
// Since we're going to load the search in the same browser, blur the search
// UI to prevent further interaction before we start loading.
this._reply(msg, "Blur");
this._reply(browser, "Blur");
browser.loadURI(submission.uri.spec, {
postData: submission.postData,
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
@ -308,7 +270,7 @@ var ContentSearch = {
let priv = PrivateBrowsingUtils.isBrowserPrivate(browser);
// fetch() rejects its promise if there's a pending request, but since we
// process our event queue serially, there's never a pending request.
this._currentSuggestion = { controller, target: browser };
this._currentSuggestion = { controller, browser };
let suggestions = await controller.fetch(searchString, priv, engine);
this._currentSuggestion = null;
@ -339,14 +301,14 @@ var ContentSearch = {
// isBrowserPrivate assumes that the passed-in browser has all the normal
// properties, which won't be true if the browser has been destroyed.
// That may be the case here due to the asynchronous nature of messaging.
isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser.target);
isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
} catch (err) {
return false;
}
if (isPrivate || entry === "") {
return false;
}
let browserData = this._suggestionDataForBrowser(browser.target, true);
let browserData = this._suggestionDataForBrowser(browser, true);
FormHistory.update(
{
op: "bump",
@ -402,60 +364,59 @@ var ContentSearch = {
this._currentEventPromise = (async () => {
try {
await this["_on" + event.type](event.data);
await this["_on" + event.type](event);
} catch (err) {
Cu.reportError(err);
} finally {
this._currentEventPromise = null;
this._processEventQueue();
}
})();
},
_cancelSuggestions(msg) {
_cancelSuggestions(browser) {
let cancelled = false;
// cancel active suggestion request
if (
this._currentSuggestion &&
this._currentSuggestion.target === msg.target
this._currentSuggestion.browser === browser
) {
this._currentSuggestion.controller.stop();
cancelled = true;
}
// cancel queued suggestion requests
for (let i = 0; i < this._eventQueue.length; i++) {
let m = this._eventQueue[i].data;
if (msg.target === m.target && m.data.type === "GetSuggestions") {
let m = this._eventQueue[i];
if (browser === m.browser && m.name === "GetSuggestions") {
this._eventQueue.splice(i, 1);
cancelled = true;
i--;
}
}
if (cancelled) {
this._reply(msg, "SuggestionsCancelled");
this._reply(browser, "SuggestionsCancelled");
}
},
async _onMessage(msg) {
let methodName = "_onMessage" + msg.data.type;
async _onMessage(eventItem) {
let methodName = "_onMessage" + eventItem.name;
if (methodName in this) {
await this._initService();
await this[methodName](msg, msg.data.data);
if (!Cu.isDeadWrapper(msg.target)) {
msg.target.removeEventListener("SwapDocShells", msg, true);
}
await this[methodName](eventItem.browser, eventItem.data);
eventItem.browser.removeEventListener("SwapDocShells", eventItem, true);
}
},
_onMessageGetState(msg, data) {
return this.currentStateObj(msg.target.ownerGlobal).then(state => {
this._reply(msg, "State", state);
_onMessageGetState(browser, data) {
return this.currentStateObj(browser.ownerGlobal).then(state => {
this._reply(browser, "State", state);
});
},
_onMessageGetEngine(msg, data) {
return this.currentStateObj(msg.target.ownerGlobal).then(state => {
this._reply(msg, "Engine", {
_onMessageGetEngine(browser, data) {
return this.currentStateObj(browser.ownerGlobal).then(state => {
this._reply(browser, "Engine", {
isPrivateWindow: state.isPrivateWindow,
engine: state.isPrivateWindow
? state.currentPrivateEngine
@ -464,32 +425,32 @@ var ContentSearch = {
});
},
_onMessageGetStrings(msg, data) {
this._reply(msg, "Strings", this.searchSuggestionUIStrings);
_onMessageGetStrings(browser, data) {
this._reply(browser, "Strings", this.searchSuggestionUIStrings);
},
_onMessageSearch(msg, data) {
this.performSearch(msg, data);
_onMessageSearch(browser, data) {
this.performSearch(browser, data);
},
_onMessageSetCurrentEngine(msg, data) {
_onMessageSetCurrentEngine(browser, data) {
Services.search.defaultEngine = Services.search.getEngineByName(data);
},
_onMessageManageEngines(msg) {
msg.target.ownerGlobal.openPreferences("paneSearch");
_onMessageManageEngines(browser) {
browser.ownerGlobal.openPreferences("paneSearch");
},
async _onMessageGetSuggestions(msg, data) {
async _onMessageGetSuggestions(browser, data) {
this._ensureDataHasProperties(data, ["engineName", "searchString"]);
let { engineName, searchString } = data;
let suggestions = await this.getSuggestions(
engineName,
searchString,
msg.target
browser
);
this._reply(msg, "Suggestions", {
this._reply(browser, "Suggestions", {
engineName: data.engineName,
searchString: suggestions.term,
formHistory: suggestions.local,
@ -497,32 +458,32 @@ var ContentSearch = {
});
},
async _onMessageAddFormHistoryEntry(msg, entry) {
await this.addFormHistoryEntry(msg, entry);
async _onMessageAddFormHistoryEntry(browser, entry) {
await this.addFormHistoryEntry(browser, entry);
},
_onMessageRemoveFormHistoryEntry(msg, entry) {
this.removeFormHistoryEntry(msg, entry);
_onMessageRemoveFormHistoryEntry(browser, entry) {
this.removeFormHistoryEntry(browser, entry);
},
_onMessageSpeculativeConnect(msg, engineName) {
_onMessageSpeculativeConnect(browser, engineName) {
let engine = Services.search.getEngineByName(engineName);
if (!engine) {
throw new Error("Unknown engine name: " + engineName);
}
if (msg.target.contentWindow) {
if (browser.contentWindow) {
engine.speculativeConnect({
window: msg.target.contentWindow,
originAttributes: msg.target.contentPrincipal.originAttributes,
window: browser.contentWindow,
originAttributes: browser.contentPrincipal.originAttributes,
});
}
},
async _onObserve(data) {
if (data === "engine-default") {
async _onObserve(eventItem) {
if (eventItem.data === "engine-default") {
let engine = await this._currentEngineObj(false);
this._broadcast("CurrentEngine", engine);
} else if (data === "engine-default-private") {
} else if (eventItem.data === "engine-default-private") {
let engine = await this._currentEngineObj(true);
this._broadcast("CurrentPrivateEngine", engine);
} else {
@ -545,26 +506,14 @@ var ContentSearch = {
return data;
},
_reply(msg, type, data) {
// We reply asyncly to messages, and by the time we reply the browser we're
// responding to may have been destroyed. messageManager is null then.
if (!Cu.isDeadWrapper(msg.target) && msg.target.messageManager) {
msg.target.messageManager.sendAsyncMessage(...this._msgArgs(type, data));
}
_reply(browser, type, data) {
browser.sendMessageToActor(type, data, "ContentSearch");
},
_broadcast(type, data) {
Services.mm.broadcastAsyncMessage(...this._msgArgs(type, data));
},
_msgArgs(type, data) {
return [
OUTBOUND_MESSAGE,
{
type,
data,
},
];
for (let actor of gContentSearchActors) {
actor.sendAsyncMessage(type, data);
}
},
async _currentEngineObj(usePrivate) {
@ -633,3 +582,49 @@ var ContentSearch = {
return this._initServicePromise;
},
};
class ContentSearchParent extends JSWindowActorParent {
constructor() {
super();
ContentSearch.init();
gContentSearchActors.add(this);
}
didDestroy() {
gContentSearchActors.delete(this);
}
receiveMessage(msg) {
// Add a temporary event handler that exists only while the message is in
// the event queue. If the message's source docshell changes browsers in
// the meantime, then we need to update the browser. event.detail will be
// the docshell's new parent <xul:browser> element.
let browser = this.browsingContext.top.embedderElement;
let eventItem = {
type: "Message",
name: msg.name,
data: msg.data,
browser,
handleEvent: event => {
let browserData = ContentSearch._suggestionMap.get(eventItem.browser);
if (browserData) {
ContentSearch._suggestionMap.delete(eventItem.browser);
ContentSearch._suggestionMap.set(event.detail, browserData);
}
browser.removeEventListener("SwapDocShells", eventItem, true);
eventItem.browser = event.detail;
eventItem.browser.addEventListener("SwapDocShells", eventItem, true);
},
};
browser.addEventListener("SwapDocShells", eventItem, true);
// Search requests cause cancellation of all Suggestion requests from the
// same browser.
if (msg.name === "Search") {
ContentSearch._cancelSuggestions();
}
ContentSearch._eventQueue.push(eventItem);
ContentSearch._processEventQueue();
}
}

View File

@ -7,6 +7,9 @@
with Files("**"):
BUG_COMPONENT = ("Firefox", "General")
with Files("ContentSearch*.jsm"):
BUG_COMPONENT = ("Firefox", "Search")
with Files("LightweightThemeChild.jsm"):
BUG_COMPONENT = ("WebExtensions", "Themes")
@ -33,6 +36,7 @@ FINAL_TARGET_FILES.actors += [
'ContentMetaChild.jsm',
'ContentMetaParent.jsm',
'ContentSearchChild.jsm',
'ContentSearchParent.jsm',
'ContextMenuChild.jsm',
'ContextMenuParent.jsm',
'DOMFullscreenChild.jsm',

View File

@ -684,7 +684,7 @@ var gDidInitialSetUp = false;
async function setUp(aNoEngine) {
if (!gDidInitialSetUp) {
var { ContentSearch } = ChromeUtils.import(
"resource:///modules/ContentSearch.jsm"
"resource:///actors/ContentSearchParent.jsm"
);
let originalOnMessageSearch = ContentSearch._onMessageSearch;
let originalOnMessageManageEngines = ContentSearch._onMessageManageEngines;

View File

@ -187,16 +187,13 @@
}
function waitForContentSearchEvent(messageType, cb) {
let mm = content.SpecialPowers.Cc[
"@mozilla.org/globalmessagemanager;1"
].getService();
mm.addMessageListener("ContentSearch", function listener(aMsg) {
if (aMsg.data.type != messageType) {
return;
function listener(event) {
if (event.detail.type == messageType) {
removeEventListener("ContentSearchClient", listener, true);
cb(event.detail.data);
}
mm.removeMessageListener("ContentSearch", listener);
cb(aMsg.data.data);
});
}
addEventListener("ContentSearchClient", listener, true);
}
function currentState() {

View File

@ -154,6 +154,25 @@ let ACTORS = {
},
},
ContentSearch: {
parent: {
moduleURI: "resource:///actors/ContentSearchParent.jsm",
},
child: {
moduleURI: "resource:///actors/ContentSearchChild.jsm",
matches: [
"about:home",
"about:newtab",
"about:welcome",
"about:privatebrowsing",
"chrome://mochitests/content/*",
],
events: {
ContentSearchClient: { capture: true, wantUntrusted: true },
},
},
},
ContextMenu: {
parent: {
moduleURI: "resource:///actors/ContextMenuParent.jsm",
@ -396,24 +415,6 @@ let LEGACY_ACTORS = {
},
},
ContentSearch: {
child: {
module: "resource:///actors/ContentSearchChild.jsm",
group: "browsers",
matches: [
"about:home",
"about:newtab",
"about:welcome",
"about:privatebrowsing",
"chrome://mochitests/content/*",
],
events: {
ContentSearchClient: { capture: true, wantUntrusted: true },
},
messages: ["ContentSearch"],
},
},
URIFixup: {
child: {
module: "resource:///actors/URIFixupChild.jsm",
@ -644,7 +645,6 @@ let initializedModules = {};
"resource://gre/modules/ContentPrefServiceParent.jsm",
"alwaysInit",
],
["ContentSearch", "resource:///modules/ContentSearch.jsm", "init"],
["UpdateListener", "resource://gre/modules/UpdateListener.jsm", "init"],
].forEach(([name, resource, init]) => {
XPCOMUtils.defineLazyGetter(this, name, () => {
@ -723,7 +723,6 @@ const listeners = {
"AboutLogins:TestOnlyResetOSAuth": ["AboutLoginsParent"],
"AboutLogins:UpdateLogin": ["AboutLoginsParent"],
"AboutLogins:VulnerableLogins": ["AboutLoginsParent"],
ContentSearch: ["ContentSearch"],
"Reader:FaviconRequest": ["ReaderParent"],
"Reader:UpdateReaderButton": ["ReaderParent"],
},

View File

@ -58,9 +58,6 @@ with Files("*Telemetry.jsm"):
with Files("ContentCrashHandlers.jsm"):
BUG_COMPONENT = ("Toolkit", "Crash Reporting")
with Files("ContentSearch.jsm"):
BUG_COMPONENT = ("Firefox", "Search")
with Files("EveryWindow.jsm"):
BUG_COMPONENT = ("Firefox", "General")
@ -135,7 +132,6 @@ EXTRA_JS_MODULES += [
'BrowserWindowTracker.jsm',
'ContentCrashHandlers.jsm',
'ContentObservers.js',
'ContentSearch.jsm',
'Discovery.jsm',
'EveryWindow.jsm',
'ExtensionsUI.jsm',

View File

@ -8,7 +8,6 @@ prefs =
[browser_BrowserWindowTracker.js]
[browser_ContentSearch.js]
support-files =
contentSearch.js
contentSearchBadImage.xml
contentSearchSuggestions.sjs
contentSearchSuggestions.xml

View File

@ -2,9 +2,8 @@
* 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 TEST_MSG = "ContentSearchTest";
const CONTENT_SEARCH_MSG = "ContentSearch";
const TEST_CONTENT_SCRIPT_BASENAME = "contentSearch.js";
const SERVICE_EVENT_TYPE = "ContentSearchService";
const CLIENT_EVENT_TYPE = "ContentSearchClient";
/* import-globals-from ../../../components/search/test/browser/head.js */
Services.scriptloader.loadSubScript(
@ -15,6 +14,20 @@ Services.scriptloader.loadSubScript(
var arrayBufferIconTested = false;
var plainURIIconTested = false;
function sendEventToContent(browser, data) {
return SpecialPowers.spawn(
browser,
[CLIENT_EVENT_TYPE, data],
(eventName, eventData) => {
content.dispatchEvent(
new content.CustomEvent(eventName, {
detail: Cu.cloneInto(eventData, content),
})
);
}
);
}
add_task(async function setup() {
const originalEngine = await Services.search.getDefault();
const originalPrivateEngine = await Services.search.getDefaultPrivate();
@ -51,11 +64,12 @@ add_task(async function setup() {
});
add_task(async function GetState() {
let { mm } = await addTab();
mm.sendAsyncMessage(TEST_MSG, {
let { browser } = await addTab();
let statePromise = await waitForTestMsg(browser, "State");
sendEventToContent(browser, {
type: "GetState",
});
let msg = await waitForTestMsg(mm, "State");
let msg = await statePromise.donePromise;
checkMsg(msg, {
type: "State",
data: await currentStateObj(false),
@ -66,33 +80,35 @@ add_task(async function GetState() {
});
add_task(async function SetDefaultEngine() {
let { mm } = await addTab();
let { browser } = await addTab();
let newDefaultEngine = await Services.search.getEngineByName("FooChromeIcon");
let oldDefaultEngine = await Services.search.getDefault();
mm.sendAsyncMessage(TEST_MSG, {
let searchPromise = await waitForTestMsg(browser, "CurrentEngine");
sendEventToContent(browser, {
type: "SetCurrentEngine",
data: newDefaultEngine.name,
});
let deferred = PromiseUtils.defer();
Services.obs.addObserver(function obs(subj, topic, data) {
info("Test observed " + data);
if (data == "engine-default") {
ok(true, "Test observed engine-default");
Services.obs.removeObserver(obs, "browser-search-engine-modified");
deferred.resolve();
}
}, "browser-search-engine-modified");
let searchPromise = waitForTestMsg(mm, "CurrentEngine");
let deferredPromise = new Promise(resolve => {
Services.obs.addObserver(function obs(subj, topic, data) {
info("Test observed " + data);
if (data == "engine-default") {
ok(true, "Test observed engine-default");
Services.obs.removeObserver(obs, "browser-search-engine-modified");
resolve();
}
}, "browser-search-engine-modified");
});
info("Waiting for test to observe engine-default...");
await deferred.promise;
let msg = await searchPromise;
await deferredPromise;
let msg = await searchPromise.donePromise;
checkMsg(msg, {
type: "CurrentEngine",
data: await constructEngineObj(newDefaultEngine),
});
let enginePromise = await waitForTestMsg(browser, "CurrentEngine");
await Services.search.setDefault(oldDefaultEngine);
msg = await waitForTestMsg(mm, "CurrentEngine");
msg = await enginePromise.donePromise;
checkMsg(msg, {
type: "CurrentEngine",
data: await constructEngineObj(oldDefaultEngine),
@ -103,10 +119,10 @@ add_task(async function SetDefaultEngine() {
// as it doesn't need to, so we just test updating the default here.
add_task(async function setDefaultEnginePrivate() {
const engine = await Services.search.getEngineByName("FooChromeIcon");
const { mm } = await addTab();
let msgPromise = waitForTestMsg(mm, "CurrentPrivateEngine");
const { browser } = await addTab();
let enginePromise = await waitForTestMsg(browser, "CurrentPrivateEngine");
await Services.search.setDefaultPrivate(engine);
let msg = await msgPromise;
let msg = await enginePromise.donePromise;
checkMsg(msg, {
type: "CurrentPrivateEngine",
data: await constructEngineObj(engine),
@ -114,17 +130,19 @@ add_task(async function setDefaultEnginePrivate() {
});
add_task(async function modifyEngine() {
let { mm } = await addTab();
let { browser } = await addTab();
let engine = await Services.search.getDefault();
let oldAlias = engine.alias;
let statePromise = await waitForTestMsg(browser, "CurrentState");
engine.alias = "ContentSearchTest";
let msg = await waitForTestMsg(mm, "CurrentState");
let msg = await statePromise.donePromise;
checkMsg(msg, {
type: "CurrentState",
data: await currentStateObj(),
});
statePromise = await waitForTestMsg(browser, "CurrentState");
engine.alias = oldAlias;
msg = await waitForTestMsg(mm, "CurrentState");
msg = await statePromise.donePromise;
checkMsg(msg, {
type: "CurrentState",
data: await currentStateObj(),
@ -132,16 +150,18 @@ add_task(async function modifyEngine() {
});
add_task(async function test_hideEngine() {
let { mm } = await addTab();
let { browser } = await addTab();
let engine = await Services.search.getEngineByName("Foo \u2661");
let statePromise = await waitForTestMsg(browser, "CurrentState");
Services.prefs.setStringPref("browser.search.hiddenOneOffs", engine.name);
let msg = await waitForTestMsg(mm, "CurrentState");
let msg = await statePromise.donePromise;
checkMsg(msg, {
type: "CurrentState",
data: await currentStateObj(undefined, "Foo \u2661"),
});
statePromise = await waitForTestMsg(browser, "CurrentState");
Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
msg = await waitForTestMsg(mm, "CurrentState");
msg = await statePromise.donePromise;
checkMsg(msg, {
type: "CurrentState",
data: await currentStateObj(),
@ -188,11 +208,11 @@ add_task(async function searchInBackgroundTab() {
});
add_task(async function badImage() {
let { mm } = await addTab();
let { browser } = await addTab();
// If the bad image URI caused an exception to be thrown within ContentSearch,
// then we'll hang waiting for the CurrentState responses triggered by the new
// engine. That's what we're testing, and obviously it shouldn't happen.
let vals = await waitForNewEngine(mm, "contentSearchBadImage.xml", 1);
let vals = await waitForNewEngine(browser, "contentSearchBadImage.xml", 1);
let engine = vals[0];
let finalCurrentStateMsg = vals[vals.length - 1];
let expectedCurrentState = await currentStateObj();
@ -212,37 +232,43 @@ add_task(async function badImage() {
});
// Removing the engine triggers a final CurrentState message. Wait for it so
// it doesn't trip up subsequent tests.
let statePromise = await waitForTestMsg(browser, "CurrentState");
await Services.search.removeEngine(engine);
await waitForTestMsg(mm, "CurrentState");
await statePromise.donePromise;
});
add_task(
async function GetSuggestions_AddFormHistoryEntry_RemoveFormHistoryEntry() {
let { mm } = await addTab();
let { browser } = await addTab();
// Add the test engine that provides suggestions.
let vals = await waitForNewEngine(mm, "contentSearchSuggestions.xml", 0);
let vals = await waitForNewEngine(
browser,
"contentSearchSuggestions.xml",
0
);
let engine = vals[0];
let searchStr = "browser_ContentSearch.js-suggestions-";
// Add a form history suggestion and wait for Satchel to notify about it.
mm.sendAsyncMessage(TEST_MSG, {
sendEventToContent(browser, {
type: "AddFormHistoryEntry",
data: searchStr + "form",
});
let deferred = PromiseUtils.defer();
Services.obs.addObserver(function onAdd(subj, topic, data) {
if (data == "formhistory-add") {
Services.obs.removeObserver(onAdd, "satchel-storage-changed");
executeSoon(() => deferred.resolve());
}
}, "satchel-storage-changed");
await deferred.promise;
await new Promise(resolve => {
Services.obs.addObserver(function onAdd(subj, topic, data) {
if (data == "formhistory-add") {
Services.obs.removeObserver(onAdd, "satchel-storage-changed");
executeSoon(resolve);
}
}, "satchel-storage-changed");
});
// Send GetSuggestions using the test engine. Its suggestions should appear
// in the remote suggestions in the Suggestions response below.
mm.sendAsyncMessage(TEST_MSG, {
let suggestionsPromise = await waitForTestMsg(browser, "Suggestions");
sendEventToContent(browser, {
type: "GetSuggestions",
data: {
engineName: engine.name,
@ -251,7 +277,7 @@ add_task(
});
// Check the Suggestions response.
let msg = await waitForTestMsg(mm, "Suggestions");
let msg = await suggestionsPromise.donePromise;
checkMsg(msg, {
type: "Suggestions",
data: {
@ -263,21 +289,23 @@ add_task(
});
// Delete the form history suggestion and wait for Satchel to notify about it.
mm.sendAsyncMessage(TEST_MSG, {
sendEventToContent(browser, {
type: "RemoveFormHistoryEntry",
data: searchStr + "form",
});
deferred = PromiseUtils.defer();
Services.obs.addObserver(function onRemove(subj, topic, data) {
if (data == "formhistory-remove") {
Services.obs.removeObserver(onRemove, "satchel-storage-changed");
executeSoon(() => deferred.resolve());
}
}, "satchel-storage-changed");
await deferred.promise;
await new Promise(resolve => {
Services.obs.addObserver(function onRemove(subj, topic, data) {
if (data == "formhistory-remove") {
Services.obs.removeObserver(onRemove, "satchel-storage-changed");
executeSoon(resolve);
}
}, "satchel-storage-changed");
});
// Send GetSuggestions again.
mm.sendAsyncMessage(TEST_MSG, {
suggestionsPromise = await waitForTestMsg(browser, "Suggestions");
sendEventToContent(browser, {
type: "GetSuggestions",
data: {
engineName: engine.name,
@ -286,7 +314,7 @@ add_task(
});
// The formHistory suggestions in the Suggestions response should be empty.
msg = await waitForTestMsg(mm, "Suggestions");
msg = await suggestionsPromise.donePromise;
checkMsg(msg, {
type: "Suggestions",
data: {
@ -298,15 +326,15 @@ add_task(
});
// Finally, clean up by removing the test engine.
let statePromise = await waitForTestMsg(browser, "CurrentState");
await Services.search.removeEngine(engine);
await waitForTestMsg(mm, "CurrentState");
await statePromise.donePromise;
}
);
async function performSearch(browser, data, expectedURL) {
let mm = browser.messageManager;
let stoppedPromise = BrowserTestUtils.browserStopped(browser, expectedURL);
mm.sendAsyncMessage(TEST_MSG, {
sendEventToContent(browser, {
type: "Search",
data,
expectedURL,
@ -362,44 +390,69 @@ function checkArrayBuffers(actual, expected) {
}
function checkMsg(actualMsg, expectedMsgData) {
let actualMsgData = actualMsg.data;
SimpleTest.isDeeply(actualMsg.data, expectedMsgData, "Checking message");
SimpleTest.isDeeply(actualMsg, expectedMsgData, "Checking message");
// Engines contain ArrayBuffers which we have to compare byte by byte and
// not as Objects (like SimpleTest.isDeeply does).
checkArrayBuffers(actualMsgData, expectedMsgData);
checkArrayBuffers(actualMsg, expectedMsgData);
}
function waitForTestMsg(mm, type) {
return new Promise(resolve => {
info("Waiting for " + TEST_MSG + " message " + type + "...");
mm.addMessageListener(TEST_MSG, function onMsg(msg) {
info("Received " + TEST_MSG + " message " + msg.data.type + "\n");
if (msg.data.type == type) {
mm.removeMessageListener(TEST_MSG, onMsg);
resolve(msg);
async function waitForTestMsg(browser, type, count = 1) {
await SpecialPowers.spawn(
browser,
[SERVICE_EVENT_TYPE, type, count],
(childEvent, childType, childCount) => {
content.eventDetails = [];
function listener(event) {
if (event.detail.type != childType) {
return;
}
content.eventDetails.push(event.detail);
if (--childCount > 0) {
return;
}
content.removeEventListener(childEvent, listener, true);
}
});
});
content.addEventListener(childEvent, listener, true);
}
);
let donePromise = SpecialPowers.spawn(
browser,
[type, count],
async (childType, childCount) => {
await ContentTaskUtils.waitForCondition(() => {
return content.eventDetails.length == childCount;
}, "Expected " + childType + " event");
return childCount > 1 ? content.eventDetails : content.eventDetails[0];
}
);
return { donePromise };
}
function waitForNewEngine(mm, basename, numImages) {
async function waitForNewEngine(browser, basename, numImages) {
info("Waiting for engine to be added: " + basename);
// Wait for the search events triggered by adding the new engine.
// engine-added engine-loaded
let expectedSearchEvents = ["CurrentState", "CurrentState"];
let count = 2;
// engine-changed for each of the images
for (let i = 0; i < numImages; i++) {
expectedSearchEvents.push("CurrentState");
count++;
}
let eventPromises = expectedSearchEvents.map(e => waitForTestMsg(mm, e));
let statePromise = await waitForTestMsg(browser, "CurrentState", count);
// Wait for addEngine().
let url = getRootDirectory(gTestPath) + basename;
return Promise.all(
[Services.search.addEngine(url, "", false)].concat(eventPromises)
);
let engine = await Services.search.addEngine(url, "", false);
let results = await statePromise.donePromise;
return [engine, ...results];
}
async function addTab() {
@ -409,10 +462,7 @@ async function addTab() {
);
registerCleanupFunction(() => gBrowser.removeTab(tab));
let url = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME;
let mm = tab.linkedBrowser.messageManager;
mm.loadFrameScript(url, false);
return { browser: tab.linkedBrowser, mm };
return { browser: tab.linkedBrowser };
}
var currentStateObj = async function(isPrivateWindowValue, hiddenEngine = "") {

View File

@ -1,33 +0,0 @@
/* 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/. */
/* eslint-env mozilla/frame-script */
const TEST_MSG = "ContentSearchTest";
const SERVICE_EVENT_TYPE = "ContentSearchService";
const CLIENT_EVENT_TYPE = "ContentSearchClient";
// Forward events from the in-content service to the test.
content.addEventListener(SERVICE_EVENT_TYPE, event => {
// The event dispatch code in content.js clones the event detail into the
// content scope. That's generally the right thing, but causes us to end
// up with an XrayWrapper to it here, which will screw us up when trying to
// serialize the object in sendAsyncMessage. Waive Xrays for the benefit of
// the test machinery.
sendAsyncMessage(TEST_MSG, Cu.waiveXrays(event.detail));
});
// Forward messages from the test to the in-content service.
addMessageListener(TEST_MSG, msg => {
(async function() {
// If the message is a search, stop the page from loading and then tell the
// test that it loaded.
content.dispatchEvent(
new content.CustomEvent(CLIENT_EVENT_TYPE, {
detail: Cu.cloneInto(msg.data, content),
})
);
})();
});

View File

@ -18,11 +18,6 @@ ChromeUtils.defineModuleGetter(
"AddonManager",
"resource://gre/modules/AddonManager.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ContentSearch",
"resource:///modules/ContentSearch.jsm"
);
const SIMPLETEST_OVERRIDES = [
"ok",