/* 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/. */ ChromeUtils.defineESModuleGetters(this, { SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", }); SearchTestUtils.init(this); const SERVICE_EVENT_TYPE = "ContentSearchService"; const CLIENT_EVENT_TYPE = "ContentSearchClient"; 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_setup(async function () { await SpecialPowers.pushPrefEnv({ set: [ ["browser.newtab.preload", false], ["browser.search.separatePrivateDefault.ui.enabled", true], ["browser.search.separatePrivateDefault", true], ], }); await SearchTestUtils.promiseNewSearchEngine({ url: "chrome://mochitests/content/browser/browser/components/search/test/browser/testEngine.xml", setAsDefault: true, }); await SearchTestUtils.promiseNewSearchEngine({ url: "chrome://mochitests/content/browser/browser/components/search/test/browser/testEngine_diacritics.xml", setAsDefaultPrivate: true, }); await SearchTestUtils.promiseNewSearchEngine({ url: getRootDirectory(gTestPath) + "testEngine_chromeicon.xml", }); }); add_task(async function GetState() { let { browser } = await addTab(); let statePromise = await waitForTestMsg(browser, "State"); sendEventToContent(browser, { type: "GetState", }); let msg = await statePromise.donePromise; checkMsg(msg, { type: "State", data: await currentStateObj(false), }); ok(arrayBufferIconTested, "ArrayBuffer path for the iconData was tested"); ok(plainURIIconTested, "Plain URI path for the iconData was tested"); }); add_task(async function SetDefaultEngine() { let { browser } = await addTab(); let newDefaultEngine = await Services.search.getEngineByName("FooChromeIcon"); let oldDefaultEngine = await Services.search.getDefault(); let searchPromise = await waitForTestMsg(browser, "CurrentEngine"); sendEventToContent(browser, { type: "SetCurrentEngine", data: newDefaultEngine.name, }); 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 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, Ci.nsISearchService.CHANGE_REASON_UNKNOWN ); msg = await enginePromise.donePromise; checkMsg(msg, { type: "CurrentEngine", data: await constructEngineObj(oldDefaultEngine), }); }); // ContentSearchChild doesn't support setting the private engine at this time // 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 { browser } = await addTab(); let enginePromise = await waitForTestMsg(browser, "CurrentPrivateEngine"); await Services.search.setDefaultPrivate( engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN ); let msg = await enginePromise.donePromise; checkMsg(msg, { type: "CurrentPrivateEngine", data: await constructEngineObj(engine), }); }); add_task(async function modifyEngine() { 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 statePromise.donePromise; checkMsg(msg, { type: "CurrentState", data: await currentStateObj(), }); statePromise = await waitForTestMsg(browser, "CurrentState"); engine.alias = oldAlias; msg = await statePromise.donePromise; checkMsg(msg, { type: "CurrentState", data: await currentStateObj(), }); }); add_task(async function test_hideEngine() { 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 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 statePromise.donePromise; checkMsg(msg, { type: "CurrentState", data: await currentStateObj(), }); }); add_task(async function search() { let { browser } = await addTab(); let engine = await Services.search.getDefault(); let data = { engineName: engine.name, searchString: "ContentSearchTest", healthReportKey: "ContentSearchTest", searchPurpose: "ContentSearchTest", }; let submissionURL = engine.getSubmission(data.searchString, "", data.whence) .uri.spec; await performSearch(browser, data, submissionURL); }); add_task(async 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. let { browser } = await addTab(); let engine = await Services.search.getDefault(); let data = { engineName: engine.name, searchString: "ContentSearchTest", healthReportKey: "ContentSearchTest", searchPurpose: "ContentSearchTest", }; let submissionURL = engine.getSubmission(data.searchString, "", data.whence) .uri.spec; let searchPromise = performSearch(browser, data, submissionURL); let newTab = BrowserTestUtils.addTab(gBrowser); gBrowser.selectedTab = newTab; registerCleanupFunction(() => gBrowser.removeTab(newTab)); await searchPromise; }); add_task(async function badImage() { 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(browser, "contentSearchBadImage.xml"); let engine = vals[0]; let finalCurrentStateMsg = vals[vals.length - 1]; let expectedCurrentState = await currentStateObj(); let expectedEngine = expectedCurrentState.engines.find( e => e.name == engine.name ); ok(!!expectedEngine, "Sanity check: engine should be in expected state"); ok( expectedEngine.iconData === "chrome://browser/skin/search-engine-placeholder.png", "Sanity check: icon of engine in expected state should be the placeholder: " + expectedEngine.iconData ); 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. let statePromise = await waitForTestMsg(browser, "CurrentState"); await Services.search.removeEngine(engine); await statePromise.donePromise; }); add_task( async function GetSuggestions_AddFormHistoryEntry_RemoveFormHistoryEntry() { let { browser } = await addTab(); // Add the test engine that provides suggestions. let vals = await waitForNewEngine(browser, "contentSearchSuggestions.xml"); let engine = vals[0]; let searchStr = "browser_ContentSearch.js-suggestions-"; // Add a form history suggestion and wait for Satchel to notify about it. sendEventToContent(browser, { type: "AddFormHistoryEntry", data: { value: searchStr + "form", engineName: engine.name, }, }); 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. let suggestionsPromise = await waitForTestMsg(browser, "Suggestions"); sendEventToContent(browser, { type: "GetSuggestions", data: { engineName: engine.name, searchString: searchStr, }, }); // Check the Suggestions response. let msg = await suggestionsPromise.donePromise; 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. sendEventToContent(browser, { type: "RemoveFormHistoryEntry", data: searchStr + "form", }); 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. suggestionsPromise = await waitForTestMsg(browser, "Suggestions"); sendEventToContent(browser, { type: "GetSuggestions", data: { engineName: engine.name, searchString: searchStr, }, }); // The formHistory suggestions in the Suggestions response should be empty. msg = await suggestionsPromise.donePromise; checkMsg(msg, { type: "Suggestions", data: { engineName: engine.name, searchString: searchStr, formHistory: [], remote: [searchStr + "foo", searchStr + "bar"], }, }); // Finally, clean up by removing the test engine. let statePromise = await waitForTestMsg(browser, "CurrentState"); await Services.search.removeEngine(engine); await statePromise.donePromise; } ); async function performSearch(browser, data, expectedURL) { let stoppedPromise = BrowserTestUtils.browserStopped(browser, expectedURL); sendEventToContent(browser, { type: "Search", data, expectedURL, }); await stoppedPromise; // BrowserTestUtils.browserStopped should ensure this, but let's // be absolutely sure. Assert.equal( browser.currentURI.spec, expectedURL, "Correct search page loaded" ); } 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) { 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(actualMsg, expectedMsgData); } 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 }; } async function waitForNewEngine(browser, basename) { info("Waiting for engine to be added: " + basename); // Wait for the search events triggered by adding the new engine. // There are two events triggerd by engine-added and engine-loaded let statePromise = await waitForTestMsg(browser, "CurrentState", 2); // Wait for addOpenSearchEngine(). let engine = await SearchTestUtils.promiseNewSearchEngine({ url: getRootDirectory(gTestPath) + basename, }); let results = await statePromise.donePromise; return [engine, ...results]; } async function addTab() { let tab = await BrowserTestUtils.openNewForegroundTab( gBrowser, "about:newtab" ); registerCleanupFunction(() => gBrowser.removeTab(tab)); return { browser: tab.linkedBrowser }; } var currentStateObj = async function (isPrivateWindowValue, hiddenEngine = "") { let state = { engines: [], currentEngine: await constructEngineObj(await Services.search.getDefault()), currentPrivateEngine: await constructEngineObj( await Services.search.getDefaultPrivate() ), }; for (let engine of await Services.search.getVisibleEngines()) { let uri = engine.getIconURLBySize(16, 16); state.engines.push({ name: engine.name, iconData: await iconDataFromURI(uri), hidden: engine.name == hiddenEngine, isAppProvided: engine.isAppProvided, }); } if (typeof isPrivateWindowValue == "boolean") { state.isInPrivateBrowsingMode = isPrivateWindowValue; state.isAboutPrivateBrowsing = isPrivateWindowValue; } return state; }; async function constructEngineObj(engine) { let uriFavicon = engine.getIconURLBySize(16, 16); return { name: engine.name, iconData: await iconDataFromURI(uriFavicon), isAppProvided: engine.isAppProvided, }; } function iconDataFromURI(uri) { if (!uri) { return Promise.resolve( "chrome://browser/skin/search-engine-placeholder.png" ); } if (!uri.startsWith("data:")) { plainURIIconTested = true; return Promise.resolve(uri); } return new Promise(resolve => { let xhr = new XMLHttpRequest(); xhr.open("GET", uri, true); xhr.responseType = "arraybuffer"; xhr.onerror = () => { resolve("chrome://browser/skin/search-engine-placeholder.png"); }; xhr.onload = () => { arrayBufferIconTested = true; resolve(xhr.response); }; try { xhr.send(); } catch (err) { resolve("chrome://browser/skin/search-engine-placeholder.png"); } }); }