/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js", this ); ChromeUtils.defineESModuleGetters(this, { CustomizableUITestUtils: "resource://testing-common/CustomizableUITestUtils.sys.mjs", QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", }); const lazy = {}; ChromeUtils.defineLazyGetter(lazy, "QuickSuggestTestUtils", () => { const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( "resource://testing-common/QuickSuggestTestUtils.sys.mjs" ); module.init(this); return module; }); ChromeUtils.defineLazyGetter(this, "MerinoTestUtils", () => { const { MerinoTestUtils: module } = ChromeUtils.importESModule( "resource://testing-common/MerinoTestUtils.sys.mjs" ); module.init(this); return module; }); ChromeUtils.defineLazyGetter(this, "GeolocationTestUtils", () => { const { GeolocationTestUtils: module } = ChromeUtils.importESModule( "resource://testing-common/GeolocationTestUtils.sys.mjs" ); module.init(this); return module; }); ChromeUtils.defineESModuleGetters(lazy, { UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", }); ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( Ci.nsIObserver ).wrappedJSObject; }); async function addTopSites(url) { for (let i = 0; i < 5; i++) { await PlacesTestUtils.addVisits(url); } await updateTopSites(sites => { return sites && sites[0] && sites[0].url == url; }); } function assertAbandonmentTelemetry(expectedExtraList) { assertGleanTelemetry("abandonment", expectedExtraList); } function assertEngagementTelemetry(expectedExtraList) { assertGleanTelemetry("engagement", expectedExtraList); } function assertExposureTelemetry(expectedExtraList) { assertGleanTelemetry("exposure", expectedExtraList); } function assertDisableTelemetry(expectedExtraList) { assertGleanTelemetry("disable", expectedExtraList); } function assertBounceTelemetry(expectedExtraList) { assertGleanTelemetry("bounce", expectedExtraList); } function assertGleanTelemetry(telemetryName, expectedExtraList) { const camelName = telemetryName.replaceAll(/_(.)/g, (match, p1) => p1.toUpperCase() ); const telemetries = Glean.urlbar[camelName].testGetValue() ?? []; info( "Asserting Glean telemetry is correct, actual events are: " + JSON.stringify(telemetries) ); Assert.equal( telemetries.length, expectedExtraList.length, "Telemetry event length matches expected event length." ); for (let i = 0; i < telemetries.length; i++) { const telemetry = telemetries[i]; Assert.equal(telemetry.category, "urlbar"); Assert.equal(telemetry.name, telemetryName); const expectedExtra = expectedExtraList[i]; for (const key of Object.keys(expectedExtra)) { Assert.equal( telemetry.extra[key], expectedExtra[key], `${key} is correct` ); } } } async function ensureQuickSuggestInit({ ...args } = {}) { return lazy.QuickSuggestTestUtils.ensureQuickSuggestInit({ remoteSettingsRecords: [ { collection: lazy.QuickSuggestTestUtils.RS_COLLECTION.AMP, type: lazy.QuickSuggestTestUtils.RS_TYPE.AMP, attachment: [ lazy.QuickSuggestTestUtils.ampRemoteSettings({ keywords: ["amp", "amp and wikipedia"], }), ], }, { collection: lazy.QuickSuggestTestUtils.RS_COLLECTION.OTHER, type: lazy.QuickSuggestTestUtils.RS_TYPE.WIKIPEDIA, attachment: [ lazy.QuickSuggestTestUtils.wikipediaRemoteSettings({ keywords: ["wikipedia", "amp and wikipedia"], }), ], }, lazy.QuickSuggestTestUtils.weatherRecord(), { type: "dynamic-suggestions", suggestion_type: "test-exposure-aaa", score: 1.0, attachment: [ { keywords: ["aaa keyword"], data: { result: { isHiddenExposure: true, }, }, }, ], }, { type: "dynamic-suggestions", suggestion_type: "important_dates", score: 1.0, attachment: [ { keywords: ["important dates"], data: { result: { payload: { dates: [ // These dates are always in the future, so that we will // always get a match in the test when we expect one. Temporal.Now.plainDateISO().add({ months: 1 }).toString(), Temporal.Now.plainDateISO().add({ years: 1 }).toString(), ], name: "Event 1", }, }, }, }, ], }, ], ...args, }); } async function doBlur(testUtils = UrlbarTestUtils, win = window) { await testUtils.promisePopupClose(win, () => { testUtils.getUrlbar(win).blur(); }); } async function doClick() { const selected = UrlbarTestUtils.getSelectedRow(window); const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); EventUtils.synthesizeMouseAtCenter(selected, {}); await onLoad; } async function doClickSubButton(selector) { const selected = UrlbarTestUtils.getSelectedElement(window); const button = selected.closest(".urlbarView-row").querySelector(selector); EventUtils.synthesizeMouseAtCenter(button, {}); } async function doDropAndGo(data) { const onLoad = BrowserTestUtils.browserLoaded(browser); EventUtils.synthesizeDrop( document.getElementById("back-button"), gURLBar.inputField, [[{ type: "text/plain", data }]], "copy", window ); await onLoad; } async function doEnter(modifier = {}, win = window) { const onLoad = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); EventUtils.synthesizeKey("KEY_Enter", modifier, win); await onLoad; } async function doPaste(data) { await SimpleTest.promiseClipboardChange(data, () => { clipboardHelper.copyString(data); }); gURLBar.focus(); gURLBar.select(); document.commandDispatcher .getControllerForCommand("cmd_paste") .doCommand("cmd_paste"); await UrlbarTestUtils.promiseSearchComplete(window); } async function doPasteAndGo(data) { await SimpleTest.promiseClipboardChange(data, () => { clipboardHelper.copyString(data); }); const inputBox = gURLBar.querySelector("moz-input-box"); const contextMenu = inputBox.menupopup; const onPopup = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { type: "contextmenu", button: 2, }); await onPopup; const onLoad = BrowserTestUtils.browserLoaded(browser); const menuitem = inputBox.getMenuItem("paste-and-go"); contextMenu.activateItem(menuitem); await onLoad; } async function doTest(testFn) { await Services.fog.testFlushAllChildren(); Services.fog.testResetFOG(); gURLBar.controller.engagementEvent.reset(); await PlacesUtils.history.clear(); await PlacesUtils.bookmarks.eraseEverything(); await PlacesTestUtils.clearHistoryVisits(); await PlacesTestUtils.clearInputHistory(); await UrlbarTestUtils.formHistory.clear(window); await QuickSuggest.clearDismissedSuggestions(); await updateTopSites(() => true); await BrowserTestUtils.withNewTab(gBrowser, testFn); } async function initGroupTest() { /* import-globals-from head-groups.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-engagementTelemetry/head-groups.js", this ); await setup(); } async function initInteractionTest() { /* import-globals-from head-interaction.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-engagementTelemetry/head-interaction.js", this ); await setup(); } async function initNCharsAndNWordsTest() { /* import-globals-from head-n_chars_n_words.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-engagementTelemetry/head-n_chars_n_words.js", this ); await setup(); } async function initSapTest() { /* import-globals-from head-sap.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-engagementTelemetry/head-sap.js", this ); await setup(); } async function initSearchEngineDefaultIdTest() { /* import-globals-from head-search_engine_default_id.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-engagementTelemetry/head-search_engine_default_id.js", this ); await setup(); } async function initSearchModeTest() { /* import-globals-from head-search_mode.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-engagementTelemetry/head-search_mode.js", this ); await setup(); } async function initExposureTest() { /* import-globals-from head-exposure.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-engagementTelemetry/head-exposure.js", this ); await setup(); } function loadOmniboxAddon({ keyword }) { return ExtensionTestUtils.loadExtension({ manifest: { permissions: ["tabs"], omnibox: { keyword, }, }, background() { /* global browser */ browser.omnibox.setDefaultSuggestion({ description: "doit", }); browser.omnibox.onInputEntered.addListener(() => { browser.tabs.update({ url: "https://example.com/" }); }); browser.omnibox.onInputChanged.addListener((text, suggest) => { suggest([]); }); }, }); } async function loadRemoteTab(url) { await SpecialPowers.pushPrefEnv({ set: [ ["browser.urlbar.suggest.searches", false], ["browser.urlbar.maxHistoricalSearchSuggestions", 0], ["browser.urlbar.autoFill", false], ["services.sync.username", "fake"], ], }); const REMOTE_TAB = { id: "test", type: "client", lastModified: 1492201200, name: "test", clientType: "desktop", tabs: [ { type: "tab", title: "tesrt", url, icon: UrlbarUtils.ICON.DEFAULT, client: "test", lastUsed: Math.floor(Date.now() / 1000), }, ], }; const sandbox = lazy.sinon.createSandbox(); // eslint-disable-next-line no-undef const syncedTabs = SyncedTabs; const originalSyncedTabsInternal = syncedTabs._internal; syncedTabs._internal = { isConfiguredToSyncTabs: true, hasSyncedThisSession: true, getTabClients() { return Promise.resolve([]); }, syncTabs() { return Promise.resolve(); }, }; const weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( Ci.nsISupports ).wrappedJSObject; const oldWeaveServiceReady = weaveXPCService.ready; weaveXPCService.ready = true; sandbox .stub(syncedTabs._internal, "getTabClients") .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); return { async unload() { sandbox.restore(); weaveXPCService.ready = oldWeaveServiceReady; syncedTabs._internal = originalSyncedTabsInternal; // Reset internal cache in UrlbarProviderRemoteTabs. Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); await SpecialPowers.popPrefEnv(); }, }; } async function openPopup(input, testUtils = UrlbarTestUtils, win = window) { await testUtils.promisePopupOpen(win, async () => { await testUtils.inputIntoURLBar(win, input); }); await testUtils.promiseSearchComplete(win); } async function selectRowByURL(url) { for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); if (detail.url === url) { UrlbarTestUtils.setSelectedRowIndex(window, i); return; } } } async function selectRowByProvider(provider) { for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); if (detail.result.providerName === provider) { UrlbarTestUtils.setSelectedRowIndex(window, i); break; } } } async function selectRowByType(type) { for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); if (detail.result.payload.type === type) { UrlbarTestUtils.setSelectedRowIndex(window, i); return; } } } async function setup() { await SpecialPowers.pushPrefEnv({ set: [ ["browser.urlbar.searchEngagementTelemetry.enabled", true], ["browser.urlbar.quickactions.enabled", true], ["browser.urlbar.secondaryActions.featureGate", true], ["browser.urlbar.scotchBonnet.enableOverride", false], ], }); const engine = await SearchTestUtils.installOpenSearchEngine({ url: "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml", }); const originalDefaultEngine = await SearchService.getDefault(); await SearchService.setDefault(engine, SearchService.CHANGE_REASON.UNKNOWN); await SearchService.moveEngine(engine, 0); registerCleanupFunction(async function () { // Tests verify that no prefs have been changed so clear any // so clear any prefs we may have touched while running tests. let prefs = [ "services.sync.lastTabFetch", "services.settings.clock_skew_seconds", "services.settings.last_update_seconds", "services.settings.last_etag", "browser.urlbar.recentsearches.lastDefaultChanged", "browser.search.totalSearches", "browser.urlbar.events.bounce.maxSecondsFromLastSearch", ]; prefs.forEach(pref => Services.prefs.clearUserPref(pref)); await SpecialPowers.popPrefEnv(); await SearchService.setDefault( originalDefaultEngine, SearchService.CHANGE_REASON.UNKNOWN ); }); } async function setupNimbus(variables) { return lazy.UrlbarTestUtils.initNimbusFeature(variables); } async function showResultByArrowDown() { gURLBar.value = ""; gURLBar.select(); await UrlbarTestUtils.promisePopupOpen(window, () => { EventUtils.synthesizeKey("KEY_ArrowDown"); }); await UrlbarTestUtils.promiseSearchComplete(window); } async function expectNoConsoleErrors(task) { let endConsoleListening = TestUtils.listenForConsoleMessages(); let msgs; let taskResult; try { taskResult = await task(); } finally { msgs = await endConsoleListening(); } for (let msg of msgs) { if (msg.level === "error") { throw new Error(`Console error detected: ${msg.arguments[0]}`); } } return taskResult; } /** * Helper to run a semantic history test. * * @param {Array} results array of results to return * @param {Function} task async function to wrap * @param {number} [embeddingSize] size of embeddings */ async function doTestWithSemantic(results, task, embeddingSize = 16) { /** * A mock object that pretends to be an MLEngine. */ class MockMLEngine { async run(request) { const texts = request.args[0]; return texts.map(text => { if (typeof text !== "string" || text.trim() === "") { throw new Error("Invalid input: text must be a non-empty string"); } // Return a mock embedding vector (e.g., an array of zeros) return Array(embeddingSize).fill(0); }); } } const { getPlacesSemanticHistoryManager } = ChromeUtils.importESModule( "resource://gre/modules/PlacesSemanticHistoryManager.sys.mjs" ); let semanticManager = getPlacesSemanticHistoryManager(); let canUseSemanticStub = sinon.stub(semanticManager, "canUseSemanticSearch"); canUseSemanticStub.get(() => true); let hasSufficientEntriesStub = sinon .stub(semanticManager, "hasSufficientEntriesForSearching") .resolves(true); semanticManager.embedder.setEngine(new MockMLEngine()); let inferStub = sinon.stub(semanticManager, "infer").resolves({ results }); await SpecialPowers.pushPrefEnv({ set: [["places.semanticHistory.featureGate", true]], }); try { await doTest(task); } finally { await SpecialPowers.popPrefEnv(); hasSufficientEntriesStub.restore(); canUseSemanticStub.restore(); inferStub.restore(); } }