/* 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 TEST_MSG = "ContentSearchTest"; const CONTENT_SEARCH_MSG = "ContentSearch"; const TEST_CONTENT_SCRIPT_BASENAME = "contentSearch.js"; // This timeout is absurdly high to avoid random failures like bug 1087120. // That bug was reported when the timeout was 5 seconds, so let's try 10. const SUGGESTIONS_TIMEOUT = 10000; var gMsgMan; add_task(function* GetState() { yield addTab(); gMsgMan.sendAsyncMessage(TEST_MSG, { type: "GetState", }); let msg = yield waitForTestMsg("State"); checkMsg(msg, { type: "State", data: yield currentStateObj(), }); }); add_task(function* SetCurrentEngine() { yield addTab(); let newCurrentEngine = null; let oldCurrentEngine = Services.search.currentEngine; let engines = Services.search.getVisibleEngines(); for (let engine of engines) { if (engine != oldCurrentEngine) { newCurrentEngine = engine; break; } } if (!newCurrentEngine) { info("Couldn't find a non-selected search engine, " + "skipping this part of the test"); return; } gMsgMan.sendAsyncMessage(TEST_MSG, { type: "SetCurrentEngine", data: newCurrentEngine.name, }); let deferred = Promise.defer(); Services.obs.addObserver(function obs(subj, topic, data) { info("Test observed " + data); if (data == "engine-current") { ok(true, "Test observed engine-current"); Services.obs.removeObserver(obs, "browser-search-engine-modified"); deferred.resolve(); } }, "browser-search-engine-modified", false); let searchPromise = waitForTestMsg("CurrentEngine"); info("Waiting for test to observe engine-current..."); yield deferred.promise; let msg = yield searchPromise; checkMsg(msg, { type: "CurrentEngine", data: yield currentEngineObj(newCurrentEngine), }); Services.search.currentEngine = oldCurrentEngine; msg = yield waitForTestMsg("CurrentEngine"); checkMsg(msg, { type: "CurrentEngine", data: yield currentEngineObj(oldCurrentEngine), }); }); add_task(function* modifyEngine() { yield addTab(); let engine = Services.search.currentEngine; let oldAlias = engine.alias; engine.alias = "ContentSearchTest"; let msg = yield waitForTestMsg("CurrentState"); checkMsg(msg, { type: "CurrentState", data: yield currentStateObj(), }); engine.alias = oldAlias; msg = yield waitForTestMsg("CurrentState"); checkMsg(msg, { type: "CurrentState", data: yield currentStateObj(), }); }); add_task(function* search() { yield addTab(); let engine = Services.search.currentEngine; let data = { engineName: engine.name, searchString: "ContentSearchTest", whence: "ContentSearchTest", }; gMsgMan.sendAsyncMessage(TEST_MSG, { type: "Search", data: data, }); let submissionURL = engine.getSubmission(data.searchString, "", data.whence).uri.spec; yield waitForLoadAndStopIt(gBrowser.selectedBrowser, submissionURL); }); add_task(function* searchInBackgroundTab() { // This test is like search(), but it opens a new tab after starting a search // in another. In other words, it performs a search in a background tab. The // search page should be loaded in the same tab that performed the search, in // the background tab. yield addTab(); let searchBrowser = gBrowser.selectedBrowser; let engine = Services.search.currentEngine; let data = { engineName: engine.name, searchString: "ContentSearchTest", whence: "ContentSearchTest", }; gMsgMan.sendAsyncMessage(TEST_MSG, { type: "Search", data: data, }); let newTab = gBrowser.addTab(); gBrowser.selectedTab = newTab; registerCleanupFunction(() => gBrowser.removeTab(newTab)); let submissionURL = engine.getSubmission(data.searchString, "", data.whence).uri.spec; yield waitForLoadAndStopIt(searchBrowser, submissionURL); }); add_task(function* badImage() { yield 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 = yield waitForNewEngine("contentSearchBadImage.xml", 1); let engine = vals[0]; let finalCurrentStateMsg = vals[vals.length - 1]; let expectedCurrentState = yield currentStateObj(); let expectedEngine = expectedCurrentState.engines.find(e => e.name == engine.name); ok(!!expectedEngine, "Sanity check: engine should be in expected state"); ok(expectedEngine.iconBuffer === null, "Sanity check: icon array buffer of engine in expected state " + "should be null: " + expectedEngine.iconBuffer); checkMsg(finalCurrentStateMsg, { type: "CurrentState", data: expectedCurrentState, }); // Removing the engine triggers a final CurrentState message. Wait for it so // it doesn't trip up subsequent tests. Services.search.removeEngine(engine); yield waitForTestMsg("CurrentState"); }); add_task(function* GetSuggestions_AddFormHistoryEntry_RemoveFormHistoryEntry() { yield addTab(); // Add the test engine that provides suggestions. let vals = yield waitForNewEngine("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. gMsgMan.sendAsyncMessage(TEST_MSG, { type: "AddFormHistoryEntry", data: searchStr + "form", }); let deferred = Promise.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", false); yield deferred.promise; // Send GetSuggestions using the test engine. Its suggestions should appear // in the remote suggestions in the Suggestions response below. gMsgMan.sendAsyncMessage(TEST_MSG, { type: "GetSuggestions", data: { engineName: engine.name, searchString: searchStr, remoteTimeout: SUGGESTIONS_TIMEOUT, }, }); // Check the Suggestions response. let msg = yield waitForTestMsg("Suggestions"); checkMsg(msg, { type: "Suggestions", data: { engineName: engine.name, searchString: searchStr, formHistory: [searchStr + "form"], remote: [searchStr + "foo", searchStr + "bar"], }, }); // Delete the form history suggestion and wait for Satchel to notify about it. gMsgMan.sendAsyncMessage(TEST_MSG, { type: "RemoveFormHistoryEntry", data: searchStr + "form", }); deferred = Promise.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", false); yield deferred.promise; // Send GetSuggestions again. gMsgMan.sendAsyncMessage(TEST_MSG, { type: "GetSuggestions", data: { engineName: engine.name, searchString: searchStr, remoteTimeout: SUGGESTIONS_TIMEOUT, }, }); // The formHistory suggestions in the Suggestions response should be empty. msg = yield waitForTestMsg("Suggestions"); checkMsg(msg, { type: "Suggestions", data: { engineName: engine.name, searchString: searchStr, formHistory: [], remote: [searchStr + "foo", searchStr + "bar"], }, }); // Finally, clean up by removing the test engine. Services.search.removeEngine(engine); yield waitForTestMsg("CurrentState"); }); function buffersEqual(actualArrayBuffer, expectedArrayBuffer) { let expectedView = new Int8Array(expectedArrayBuffer); let actualView = new Int8Array(actualArrayBuffer); for (let i = 0; i < expectedView.length; i++) { if (actualView[i] != expectedView[i]) { return false; } } return true; } function arrayBufferEqual(actualArrayBuffer, expectedArrayBuffer) { ok(actualArrayBuffer instanceof ArrayBuffer, "Actual value is ArrayBuffer."); ok(expectedArrayBuffer instanceof ArrayBuffer, "Expected value is ArrayBuffer."); Assert.equal(actualArrayBuffer.byteLength, expectedArrayBuffer.byteLength, "Array buffers have the same length."); ok(buffersEqual(actualArrayBuffer, expectedArrayBuffer), "Buffers are equal."); } function checkArrayBuffers(actual, expected) { if (actual instanceof ArrayBuffer) { arrayBufferEqual(actual, expected); } if (typeof actual == "object") { for (let i in actual) { checkArrayBuffers(actual[i], expected[i]); } } } function checkMsg(actualMsg, expectedMsgData) { let actualMsgData = actualMsg.data; SimpleTest.isDeeply(actualMsg.data, 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); } function waitForMsg(name, type) { let deferred = Promise.defer(); info("Waiting for " + name + " message " + type + "..."); gMsgMan.addMessageListener(name, function onMsg(msg) { info("Received " + name + " message " + msg.data.type + "\n"); if (msg.data.type == type) { gMsgMan.removeMessageListener(name, onMsg); deferred.resolve(msg); } }); return deferred.promise; } function waitForTestMsg(type) { return waitForMsg(TEST_MSG, type); } function waitForNewEngine(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"]; // engine-changed for each of the images for (let i = 0; i < numImages; i++) { expectedSearchEvents.push("CurrentState"); } let eventPromises = expectedSearchEvents.map(e => waitForTestMsg(e)); // Wait for addEngine(). let addDeferred = Promise.defer(); let url = getRootDirectory(gTestPath) + basename; Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "", false, { onSuccess: function (engine) { info("Search engine added: " + basename); addDeferred.resolve(engine); }, onError: function (errCode) { ok(false, "addEngine failed with error code " + errCode); addDeferred.reject(); }, }); return Promise.all([addDeferred.promise].concat(eventPromises)); } function waitForLoadAndStopIt(browser, expectedURL) { let deferred = Promise.defer(); let listener = { onStateChange: function (webProg, req, flags, status) { if (req instanceof Ci.nsIChannel) { let url = req.originalURI.spec; info("onStateChange " + url); let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_START; if ((flags & docStart) && webProg.isTopLevel && url == expectedURL) { browser.removeProgressListener(listener); ok(true, "Expected URL loaded"); req.cancel(Components.results.NS_ERROR_FAILURE); deferred.resolve(); } } }, QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference, ]), }; browser.addProgressListener(listener); info("Waiting for URL to load: " + expectedURL); return deferred.promise; } function addTab() { let deferred = Promise.defer(); let tab = gBrowser.addTab(); gBrowser.selectedTab = tab; tab.linkedBrowser.addEventListener("load", function load() { tab.linkedBrowser.removeEventListener("load", load, true); let url = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME; gMsgMan = tab.linkedBrowser.messageManager; gMsgMan.sendAsyncMessage(CONTENT_SEARCH_MSG, { type: "AddToWhitelist", data: ["about:blank"], }); waitForMsg(CONTENT_SEARCH_MSG, "AddToWhitelistAck").then(() => { gMsgMan.loadFrameScript(url, false); deferred.resolve(); }); }, true); registerCleanupFunction(() => gBrowser.removeTab(tab)); return deferred.promise; } let currentStateObj = Task.async(function* () { let state = { engines: [], currentEngine: yield currentEngineObj(), }; for (let engine of Services.search.getVisibleEngines()) { let uri = engine.getIconURLBySize(16, 16); state.engines.push({ name: engine.name, iconBuffer: yield arrayBufferFromDataURI(uri), }); } return state; }); let currentEngineObj = Task.async(function* () { let engine = Services.search.currentEngine; let uri1x = engine.getIconURLBySize(65, 26); let uri2x = engine.getIconURLBySize(130, 52); let uriFavicon = engine.getIconURLBySize(16, 16); let bundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties"); return { name: engine.name, placeholder: bundle.formatStringFromName("searchWithEngine", [engine.name], 1), logoBuffer: yield arrayBufferFromDataURI(uri1x), logo2xBuffer: yield arrayBufferFromDataURI(uri2x), iconBuffer: yield arrayBufferFromDataURI(uriFavicon), }; }); function arrayBufferFromDataURI(uri) { if (!uri) { return Promise.resolve(null); } let deferred = Promise.defer(); let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. createInstance(Ci.nsIXMLHttpRequest); xhr.open("GET", uri, true); xhr.responseType = "arraybuffer"; xhr.onloadend = () => { deferred.resolve(xhr.response); }; try { xhr.send(); } catch (err) { return Promise.resolve(null); } return deferred.promise; }