tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 482c1522a520cf392c53bacd847a52225b46fd6d
parent b1ef8e49dd8db810cf5aef0f17fb8dee7e6587b9
Author: Daisuke Akatsuka <daisuke@birchill.co.jp>
Date:   Sat,  1 Nov 2025 01:42:23 +0000

Bug 1995894: Use geolocation in GeolocationUtils when detecting the local location r=home-newtab-reviewers,adw,reemhamz

Differential Revision: https://phabricator.services.mozilla.com/D270443

Diffstat:
Mbrowser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js | 4+++-
Mbrowser/components/urlbar/tests/quicksuggest/GeolocationTestUtils.sys.mjs | 39++++++++++++++++++++++++---------------
Mbrowser/components/urlbar/tests/quicksuggest/browser/browser_weather.js | 2+-
Mbrowser/components/urlbar/tests/quicksuggest/unit/test_weather.js | 28+++++++++++++++++++++-------
Mbrowser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js | 8++++++--
Mbrowser/extensions/newtab/lib/WeatherFeed.sys.mjs | 178++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mbrowser/extensions/newtab/test/browser/browser_customize_menu_content.js | 7+++++++
Mbrowser/extensions/newtab/test/xpcshell/test_WeatherFeed.js | 222++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
8 files changed, 386 insertions(+), 102 deletions(-)

diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js @@ -487,7 +487,9 @@ add_task(async function selected_result_weather() { const cleanupQuickSuggest = await ensureQuickSuggestInit(); await MerinoTestUtils.initWeather(); - const cleanupGeolocation = GeolocationTestUtils.stubGeolocation(); + const cleanupGeolocation = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); await doTest(async () => { let provider = "UrlbarProviderQuickSuggest"; diff --git a/browser/components/urlbar/tests/quicksuggest/GeolocationTestUtils.sys.mjs b/browser/components/urlbar/tests/quicksuggest/GeolocationTestUtils.sys.mjs @@ -1,18 +1,37 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +const lazy = {}; -const lazy = XPCOMUtils.declareLazy({ - GeolocationUtils: - "moz-src:///browser/components/urlbar/private/GeolocationUtils.sys.mjs", +ChromeUtils.defineESModuleGetters(lazy, { sinon: "resource://testing-common/Sinon.sys.mjs", }); +ChromeUtils.defineLazyGetter(lazy, "GeolocationUtils", () => { + try { + return ChromeUtils.importESModule( + "moz-src:///browser/components/urlbar/private/GeolocationUtils.sys.mjs" + ).GeolocationUtils; + } catch { + // Fallback to URI format prior to FF 144. + return ChromeUtils.importESModule( + "resource:///modules/urlbar/private/GeolocationUtils.sys.mjs" + ).GeolocationUtils; + } +}); + /** * */ class _GeolocationTestUtils { + get SAN_FRANCISCO() { + return { + country_code: "US", + city: "San Francisco", + region_code: "CA", + }; + } + /** * Initializes the utils. * @@ -32,24 +51,14 @@ class _GeolocationTestUtils { /** * Setup stub for GeolocationUtils.geolocation() using given geolocation. - * If the geolocation parameter is null, set "San Francisco" geolocation. - * NOTE: This returns function to restore the dummy function. * - * @param {object} [geolocation] + * @param {object} geolocation * @param {string} [geolocation.country_code] * @param {string} [geolocation.city] * @param {string} [geolocation.region_code] * @returns {Function} function to restore the stub. */ stubGeolocation(geolocation) { - if (!geolocation) { - geolocation = { - country_code: "US", - city: "San Francisco", - region_code: "CA", - }; - } - let sandbox = lazy.sinon.createSandbox(); sandbox.stub(lazy.GeolocationUtils, "geolocation").resolves(geolocation); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js @@ -23,7 +23,7 @@ add_setup(async function () { ], }); await MerinoTestUtils.initWeather(); - GeolocationTestUtils.stubGeolocation(); + GeolocationTestUtils.stubGeolocation(GeolocationTestUtils.SAN_FRANCISCO); }); // Does a search, clicks the "Show less frequently" result menu command, and diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js @@ -71,7 +71,9 @@ add_task(async function disableAndEnable() { }); async function doBasicDisableAndEnableTest(pref) { - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); // Disable the feature. It should be immediately uninitialized. UrlbarPrefs.set(pref, false); @@ -126,7 +128,9 @@ add_task(async function noSuggestion() { // When the Merino response doesn't include a `region_code` for the geolocated // version of the suggestion, the suggestion title should only contain a city. add_task(async function geolocationSuggestionNoRegion() { - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); let { suggestions } = MerinoTestUtils.server.response.body; let s = { ...MerinoTestUtils.WEATHER_SUGGESTION }; @@ -159,7 +163,9 @@ add_task(async function geolocationSuggestionNoRegion() { // the suggestion's URL, the suggestion should be shown and the history visit // should not be shown. add_task(async function urlAlreadyInHistory() { - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); // A visit to the weather suggestion's exact URL. let suggestionVisit = { @@ -313,7 +319,9 @@ async function doLocaleTest({ shouldRunTask, osUnit, unitsByLocale }) { // enabled rather than being set according to region/locale. skipSuggestReset: true, callback: async () => { - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); info("Checking locale: " + locale); await check_results({ @@ -489,7 +497,9 @@ async function doRegionTest({ homeRegion, locale, query, expectedTitleL10n }) { // Tests dismissal. add_task(async function dismissal() { - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); await doDismissAllTest({ result: QuickSuggestTestUtils.weatherResult(), @@ -509,7 +519,9 @@ add_task(async function dismissal() { // When a Nimbus experiment is installed, it should override the remote settings // weather record. add_task(async function nimbusOverride() { - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); let defaultResult = QuickSuggestTestUtils.weatherResult(); // Verify a search works as expected with the default remote settings weather @@ -828,7 +840,9 @@ add_task(async function cityRegionQueries() { // Tests weather queries that don't include a city. add_task(async function noCityQuery() { - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); await doCityTest({ desc: "No city in query, so only one call to Merino should be made and Merino does the geolocation internally", diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js @@ -391,7 +391,9 @@ async function doKeywordsTest({ info("Doing keywords test: " + desc); info(JSON.stringify({ nimbusValues, settingsData, minKeywordLength })); - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); let nimbusCleanup; if (nimbusValues) { @@ -565,7 +567,9 @@ async function doShowLessFrequentlyTest({ info("Doing increment test: " + desc); info(JSON.stringify({ weather, configuration, nimbusValues })); - let cleanup = GeolocationTestUtils.stubGeolocation(); + let cleanup = GeolocationTestUtils.stubGeolocation( + GeolocationTestUtils.SAN_FRANCISCO + ); let nimbusCleanup; if (nimbusValues) { diff --git a/browser/extensions/newtab/lib/WeatherFeed.sys.mjs b/browser/extensions/newtab/lib/WeatherFeed.sys.mjs @@ -25,6 +25,19 @@ ChromeUtils.defineLazyGetter(lazy, "MerinoClient", () => { } }); +ChromeUtils.defineLazyGetter(lazy, "GeolocationUtils", () => { + try { + return ChromeUtils.importESModule( + "moz-src:///browser/components/urlbar/private/GeolocationUtils.sys.mjs" + ).GeolocationUtils; + } catch { + // Fallback to URI format prior to FF 144. + return ChromeUtils.importESModule( + "resource:///modules/urlbar/private/GeolocationUtils.sys.mjs" + ).GeolocationUtils; + } +}); + import { actionTypes as at, actionCreators as ac, @@ -93,37 +106,6 @@ export class WeatherFeed { this.fetchTimer = 0; } - /** - * This thin wrapper around the fetch call makes it easier for us to write - * automated tests that simulate responses. - */ - async fetchHelper(retries = 3, queryOverride = null) { - this.restartFetchTimer(); - const weatherQuery = this.store.getState().Prefs.values[PREF_WEATHER_QUERY]; - let suggestions = []; - let retry = 0; - const query = queryOverride ?? weatherQuery ?? ""; - while (retry++ < retries && suggestions.length === 0) { - try { - suggestions = await this.merino.fetch({ - query, - providers: MERINO_PROVIDER, - timeoutMs: 7000, - otherParams: { - request_type: "weather", - source: "newtab", - }, - }); - } catch (error) { - // We don't need to do anything with this right now. - } - } - - // results from the API or empty array if null - this.suggestions = suggestions ?? []; - return this.suggestions; - } - async fetch() { // Keep a handle on the `MerinoClient` instance that exists at the start of // this fetch. If fetching stops or this `Weather` instance is uninitialized @@ -133,7 +115,7 @@ export class WeatherFeed { this.merino = await this.MerinoClient(MERINO_CLIENT_KEY); } - await this.fetchHelper(); + this.suggestions = await this._fetchHelper(); if (this.suggestions.length) { const hasLocationData = @@ -227,41 +209,6 @@ export class WeatherFeed { } } - async fetchLocationByIP() { - if (!this.merino) { - this.merino = await this.MerinoClient(MERINO_CLIENT_KEY); - } - - // First we fetch the forecast through user's IP Address - // which is done by not adding in a query parameter, but keeping the "weather" request_type. - // This method is mentioned in the AccuWeather docs: - // https://apidev.accuweather.com/developers/locationsAPIguide#IPAddress - try { - const ipLocation = await this.fetchHelper(3, ""); - - const ipData = ipLocation?.[0]; - - // Second, we use the city name that came from the IP look up to get the normalized merino response - // For context, the IP lookup response does not have the complete response data we need - const locationData = await this.merino.fetch({ - query: ipData.city_name, - providers: MERINO_PROVIDER, - timeoutMs: 7000, - otherParams: { - request_type: "location", - source: "newtab", - }, - }); - - const response = locationData?.[0]?.locations?.[0]; - return response; - // return response - } catch (err) { - console.error("WeatherFeed failed to look up IP"); - return null; - } - } - async onPrefChangedAction(action) { switch (action.data.name) { case PREF_WEATHER_QUERY: @@ -334,11 +281,10 @@ export class WeatherFeed { this.store.dispatch(ac.SetPref("weather.optInAccepted", true)); this.store.dispatch(ac.SetPref("weather.optInDisplayed", false)); - const detectedLocation = await this.fetchLocationByIP(); + const detectedLocation = await this._fetchNormalizedLocation(); if (detectedLocation) { // Build the payload exactly like manual search does - this.store.dispatch( ac.BroadcastToContent({ type: at.WEATHER_LOCATION_DATA_UPDATE, @@ -361,6 +307,95 @@ export class WeatherFeed { } } } + + /** + * This thin wrapper around the fetch call makes it easier for us to write + * automated tests that simulate responses. + */ + async _fetchHelper(retries = 3, queryOverride = null) { + this.restartFetchTimer(); + + const weatherQuery = this.store.getState().Prefs.values[PREF_WEATHER_QUERY]; + const query = queryOverride ?? weatherQuery ?? ""; + const otherParams = { + request_type: "weather", + source: "newtab", + }; + + if (!query) { + let geolocation = await lazy.GeolocationUtils.geolocation(); + if (!geolocation) { + return []; + } + + if (geolocation.country_code) { + otherParams.country = geolocation.country_code; + } + let region = geolocation.region_code || geolocation.region; + if (region) { + otherParams.region = region; + } + let city = geolocation.city || geolocation.region; + if (city) { + otherParams.city = city; + } + } + + let suggestions; + let retry = 0; + + while (retry++ < retries && !suggestions?.length) { + try { + suggestions = await this.merino.fetch({ + query, + providers: MERINO_PROVIDER, + timeoutMs: 7000, + otherParams, + }); + } catch (error) { + // We don't need to do anything with this right now. + } + } + + // results from the API or empty array if null + return suggestions ?? []; + } + + async _fetchNormalizedLocation() { + const geolocation = await lazy.GeolocationUtils.geolocation(); + if (!geolocation) { + return null; + } + + // "region" might be able to be city if geolocation.city is null + const city = geolocation.city || geolocation.region; + if (!city) { + return null; + } + + if (!this.merino) { + this.merino = await this.MerinoClient(MERINO_CLIENT_KEY); + } + + try { + // We use the given city name look up to get the normalized merino response + const locationData = await this.merino.fetch({ + query: city, + providers: MERINO_PROVIDER, + timeoutMs: 7000, + otherParams: { + request_type: "location", + source: "newtab", + }, + }); + + const response = locationData?.[0]?.locations?.[0]; + return response; + } catch (err) { + console.error("WeatherFeed failed to get normalized location"); + return null; + } + } } /** @@ -368,7 +403,10 @@ export class WeatherFeed { * This makes it easier for us to write automated tests that simulate responses. */ WeatherFeed.prototype.MerinoClient = (...args) => { - return new lazy.MerinoClient(...args); + return new lazy.MerinoClient({ + allowOhttp: true, + ...args, + }); }; WeatherFeed.prototype.PersistentCache = (...args) => { return new lazy.PersistentCache(...args); diff --git a/browser/extensions/newtab/test/browser/browser_customize_menu_content.js b/browser/extensions/newtab/test/browser/browser_customize_menu_content.js @@ -9,11 +9,18 @@ const { WeatherFeed } = ChromeUtils.importESModule( ); ChromeUtils.defineESModuleGetters(this, { + GeolocationTestUtils: + "resource://testing-common/GeolocationTestUtils.sys.mjs", MerinoTestUtils: "resource://testing-common/MerinoTestUtils.sys.mjs", }); const { WEATHER_SUGGESTION } = MerinoTestUtils; +add_setup(async function () { + GeolocationTestUtils.init(this); + GeolocationTestUtils.stubGeolocation(GeolocationTestUtils.SAN_FRANCISCO); +}); + test_newtab({ async before({ pushPrefs }) { await pushPrefs( diff --git a/browser/extensions/newtab/test/xpcshell/test_WeatherFeed.js b/browser/extensions/newtab/test/xpcshell/test_WeatherFeed.js @@ -7,12 +7,15 @@ ChromeUtils.defineESModuleGetters(this, { actionCreators: "resource://newtab/common/Actions.mjs", actionTypes: "resource://newtab/common/Actions.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", + GeolocationTestUtils: + "resource://testing-common/GeolocationTestUtils.sys.mjs", MerinoTestUtils: "resource://testing-common/MerinoTestUtils.sys.mjs", WeatherFeed: "resource://newtab/lib/WeatherFeed.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", }); const { WEATHER_SUGGESTION } = MerinoTestUtils; +GeolocationTestUtils.init(this); const WEATHER_ENABLED = "browser.newtabpage.activity-stream.showWeather"; const SYS_WEATHER_ENABLED = @@ -130,8 +133,7 @@ add_task(async function test_onAction_INIT() { sandbox.stub(feed, "isEnabled").returns(true); - sandbox.stub(feed, "fetchHelper"); - feed.suggestions = [WEATHER_SUGGESTION]; + sandbox.stub(feed, "_fetchHelper").resolves([WEATHER_SUGGESTION]); feed.locationData = locationData; feed.store = { dispatch: sinon.spy(), @@ -188,8 +190,8 @@ add_task(async function test_onAction_opt_in_location_success() { }, }; - // Stub fetchLocationByIP() to simulate a successful lookup - sandbox.stub(feed, "fetchLocationByIP").resolves({ + // Stub _fetchNormalizedLocation() to simulate a successful lookup + sandbox.stub(feed, "_fetchNormalizedLocation").resolves({ localized_name: "Testville", administrative_area: "Paris", country: "FR", @@ -252,8 +254,8 @@ add_task(async function test_onAction_opt_in_no_location_found() { }, }; - // Test that fetchLocationByIP doesn't return a location - sandbox.stub(feed, "fetchLocationByIP").resolves(null); + // Test that _fetchNormalizedLocation doesn't return a location + sandbox.stub(feed, "_fetchNormalizedLocation").resolves(null); await feed.onAction({ type: actionTypes.WEATHER_USER_OPT_IN_LOCATION }); @@ -287,3 +289,211 @@ add_task(async function test_onAction_opt_in_no_location_found() { sandbox.restore(); }); + +// Test fetching weather information using GeolocationUtils.geolocation() +add_task(async function test_fetch_weather_with_geolocation() { + const TEST_DATA = [ + { + geolocation: { + country_code: "US", + region_code: "CA", + region: "Califolnia", + city: "San Francisco", + }, + expected: { + country: "US", + region: "CA", + city: "San Francisco", + }, + }, + { + geolocation: { + country_code: "JP", + region_code: "14", + region: "Kanagawa", + city: "", + }, + expected: { + country: "JP", + region: "14", + city: "Kanagawa", + }, + }, + { + geolocation: { + country_code: "TestCountry", + region_code: "", + region: "TestRegion", + city: "TestCity", + }, + expected: { + country: "TestCountry", + region: "TestRegion", + city: "TestCity", + }, + }, + { + geolocation: { + country_code: "TestCountry", + }, + expected: { + country: "TestCountry", + }, + }, + { + geolocation: { + region_code: "TestRegionCode", + }, + expected: { + region: "TestRegionCode", + }, + }, + { + geolocation: { + region: "TestRegion", + }, + expected: { + region: "TestRegion", + city: "TestRegion", + }, + }, + { + geolocation: { + city: "TestCity", + }, + expected: { + city: "TestCity", + }, + }, + { + geolocation: {}, + expected: {}, + }, + { + geolocation: null, + expected: false, + }, + ]; + + for (let { geolocation, expected } of TEST_DATA) { + info(`Test for ${JSON.stringify(geolocation)}`); + + let sandbox = sinon.createSandbox(); + sandbox.stub(WeatherFeed.prototype, "PersistentCache").returns({ + set: () => {}, + get: () => {}, + }); + + let feed = new WeatherFeed(); + sandbox.stub(feed, "isEnabled").returns(true); + feed.store = { + dispatch: sinon.spy(), + getState() { + return { Prefs: { values: {} } }; + }, + }; + feed.merino = { fetch: () => {} }; + + // Stub merino client + let stub = sandbox.stub(feed.merino, "fetch").resolves(["result"]); + let cleanupGeolocationStub = + GeolocationTestUtils.stubGeolocation(geolocation); + + await feed.onAction({ type: actionTypes.SYSTEM_TICK }); + + if (expected) { + sinon.assert.calledOnce(stub); + sinon.assert.calledWith(stub, { + otherParams: { request_type: "weather", source: "newtab", ...expected }, + providers: ["accuweather"], + query: "", + timeoutMs: 7000, + }); + } else { + sinon.assert.notCalled(stub); + } + + await cleanupGeolocationStub(); + sandbox.restore(); + } +}); + +// Test detecting location using GeolocationUtils.geolocation() +add_task(async function test_detect_location_with_geolocation() { + const TEST_DATA = [ + { + geolocation: { + city: "San Francisco", + }, + expected: "San Francisco", + }, + { + geolocation: { + city: "", + region: "Yokohama", + }, + expected: "Yokohama", + }, + { + geolocation: { + region: "Tokyo", + }, + expected: "Tokyo", + }, + { + geolocation: { + city: "", + region: "", + }, + expected: false, + }, + { + geolocation: {}, + expected: false, + }, + { + geolocation: null, + expected: false, + }, + ]; + for (let { geolocation, expected } of TEST_DATA) { + info(`Test for ${JSON.stringify(geolocation)}`); + + let sandbox = sinon.createSandbox(); + sandbox.stub(WeatherFeed.prototype, "PersistentCache").returns({ + set: () => {}, + get: () => {}, + }); + + let feed = new WeatherFeed(); + feed.store = { + dispatch: sinon.spy(), + getState() { + return { Prefs: { values: {} } }; + }, + }; + feed.merino = { fetch: () => {} }; + + // Stub merino client + let stub = sandbox.stub(feed.merino, "fetch").resolves(null); + // Stub geolocation + let cleanupGeolocationStub = + GeolocationTestUtils.stubGeolocation(geolocation); + await feed.onAction({ type: actionTypes.WEATHER_USER_OPT_IN_LOCATION }); + + if (expected) { + sinon.assert.calledOnce(stub); + sinon.assert.calledWith(stub, { + otherParams: { request_type: "location", source: "newtab" }, + providers: ["accuweather"], + query: expected, + timeoutMs: 7000, + }); + } else { + sinon.assert.notCalled(stub); + } + + await cleanupGeolocationStub(); + sandbox.restore(); + } +});