/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /** * This file tests urlbar autofill telemetry. */ "use strict"; const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; function assertSearchTelemetryEmpty(search_hist) { const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); Assert.ok( !(SCALAR_URLBAR in scalars), `Should not have recorded ${SCALAR_URLBAR}` ); // SEARCH_COUNTS should not contain any engine counts at all. The keys in this // histogram are search engine telemetry identifiers. Assert.deepEqual( Object.keys(search_hist.snapshot()), [], "SEARCH_COUNTS is empty" ); let sapEvent = Glean.sap.counts.testGetValue(); Assert.equal(sapEvent, null, "Should not have recorded any SAP events"); // Also check events. let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ); events = (events.parent || []).filter( e => e[1] == "navigation" && e[2] == "search" ); Assert.deepEqual( events, [], "Should not have recorded any navigation search events" ); } function snapshotHistograms() { Services.telemetry.clearScalars(); Services.telemetry.clearEvents(); Services.fog.testResetFOG(); return { search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), }; } /** * Performs a search and picks the first result. * * @param {string} searchString * The search string. Assumed to trigger an autofill result * @param {string} autofilledValue * The input's expected value after autofill occurs. * @param {string} unpickResult * Optional: If true, do not pick any result. Default value is false. * @param {string} urlToSelect * Optional: If want to select result except autofill, pass the URL. */ async function triggerAutofillAndPickResult( searchString, autofilledValue, unpickResult = false, urlToSelect = null ) { await BrowserTestUtils.withNewTab("about:blank", async () => { await UrlbarTestUtils.promiseAutocompleteResultPopup({ window, value: searchString, fireInputEvent: true, }); let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); Assert.ok(details.autofill, "Result is autofill"); Assert.equal(gURLBar.value, autofilledValue, "gURLBar.value"); Assert.equal(gURLBar.selectionStart, searchString.length, "selectionStart"); Assert.equal(gURLBar.selectionEnd, autofilledValue.length, "selectionEnd"); if (urlToSelect) { for (let row = 0; row < UrlbarTestUtils.getResultCount(window); row++) { const result = await UrlbarTestUtils.getDetailsOfResultAt(window, row); if (result.url === urlToSelect) { UrlbarTestUtils.setSelectedRowIndex(window, row); break; } } } if (unpickResult) { // Close popup without any action. await UrlbarTestUtils.promisePopupClose(window); return; } let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); EventUtils.synthesizeKey("KEY_Enter"); await loadPromise; let url; if (urlToSelect) { url = urlToSelect; } else { url = autofilledValue.includes(":") ? autofilledValue : "http://" + autofilledValue; } Assert.equal(gBrowser.currentURI.spec, url, "Loaded URL is correct"); }); } function createOtherAutofillProvider(searchString, autofilledValue) { return new UrlbarTestUtils.TestProvider({ priority: Infinity, type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC, results: [ new UrlbarResult({ type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, heuristic: true, autofill: { value: autofilledValue, selectionStart: searchString.length, selectionEnd: autofilledValue.length, // Leave out `type` to trigger "other" }, payload: { title: "Test", url: "http://example.com/", }, }), ], }); } // Allow more time for Mac machines so they don't time out in verify mode. if (AppConstants.platform == "macosx") { requestLongerTimeout(3); } add_setup(async function () { await PlacesUtils.bookmarks.eraseEverything(); await PlacesUtils.history.clear(); await PlacesTestUtils.clearInputHistory(); // Enable local telemetry recording for the duration of the tests. const originalCanRecord = Services.telemetry.canRecordExtended; Services.telemetry.canRecordExtended = true; // Make sure autofill is tested without upgrading pages to https await SpecialPowers.pushPrefEnv({ set: [ ["dom.security.https_first", false], ["dom.security.https_first_schemeless", false], ], }); registerCleanupFunction(async () => { Services.telemetry.canRecordExtended = originalCanRecord; await PlacesTestUtils.clearInputHistory(); await PlacesUtils.history.clear(); }); }); // Checks adaptive history, origin, and URL autofill. add_task(async function history() { const testData = [ { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "ex", autofilled: "example.com/", }, { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "exa", autofilled: "example.com/test", }, { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "exam", autofilled: "example.com/test", }, { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "example.com", autofilled: "example.com/test", }, { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "example.com/", autofilled: "example.com/test", }, { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "example.com/test", autofilled: "example.com/test", }, { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, { url: "http://example.org/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "example.org", autofilled: "example.org/", }, { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, { url: "http://example.com/test/url", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "example.com/test/", autofilled: "example.com/test/", }, { useAdaptiveHistory: true, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [ { uri: "http://example.com/test", input: "http://example.com/test" }, ], userInput: "http://example.com/test", autofilled: "http://example.com/test", }, { useAdaptiveHistory: false, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "example", autofilled: "example.com/", }, { useAdaptiveHistory: false, visitHistory: [ { url: "http://example.com/test", transition: PlacesUtils.history.TRANSITION_TYPED, }, ], inputHistory: [{ uri: "http://example.com/test", input: "exa" }], userInput: "example.com/te", autofilled: "example.com/test", }, ]; for (const { useAdaptiveHistory, visitHistory, inputHistory, userInput, autofilled, } of testData) { const histograms = snapshotHistograms(); await PlacesTestUtils.addVisits(visitHistory); for (const { uri, input } of inputHistory) { await UrlbarUtils.addToInputHistory(uri, input); } await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory); await triggerAutofillAndPickResult(userInput, autofilled); assertSearchTelemetryEmpty(histograms.search_hist); UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); await PlacesTestUtils.clearInputHistory(); await PlacesUtils.history.clear(); } }); // Checks about-page autofill (e.g., "about:about"). add_task(async function about() { let histograms = snapshotHistograms(); await triggerAutofillAndPickResult("about:abou", "about:about"); assertSearchTelemetryEmpty(histograms.search_hist); await PlacesUtils.history.clear(); }); // Checks the "other" fallback, which shouldn't normally happen. add_task(async function other() { let searchString = "exam"; let autofilledValue = "example.com/"; let provider = createOtherAutofillProvider(searchString, autofilledValue); let providersManager = ProvidersManager.getInstanceForSap("urlbar"); providersManager.registerProvider(provider); let histograms = snapshotHistograms(); await triggerAutofillAndPickResult(searchString, autofilledValue); assertSearchTelemetryEmpty(histograms.search_hist); await PlacesUtils.history.clear(); providersManager.unregisterProvider(provider); }); // Checks autofill deletion telemetry. add_task(async function deletion() { // Raise the adaptive-history backspace threshold so that subtests which // backspace through the whole autofilled value don't trip the penalty and // block the origin for subsequent subtests. await SpecialPowers.pushPrefEnv({ set: [["browser.urlbar.autoFill.backspaceThreshold", 100]], }); await PlacesTestUtils.addVisits([ { url: "http://example.com/", transition: PlacesUtils.history.TRANSITION_TYPED, }, ]); await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); info("Delete autofilled value by DELETE key"); await doDeletionTest({ firstSearchString: "exa", firstAutofilledValue: "example.com/", trigger: () => { EventUtils.synthesizeKey("KEY_Delete"); Assert.equal(gURLBar.value, "exa"); }, expectedScalar: 1, }); info("Delete autofilled value by BACKSPACE key"); await doDeletionTest({ firstSearchString: "exa", firstAutofilledValue: "example.com/", trigger: () => { EventUtils.synthesizeKey("KEY_Backspace"); Assert.equal(gURLBar.value, "exa"); }, expectedScalar: 1, }); info("Delete autofilled value twice"); await doDeletionTest({ firstSearchString: "exa", firstAutofilledValue: "example.com/", trigger: () => { // Delete autofilled string. EventUtils.synthesizeKey("KEY_Delete"); Assert.equal(gURLBar.value, "exa"); // Re-autofilling. EventUtils.synthesizeKey("m"); Assert.equal(gURLBar.value, "example.com/"); Assert.equal(gURLBar.selectionStart, "exam".length); Assert.equal(gURLBar.selectionEnd, "example.com/".length); // Delete autofilled string again. EventUtils.synthesizeKey("KEY_Backspace"); Assert.equal(gURLBar.value, "exam"); }, expectedScalar: 2, }); info("Delete autofilled value and then delete again"); await doDeletionTest({ firstSearchString: "exa", firstAutofilledValue: "example.com/", trigger: () => { EventUtils.synthesizeKey("KEY_Backspace"); Assert.equal(gURLBar.value, "exa"); EventUtils.synthesizeKey("KEY_Backspace"); Assert.equal(gURLBar.value, "ex"); }, expectedScalar: 1, }); info("Delete one char after unselecting autofilled string"); await doDeletionTest({ firstSearchString: "exa", firstAutofilledValue: "example.com/", trigger: () => { // Cancel selection. EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); Assert.equal(gURLBar.selectionStart, "example.com/".length); Assert.equal(gURLBar.selectionEnd, "example.com/".length); EventUtils.synthesizeKey("KEY_Backspace"); Assert.equal(gURLBar.value, "example.com"); }, expectedScalar: 0, }); info("Delete autofilled value after unselecting autofilled string"); await doDeletionTest({ firstSearchString: "exa", firstAutofilledValue: "example.com/", trigger: () => { // Cancel selection. EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); Assert.equal(gURLBar.selectionStart, "example.com/".length); Assert.equal(gURLBar.selectionEnd, "example.com/".length); // Delete autofilled string one by one. for (let i = 0; i < "mple.com/".length; i++) { EventUtils.synthesizeKey("KEY_Backspace"); } Assert.equal(gURLBar.value, "exa"); }, expectedScalar: 0, }); info( "Delete autofilled value after unselecting autofilled string then selecting them manually again" ); await doDeletionTest({ firstSearchString: "exa", firstAutofilledValue: "example.com/", trigger: () => { // Cancel selection. const previousSelectionStart = gURLBar.selectionStart; const previousSelectionEnd = gURLBar.selectionEnd; EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); Assert.equal(gURLBar.selectionStart, "example.com/".length); Assert.equal(gURLBar.selectionEnd, "example.com/".length); // Select same range again. gURLBar.selectionStart = previousSelectionStart; gURLBar.selectionEnd = previousSelectionEnd; EventUtils.synthesizeKey("KEY_Backspace"); Assert.equal(gURLBar.value, "exa"); }, expectedScalar: 1, }); await PlacesUtils.history.clear(); await SpecialPowers.popPrefEnv(); }); async function doDeletionTest({ firstSearchString, firstAutofilledValue, trigger, expectedScalar, }) { await BrowserTestUtils.withNewTab("about:blank", async () => { await UrlbarTestUtils.promiseAutocompleteResultPopup({ window, value: firstSearchString, fireInputEvent: true, }); const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); Assert.ok(details.autofill, "Result is autofill"); Assert.equal(gURLBar.value, firstAutofilledValue, "gURLBar.value"); Assert.equal( gURLBar.selectionStart, firstSearchString.length, "selectionStart" ); Assert.equal( gURLBar.selectionEnd, firstAutofilledValue.length, "selectionEnd" ); await trigger(); const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); if (expectedScalar) { TelemetryTestUtils.assertScalar( scalars, "urlbar.autofill_deletion", expectedScalar ); } else { TelemetryTestUtils.assertScalarUnset(scalars, "urlbar.autofill_deletion"); } await UrlbarTestUtils.promisePopupClose(window); }); }