summaryrefslogtreecommitdiffstats
path: root/browser/modules/test/browser/browser_ContentSearch.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/test/browser/browser_ContentSearch.js')
-rw-r--r--browser/modules/test/browser/browser_ContentSearch.js519
1 files changed, 519 insertions, 0 deletions
diff --git a/browser/modules/test/browser/browser_ContentSearch.js b/browser/modules/test/browser/browser_ContentSearch.js
new file mode 100644
index 0000000000..0e489a54a1
--- /dev/null
+++ b/browser/modules/test/browser/browser_ContentSearch.js
@@ -0,0 +1,519 @@
+/* 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");
+ }
+ });
+}