QuickSuggestTestUtils.sys.mjs (50044B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* eslint-disable jsdoc/require-param */ 5 6 const lazy = {}; 7 8 ChromeUtils.defineESModuleGetters(lazy, { 9 AmpSuggestions: 10 "moz-src:///browser/components/urlbar/private/AmpSuggestions.sys.mjs", 11 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", 12 NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs", 13 QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs", 14 Region: "resource://gre/modules/Region.sys.mjs", 15 RemoteSettingsServer: 16 "resource://testing-common/RemoteSettingsServer.sys.mjs", 17 SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", 18 SharedRemoteSettingsService: 19 "resource://gre/modules/RustSharedRemoteSettingsService.sys.mjs", 20 Suggestion: 21 "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs", 22 TestUtils: "resource://testing-common/TestUtils.sys.mjs", 23 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 24 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 25 YelpSubjectType: 26 "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs", 27 setTimeout: "resource://gre/modules/Timer.sys.mjs", 28 }); 29 30 /** 31 * @import {Assert} from "resource://testing-common/Assert.sys.mjs" 32 */ 33 34 let gTestScope; 35 36 // Test utils singletons need special handling. Since they are uninitialized in 37 // cleanup functions, they must be re-initialized on each new test. That does 38 // not happen automatically inside system modules like this one because system 39 // module lifetimes are the app's lifetime, unlike individual browser chrome and 40 // xpcshell tests. 41 Object.defineProperty(lazy, "UrlbarTestUtils", { 42 get: () => { 43 // eslint-disable-next-line mozilla/valid-lazy 44 if (!lazy._UrlbarTestUtils) { 45 const { UrlbarTestUtils: module } = ChromeUtils.importESModule( 46 "resource://testing-common/UrlbarTestUtils.sys.mjs" 47 ); 48 module.init(gTestScope); 49 gTestScope.registerCleanupFunction(() => { 50 // Make sure the utils are re-initialized during the next test. 51 // eslint-disable-next-line mozilla/valid-lazy 52 lazy._UrlbarTestUtils = null; 53 }); 54 // eslint-disable-next-line mozilla/valid-lazy 55 lazy._UrlbarTestUtils = module; 56 } 57 // eslint-disable-next-line mozilla/valid-lazy 58 return lazy._UrlbarTestUtils; 59 }, 60 }); 61 62 // Test utils singletons need special handling. Since they are uninitialized in 63 // cleanup functions, they must be re-initialized on each new test. That does 64 // not happen automatically inside system modules like this one because system 65 // module lifetimes are the app's lifetime, unlike individual browser chrome and 66 // xpcshell tests. 67 Object.defineProperty(lazy, "MerinoTestUtils", { 68 get: () => { 69 // eslint-disable-next-line mozilla/valid-lazy 70 if (!lazy._MerinoTestUtils) { 71 const { MerinoTestUtils: module } = ChromeUtils.importESModule( 72 "resource://testing-common/MerinoTestUtils.sys.mjs" 73 ); 74 module.init(gTestScope); 75 gTestScope.registerCleanupFunction(() => { 76 // Make sure the utils are re-initialized during the next test. 77 // eslint-disable-next-line mozilla/valid-lazy 78 lazy._MerinoTestUtils = null; 79 }); 80 // eslint-disable-next-line mozilla/valid-lazy 81 lazy._MerinoTestUtils = module; 82 } 83 // eslint-disable-next-line mozilla/valid-lazy 84 return lazy._MerinoTestUtils; 85 }, 86 }); 87 88 // TODO bug 1881409: Previously this was an empty object, but the Rust backend 89 // seems to persist old config after ingesting an empty config object. 90 const DEFAULT_CONFIG = { 91 // Zero means there is no cap, the same as if this wasn't specified at all. 92 show_less_frequently_cap: 0, 93 }; 94 95 // The following properties and methods are copied from the test scope to the 96 // test utils object so they can be easily accessed. Be careful about assuming a 97 // particular property will be defined because depending on the scope -- browser 98 // test or xpcshell test -- some may not be. 99 const TEST_SCOPE_PROPERTIES = [ 100 "Assert", 101 "EventUtils", 102 "info", 103 "registerCleanupFunction", 104 ]; 105 106 /** @typedef {() => Promise<void>} cleanupFunctionType */ 107 108 /** 109 * Test utils for quick suggest. 110 */ 111 class _QuickSuggestTestUtils { 112 /** @type {Assert} */ 113 Assert = undefined; 114 115 /** @type {object} */ 116 EventUtils = undefined; 117 118 /** @type {(message:string) => void} */ 119 info = undefined; 120 121 /** @type {(cleanupFn: cleanupFunctionType) => void} */ 122 registerCleanupFunction = undefined; 123 124 /** 125 * Initializes the utils. 126 * 127 * @param {object} scope 128 * The global JS scope where tests are being run. This allows the instance 129 * to access test helpers like `Assert` that are available in the scope. 130 */ 131 init(scope) { 132 if (!scope) { 133 throw new Error("QuickSuggestTestUtils() must be called with a scope"); 134 } 135 gTestScope = scope; 136 for (let p of TEST_SCOPE_PROPERTIES) { 137 this[p] = scope[p]; 138 } 139 // If you add other properties to `this`, null them in `uninit()`. 140 141 scope.registerCleanupFunction?.(() => this.uninit()); 142 } 143 144 /** 145 * Uninitializes the utils. If they were created with a test scope that 146 * defines `registerCleanupFunction()`, you don't need to call this yourself 147 * because it will automatically be called as a cleanup function. Otherwise 148 * you'll need to call this. 149 */ 150 uninit() { 151 gTestScope = null; 152 for (let p of TEST_SCOPE_PROPERTIES) { 153 this[p] = null; 154 } 155 } 156 157 get RS_COLLECTION() { 158 return { 159 AMP: "quicksuggest-amp", 160 OTHER: "quicksuggest-other", 161 }; 162 } 163 164 get RS_TYPE() { 165 return { 166 AMP: "amp", 167 WIKIPEDIA: "wikipedia", 168 }; 169 } 170 171 get DEFAULT_CONFIG() { 172 // Return a clone so callers can modify it. 173 return Cu.cloneInto(DEFAULT_CONFIG, this); 174 } 175 176 /** 177 * Sets up local remote settings and Merino servers, registers test 178 * suggestions, and initializes Suggest. 179 * 180 * @param {object} [options] 181 * Options object 182 * @param {Array} [options.remoteSettingsRecords] 183 * Array of remote settings records. Each item in this array should be a 184 * realistic remote settings record with some exceptions as noted below. 185 * For details see `RemoteSettingsServer.addRecords()`. 186 * - `record.attachment` - Optional. This should be the attachment itself 187 * and not its metadata. It should be a JSONable object. 188 * - `record.collection` - Optional. The name of the RS collection that 189 * the record should be added to. Defaults to "quicksuggest-other". 190 * @param {Array} [options.merinoSuggestions] 191 * Array of Merino suggestion objects. If given, this function will start 192 * the mock Merino server and set appropriate online prefs so that Suggest 193 * will fetch suggestions from it. Otherwise Merino will not serve 194 * suggestions, but you can still set up Merino without using this function 195 * by using `MerinoTestUtils` directly. 196 * @param {object} [options.config] 197 * The Suggest configuration object. This should not be the full remote 198 * settings record; only pass the object that should be set to the nested 199 * `configuration` object inside the record. 200 * @param {Array} [options.prefs] 201 * An array of Suggest-related prefs to set. This is useful because setting 202 * some prefs, like feature gates, can cause Suggest to sync from remote 203 * settings; this function will set them, wait for sync to finish, and clear 204 * them when the cleanup function is called. Each item in this array should 205 * itself be a two-element array `[prefName, prefValue]` similar to the 206 * `set` array passed to `SpecialPowers.pushPrefEnv()`, except here pref 207 * names are relative to `browser.urlbar`. 208 * @returns {Promise<(() => void) | (() => Promise<void>)>} 209 * An async cleanup function. This function is automatically registered as a 210 * cleanup function, so you only need to call it if your test needs to clean 211 * up Suggest before it ends, for example if you have a small number of 212 * tasks that need Suggest and it's not enabled throughout your test. The 213 * cleanup function is idempotent so there's no harm in calling it more than 214 * once. Be sure to `await` it. 215 */ 216 async ensureQuickSuggestInit({ 217 remoteSettingsRecords = [], 218 merinoSuggestions = null, 219 config = DEFAULT_CONFIG, 220 prefs = [], 221 } = {}) { 222 this.#log("ensureQuickSuggestInit", "Started"); 223 224 this.#log("ensureQuickSuggestInit", "Awaiting ExperimentAPI.init"); 225 const initializedExperimentAPI = await lazy.ExperimentAPI.init(); 226 227 this.#log("ensureQuickSuggestInit", "Awaiting ExperimentAPI.ready"); 228 await lazy.ExperimentAPI.ready(); 229 230 // Set up the local remote settings server. 231 this.#log("ensureQuickSuggestInit", "Preparing remote settings server"); 232 if (!this.#remoteSettingsServer) { 233 this.#remoteSettingsServer = new lazy.RemoteSettingsServer(); 234 } 235 236 this.#remoteSettingsServer.removeRecords(); 237 for (let [collection, records] of this.#recordsByCollection( 238 remoteSettingsRecords 239 )) { 240 await this.#remoteSettingsServer.addRecords({ collection, records }); 241 } 242 await this.#remoteSettingsServer.addRecords({ 243 collection: this.RS_COLLECTION.OTHER, 244 records: [{ type: "configuration", configuration: config }], 245 }); 246 247 this.#log("ensureQuickSuggestInit", "Starting remote settings server"); 248 await this.#remoteSettingsServer.start(); 249 this.#log("ensureQuickSuggestInit", "Remote settings server started"); 250 251 // Init Suggest and force the region to US and the locale to en-US, which 252 // will cause Suggest to be enabled along with all suggestion types that are 253 // enabled in the US by default. Do this after setting up remote settings 254 // because the Rust backend will immediately try to sync. 255 this.#log( 256 "ensureQuickSuggestInit", 257 "Calling QuickSuggest.init() and setting prefs" 258 ); 259 await lazy.QuickSuggest.init({ region: "US", locale: "en-US" }); 260 261 // Set prefs requested by the caller. 262 for (let [name, value] of prefs) { 263 lazy.UrlbarPrefs.set(name, value); 264 } 265 266 // Tell the Rust backend to use the local remote setting server. 267 lazy.SharedRemoteSettingsService.updateServer({ 268 url: this.#remoteSettingsServer.url.toString(), 269 bucketName: "main", 270 }); 271 await lazy.QuickSuggest.rustBackend._test_setRemoteSettingsService( 272 lazy.SharedRemoteSettingsService.rustService() 273 ); 274 275 // Wait for the Rust backend to finish syncing. 276 await this.forceSync(); 277 278 // Set up Merino. This can happen any time relative to Suggest init. 279 if (merinoSuggestions) { 280 this.#log("ensureQuickSuggestInit", "Setting up Merino server"); 281 await lazy.MerinoTestUtils.server.start(); 282 lazy.MerinoTestUtils.server.response.body.suggestions = merinoSuggestions; 283 lazy.UrlbarPrefs.set("quicksuggest.online.available", true); 284 lazy.UrlbarPrefs.set("quicksuggest.online.enabled", true); 285 this.#log("ensureQuickSuggestInit", "Done setting up Merino server"); 286 } 287 288 let cleanupCalled = false; 289 let cleanup = async () => { 290 if (!cleanupCalled) { 291 cleanupCalled = true; 292 await this.#uninitQuickSuggest(prefs, !!merinoSuggestions); 293 294 if (initializedExperimentAPI) { 295 // Only reset if we're in an xpcshell-test and actually initialized 296 // the ExperimentAPI. 297 lazy.ExperimentAPI._resetForTests(); 298 } 299 } 300 }; 301 this.registerCleanupFunction?.(cleanup); 302 303 this.#log("ensureQuickSuggestInit", "Done"); 304 return cleanup; 305 } 306 307 async #uninitQuickSuggest(prefs, clearOnlinePrefs) { 308 this.#log("#uninitQuickSuggest", "Started"); 309 310 // Reset prefs, which can cause the Rust backend to start syncing. Wait for 311 // it to finish. 312 for (let [name] of prefs) { 313 lazy.UrlbarPrefs.clear(name); 314 } 315 await this.forceSync(); 316 317 this.#log("#uninitQuickSuggest", "Stopping remote settings server"); 318 await this.#remoteSettingsServer.stop(); 319 320 if (clearOnlinePrefs) { 321 lazy.UrlbarPrefs.clear("quicksuggest.online.available"); 322 lazy.UrlbarPrefs.clear("quicksuggest.online.enabled"); 323 } 324 325 await lazy.QuickSuggest.rustBackend._test_setRemoteSettingsService(null); 326 327 this.#log("#uninitQuickSuggest", "Done"); 328 } 329 330 /** 331 * Removes all records from the local remote settings server and adds a new 332 * batch of records. 333 * 334 * @param {Array} records 335 * Array of remote settings records. See `ensureQuickSuggestInit()`. 336 * @param {object} [options] 337 * Options object. 338 * @param {boolean} [options.forceSync] 339 * Whether to force Suggest to sync after updating the records. 340 */ 341 async setRemoteSettingsRecords(records, { forceSync = true } = {}) { 342 this.#log("setRemoteSettingsRecords", "Started"); 343 344 this.#remoteSettingsServer.removeRecords(); 345 for (let [collection, recs] of this.#recordsByCollection(records)) { 346 await this.#remoteSettingsServer.addRecords({ 347 collection, 348 records: recs, 349 }); 350 } 351 352 if (forceSync) { 353 this.#log("setRemoteSettingsRecords", "Forcing sync"); 354 await this.forceSync(); 355 } 356 this.#log("setRemoteSettingsRecords", "Done"); 357 } 358 359 /** 360 * Sets the quick suggest configuration. You should call this again with 361 * `DEFAULT_CONFIG` before your test finishes. See also `withConfig()`. 362 * 363 * @param {object} config 364 * The quick suggest configuration object. This should not be the full 365 * remote settings record; only pass the object that should be set to the 366 * `configuration` nested object inside the record. 367 */ 368 async setConfig(config) { 369 this.#log("setConfig", "Started"); 370 let type = "configuration"; 371 this.#remoteSettingsServer.removeRecords({ type }); 372 await this.#remoteSettingsServer.addRecords({ 373 collection: this.RS_COLLECTION.OTHER, 374 records: [{ type, configuration: config }], 375 }); 376 this.#log("setConfig", "Forcing sync"); 377 await this.forceSync(); 378 this.#log("setConfig", "Done"); 379 } 380 381 /** 382 * Forces Suggest to sync with remote settings. This can be used to ensure 383 * Suggest has finished all sync activity. 384 */ 385 async forceSync() { 386 this.#log("forceSync", "Started"); 387 if (lazy.QuickSuggest.rustBackend.isEnabled) { 388 this.#log("forceSync", "Syncing Rust backend"); 389 await lazy.QuickSuggest.rustBackend._test_ingest(); 390 this.#log("forceSync", "Done syncing Rust backend"); 391 } 392 this.#log("forceSync", "Done"); 393 } 394 395 /** 396 * Sets the quick suggest configuration, calls your callback, and restores the 397 * previous configuration. 398 * 399 * @param {object} options 400 * The options object. 401 * @param {object} options.config 402 * The configuration that should be used with the callback 403 * @param {Function} options.callback 404 * Will be called with the configuration applied 405 * 406 * @see {@link setConfig} 407 */ 408 async withConfig({ config, callback }) { 409 let original = lazy.QuickSuggest.config; 410 await this.setConfig(config); 411 await callback(); 412 await this.setConfig(original); 413 } 414 415 /** 416 * Returns an AMP (sponsored) suggestion suitable for storing in a remote 417 * settings attachment. 418 * 419 * @returns {object} 420 * An AMP suggestion for storing in remote settings. 421 */ 422 ampRemoteSettings({ 423 keywords = ["amp"], 424 full_keywords = keywords.map(kw => [kw, 1]), 425 url = "https://example.com/amp", 426 title = "Amp Suggestion", 427 score = 0.3, 428 } = {}) { 429 return { 430 keywords, 431 full_keywords, 432 url, 433 title, 434 score, 435 id: 1, 436 click_url: "https://example.com/amp-click", 437 impression_url: "https://example.com/amp-impression", 438 advertiser: "Amp", 439 iab_category: "22 - Shopping", 440 icon: "1234", 441 }; 442 } 443 444 /** 445 * Returns an expected AMP (sponsored) result that can be passed to 446 * `check_results()` in xpcshell tests. 447 * 448 * @returns {object} 449 * An object that can be passed to `check_results()`. 450 */ 451 ampResult({ 452 source = "rust", 453 provider = "Amp", 454 keyword = "amp", 455 fullKeyword = keyword, 456 title = "Amp Suggestion", 457 url = "https://example.com/amp", 458 originalUrl = url, 459 icon = null, 460 iconBlob = null, 461 impressionUrl = "https://example.com/amp-impression", 462 clickUrl = "https://example.com/amp-click", 463 blockId = 1, 464 advertiser = "Amp", 465 iabCategory = "22 - Shopping", 466 // Note that many callers use -1 here because they test without 467 // the search suggestion provider. 468 suggestedIndex = 0, 469 isSuggestedIndexRelativeToGroup = true, 470 isBestMatch = false, 471 requestId = undefined, 472 dismissalKey = undefined, 473 descriptionL10n = { id: "urlbar-result-action-sponsored" }, 474 categories = [], 475 } = {}) { 476 let result = { 477 suggestedIndex, 478 isSuggestedIndexRelativeToGroup, 479 isBestMatch, 480 type: lazy.UrlbarUtils.RESULT_TYPE.URL, 481 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 482 heuristic: false, 483 payload: { 484 title: fullKeyword ? `${fullKeyword} — ${title}` : title, 485 url, 486 originalUrl, 487 requestId, 488 source, 489 provider, 490 isSponsored: true, 491 sponsoredImpressionUrl: impressionUrl, 492 sponsoredClickUrl: clickUrl, 493 sponsoredBlockId: blockId, 494 sponsoredAdvertiser: advertiser, 495 sponsoredIabCategory: iabCategory, 496 isBlockable: true, 497 isManageable: true, 498 telemetryType: "adm_sponsored", 499 }, 500 }; 501 502 if (descriptionL10n) { 503 result.payload.descriptionL10n = descriptionL10n; 504 } 505 506 if (result.payload.source == "rust") { 507 result.payload.iconBlob = iconBlob; 508 result.payload.suggestionObject = new lazy.Suggestion.Amp({ 509 title, 510 url, 511 rawUrl: originalUrl, 512 icon: null, 513 iconMimetype: null, 514 fullKeyword, 515 blockId, 516 advertiser, 517 iabCategory, 518 categories, 519 impressionUrl, 520 clickUrl, 521 rawClickUrl: clickUrl, 522 score: 0.3, 523 ftsMatchInfo: null, 524 }); 525 } else { 526 result.payload.icon = icon; 527 if (typeof dismissalKey == "string") { 528 result.payload.dismissalKey = dismissalKey; 529 } 530 } 531 532 return result; 533 } 534 535 /** 536 * Returns a Wikipedia (non-sponsored) suggestion suitable for storing in a 537 * remote settings attachment. 538 * 539 * @returns {object} 540 * A Wikipedia suggestion for storing in remote settings. 541 */ 542 wikipediaRemoteSettings({ 543 keywords = ["wikipedia"], 544 url = "https://example.com/wikipedia", 545 title = "Wikipedia Suggestion", 546 score = 0.2, 547 } = {}) { 548 return { 549 keywords, 550 url, 551 title, 552 score, 553 id: 2, 554 click_url: "https://example.com/wikipedia-click", 555 impression_url: "https://example.com/wikipedia-impression", 556 advertiser: "Wikipedia", 557 iab_category: "5 - Education", 558 icon: "1234", 559 }; 560 } 561 562 /** 563 * Returns an expected Wikipedia result that can be passed to 564 * `check_results()` in xpcshell tests. 565 * 566 * @returns {object} 567 * An object that can be passed to `check_results()`. 568 */ 569 wikipediaResult({ 570 source = "rust", 571 provider = "Wikipedia", 572 keyword = "wikipedia", 573 fullKeyword = keyword, 574 title = "Wikipedia Suggestion", 575 url = "https://example.com/wikipedia", 576 icon = null, 577 iconBlob = null, 578 suggestedIndex = -1, 579 isSuggestedIndexRelativeToGroup = true, 580 telemetryType = "adm_nonsponsored", 581 } = {}) { 582 let result = { 583 suggestedIndex, 584 isSuggestedIndexRelativeToGroup, 585 type: lazy.UrlbarUtils.RESULT_TYPE.URL, 586 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 587 heuristic: false, 588 payload: { 589 title: fullKeyword ? `${fullKeyword} — ${title}` : title, 590 url, 591 icon, 592 iconBlob, 593 source, 594 provider, 595 telemetryType, 596 isSponsored: false, 597 isBlockable: true, 598 isManageable: true, 599 }, 600 }; 601 602 if (source == "rust") { 603 result.payload.suggestionObject = new lazy.Suggestion.Wikipedia({ 604 title, 605 url, 606 icon: null, 607 iconMimeType: null, 608 fullKeyword, 609 }); 610 } 611 612 return result; 613 } 614 615 /** 616 * Returns an AMO (addons) suggestion suitable for storing in a remote 617 * settings attachment. 618 * 619 * @returns {object} 620 * An AMO suggestion for storing in remote settings. 621 */ 622 amoRemoteSettings({ 623 keywords = ["amo"], 624 url = "https://example.com/amo", 625 title = "Amo Suggestion", 626 score = 0.2, 627 } = {}) { 628 return { 629 keywords, 630 url, 631 title, 632 score, 633 guid: "amo-suggestion@example.com", 634 icon: "https://example.com/addon.svg", 635 rating: "4.7", 636 description: "Addon with score", 637 number_of_ratings: 1256, 638 }; 639 } 640 641 /** 642 * Returns a remote settings weather record. 643 * 644 * @returns {object} 645 * A weather record for storing in remote settings. 646 */ 647 weatherRecord({ 648 keywords = ["weather"], 649 min_keyword_length = undefined, 650 score = 0.29, 651 } = {}) { 652 return { 653 type: "weather", 654 attachment: { 655 keywords, 656 min_keyword_length, 657 score, 658 }, 659 }; 660 } 661 662 /** 663 * Returns remote settings records containing geonames populated with some 664 * cities. 665 * 666 * @returns {Array} 667 * One or more geonames records for storing in remote settings. 668 */ 669 geonamesRecords() { 670 let geonames = [ 671 // United States 672 { 673 id: 6252001, 674 name: "United States", 675 feature_class: "A", 676 feature_code: "PCLI", 677 country: "US", 678 admin1: "00", 679 population: 327167434, 680 latitude: "39.76", 681 longitude: "-98.5", 682 }, 683 // Waterloo, AL 684 { 685 id: 4096497, 686 name: "Waterloo", 687 feature_class: "P", 688 feature_code: "PPL", 689 country: "US", 690 admin1: "AL", 691 admin2: "077", 692 population: 200, 693 latitude: "34.91814", 694 longitude: "-88.0642", 695 }, 696 // AL 697 { 698 id: 4829764, 699 name: "Alabama", 700 feature_class: "A", 701 feature_code: "ADM1", 702 country: "US", 703 admin1: "AL", 704 population: 4530315, 705 latitude: "32.75041", 706 longitude: "-86.75026", 707 }, 708 // Waterloo, IA 709 { 710 id: 4880889, 711 name: "Waterloo", 712 feature_class: "P", 713 feature_code: "PPLA2", 714 country: "US", 715 admin1: "IA", 716 admin2: "013", 717 admin3: "94597", 718 population: 68460, 719 latitude: "42.49276", 720 longitude: "-92.34296", 721 }, 722 // IA 723 { 724 id: 4862182, 725 name: "Iowa", 726 feature_class: "A", 727 feature_code: "ADM1", 728 country: "US", 729 admin1: "IA", 730 population: 2955010, 731 latitude: "42.00027", 732 longitude: "-93.50049", 733 }, 734 735 // Made-up cities with the same name in the US and CA. The CA city has a 736 // larger population. 737 { 738 id: 100, 739 name: "US CA City", 740 feature_class: "P", 741 feature_code: "PPL", 742 country: "US", 743 admin1: "IA", 744 population: 1, 745 latitude: "38.06084", 746 longitude: "-97.92977", 747 }, 748 { 749 id: 101, 750 name: "US CA City", 751 feature_class: "P", 752 feature_code: "PPL", 753 country: "CA", 754 admin1: "08", 755 population: 2, 756 latitude: "45.50884", 757 longitude: "-73.58781", 758 }, 759 760 // Made-up cities that are only ~1.5 km apart. 761 { 762 id: 102, 763 name: "Twin City A", 764 feature_class: "P", 765 feature_code: "PPL", 766 country: "US", 767 admin1: "GA", 768 population: 1, 769 latitude: "33.748889", 770 longitude: "-84.39", 771 }, 772 { 773 id: 103, 774 name: "Twin City B", 775 feature_class: "P", 776 feature_code: "PPL", 777 country: "US", 778 admin1: "GA", 779 population: 2, 780 latitude: "33.76", 781 longitude: "-84.4", 782 }, 783 784 // Tokyo 785 { 786 id: 1850147, 787 name: "Tokyo", 788 feature_class: "P", 789 feature_code: "PPLC", 790 country: "JP", 791 admin1: "Tokyo-to", 792 population: 9733276, 793 latitude: "35.6895", 794 longitude: "139.69171", 795 }, 796 797 // UK 798 { 799 id: 2635167, 800 name: "United Kingdom of Great Britain and Northern Ireland", 801 feature_class: "A", 802 feature_code: "PCLI", 803 country: "GB", 804 admin1: "00", 805 population: 66488991, 806 latitude: "54.75844", 807 longitude: "-2.69531", 808 }, 809 // England 810 { 811 id: 6269131, 812 name: "England", 813 feature_class: "A", 814 feature_code: "ADM1", 815 country: "GB", 816 admin1: "ENG", 817 population: 57106398, 818 latitude: "52.16045", 819 longitude: "-0.70312", 820 }, 821 // Liverpool (metropolitan borough, admin2 for Liverpool city) 822 { 823 id: 3333167, 824 name: "Liverpool", 825 feature_class: "A", 826 feature_code: "ADM2", 827 country: "GB", 828 admin1: "ENG", 829 admin2: "H8", 830 population: 484578, 831 latitude: "53.41667", 832 longitude: "-2.91667", 833 }, 834 // Liverpool (city) 835 { 836 id: 2644210, 837 name: "Liverpool", 838 feature_class: "P", 839 feature_code: "PPLA2", 840 country: "GB", 841 admin1: "ENG", 842 admin2: "H8", 843 population: 864122, 844 latitude: "53.41058", 845 longitude: "-2.97794", 846 }, 847 ]; 848 849 return [ 850 { 851 type: "geonames-2", 852 attachment: geonames, 853 }, 854 ]; 855 } 856 857 /** 858 * Returns remote settings records containing geonames alternates (alternate 859 * names) populated with some names. 860 * 861 * @returns {Array} 862 * One or more geonames alternates records for storing in remote settings. 863 */ 864 geonamesAlternatesRecords() { 865 return [ 866 { 867 type: "geonames-alternates", 868 attachment: [ 869 { 870 language: "abbr", 871 alternates_by_geoname_id: [ 872 [4829764, ["AL"]], 873 [4862182, ["IA"]], 874 [2635167, ["UK"]], 875 ], 876 }, 877 ], 878 }, 879 { 880 type: "geonames-alternates", 881 attachment: [ 882 { 883 language: "en", 884 alternates_by_geoname_id: [ 885 [ 886 2635167, 887 [ 888 { 889 name: "United Kingdom", 890 is_preferred: true, 891 is_short: true, 892 }, 893 ], 894 ], 895 ], 896 }, 897 ], 898 }, 899 ]; 900 } 901 902 /** 903 * Returns an expected AMO (addons) result that can be passed to 904 * `check_results()` in xpcshell tests. 905 * 906 * @returns {object} 907 * An object that can be passed to `check_results()`. 908 */ 909 amoResult({ 910 source = "rust", 911 provider = "Amo", 912 title = "Amo Suggestion", 913 description = "Amo description", 914 url = "https://example.com/amo", 915 originalUrl = "https://example.com/amo", 916 icon = null, 917 setUtmParams = true, 918 }) { 919 if (setUtmParams) { 920 let parsedUrl = new URL(url); 921 parsedUrl.searchParams.set("utm_medium", "firefox-desktop"); 922 parsedUrl.searchParams.set("utm_source", "firefox-suggest"); 923 url = parsedUrl.href; 924 } 925 926 let result = { 927 isBestMatch: true, 928 suggestedIndex: 1, 929 type: lazy.UrlbarUtils.RESULT_TYPE.URL, 930 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 931 heuristic: false, 932 payload: { 933 source, 934 provider, 935 title, 936 description, 937 url, 938 originalUrl, 939 icon, 940 isSponsored: false, 941 shouldShowUrl: true, 942 bottomTextL10n: { 943 id: "firefox-suggest-addons-recommended", 944 }, 945 helpUrl: lazy.QuickSuggest.HELP_URL, 946 telemetryType: "amo", 947 }, 948 }; 949 950 if (source == "rust") { 951 result.payload.suggestionObject = new lazy.Suggestion.Amo({ 952 title, 953 url: originalUrl, 954 iconUrl: icon, 955 description, 956 rating: "4.7", 957 numberOfRatings: 1, 958 guid: "amo-suggestion@example.com", 959 score: 0.2, 960 }); 961 } 962 963 return result; 964 } 965 966 /** 967 * Returns an expected MDN result that can be passed to `check_results()` in 968 * xpcshell tests. 969 * 970 * @returns {object} 971 * An object that can be passed to `check_results()`. 972 */ 973 mdnResult({ url, title, description }) { 974 let finalUrl = new URL(url); 975 finalUrl.searchParams.set("utm_medium", "firefox-desktop"); 976 finalUrl.searchParams.set("utm_source", "firefox-suggest"); 977 finalUrl.searchParams.set( 978 "utm_campaign", 979 "firefox-mdn-web-docs-suggestion-experiment" 980 ); 981 finalUrl.searchParams.set("utm_content", "treatment"); 982 983 return { 984 isBestMatch: true, 985 suggestedIndex: 1, 986 type: lazy.UrlbarUtils.RESULT_TYPE.URL, 987 source: lazy.UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK, 988 heuristic: false, 989 payload: { 990 telemetryType: "mdn", 991 title, 992 url: finalUrl.href, 993 originalUrl: url, 994 isSponsored: false, 995 description, 996 icon: "chrome://global/skin/icons/mdn.svg", 997 shouldShowUrl: true, 998 bottomTextL10n: { 999 id: "firefox-suggest-mdn-bottom-text", 1000 }, 1001 source: "rust", 1002 provider: "Mdn", 1003 suggestionObject: new lazy.Suggestion.Mdn({ 1004 title, 1005 url, 1006 description, 1007 score: 0.2, 1008 }), 1009 }, 1010 }; 1011 } 1012 1013 /** 1014 * Returns an expected Yelp result that can be passed to `check_results()` in 1015 * xpcshell tests. 1016 * 1017 * @returns {object} 1018 * An object that can be passed to `check_results()`. 1019 */ 1020 yelpResult({ 1021 url, 1022 title = undefined, 1023 titleL10n = undefined, 1024 source = "rust", 1025 provider = "Yelp", 1026 isTopPick = false, 1027 // The logic for the default Yelp `suggestedIndex` is complex and depends on 1028 // whether `UrlbarProviderSearchSuggestions` is active and whether search 1029 // suggestions are shown first. By default -- when the answer to both of 1030 // those questions is Yes -- Yelp's `suggestedIndex` is 0. 1031 suggestedIndex = 0, 1032 isSuggestedIndexRelativeToGroup = true, 1033 originalUrl = undefined, 1034 suggestedType = lazy.YelpSubjectType.SERVICE, 1035 }) { 1036 const utmParameters = "&utm_medium=partner&utm_source=mozilla"; 1037 1038 originalUrl ??= url; 1039 originalUrl = new URL(originalUrl); 1040 originalUrl.searchParams.delete("find_loc"); 1041 originalUrl = originalUrl.toString(); 1042 1043 url += utmParameters; 1044 1045 if (isTopPick) { 1046 suggestedIndex = 1; 1047 isSuggestedIndexRelativeToGroup = false; 1048 } 1049 1050 let result = { 1051 type: lazy.UrlbarUtils.RESULT_TYPE.URL, 1052 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 1053 isBestMatch: !!isTopPick, 1054 suggestedIndex, 1055 isSuggestedIndexRelativeToGroup, 1056 heuristic: false, 1057 payload: { 1058 source, 1059 provider, 1060 telemetryType: "yelp", 1061 bottomTextL10n: { 1062 id: "firefox-suggest-yelp-bottom-text", 1063 }, 1064 url, 1065 originalUrl, 1066 title, 1067 titleL10n, 1068 icon: null, 1069 isSponsored: true, 1070 }, 1071 }; 1072 1073 if (source == "rust") { 1074 result.payload.suggestionObject = new lazy.Suggestion.Yelp({ 1075 url: originalUrl, 1076 // `title` will be undefined if the caller passed in `titleL10n` 1077 // instead, but the Rust suggestion must be created with a string title. 1078 // The Rust suggestion title doesn't actually matter since no test 1079 // relies on it directly or indirectly. Pick an arbitrary string, and 1080 // make it distinctive so it's easier to track down bugs in case it does 1081 // start to matter at some point. 1082 title: title ?? "<QuickSuggestTestUtils Yelp suggestion>", 1083 icon: null, 1084 iconMimeType: null, 1085 score: 0.2, 1086 hasLocationSign: false, 1087 subjectExactMatch: false, 1088 subjectType: suggestedType, 1089 locationParam: "find_loc", 1090 }); 1091 } 1092 1093 return result; 1094 } 1095 1096 /** 1097 * Returns an expected weather result that can be passed to `check_results()` 1098 * in xpcshell tests. 1099 * 1100 * @returns {object} 1101 * An object that can be passed to `check_results()`. 1102 */ 1103 weatherResult({ 1104 source = "rust", 1105 provider = "Weather", 1106 titleL10n = undefined, 1107 temperatureUnit = undefined, 1108 } = {}) { 1109 if (!temperatureUnit) { 1110 temperatureUnit = 1111 Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c"; 1112 } 1113 1114 if (!titleL10n) { 1115 titleL10n = { 1116 id: "urlbar-result-weather-title", 1117 args: { 1118 city: lazy.MerinoTestUtils.WEATHER_SUGGESTION.city_name, 1119 region: lazy.MerinoTestUtils.WEATHER_SUGGESTION.region_code, 1120 }, 1121 }; 1122 } 1123 titleL10n = { 1124 ...titleL10n, 1125 args: { 1126 ...titleL10n.args, 1127 temperature: 1128 lazy.MerinoTestUtils.WEATHER_SUGGESTION.current_conditions 1129 .temperature[temperatureUnit], 1130 unit: temperatureUnit.toUpperCase(), 1131 }, 1132 parseMarkup: true, 1133 }; 1134 1135 return { 1136 type: lazy.UrlbarUtils.RESULT_TYPE.URL, 1137 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 1138 heuristic: false, 1139 suggestedIndex: 1, 1140 isRichSuggestion: true, 1141 richSuggestionIconVariation: "6", 1142 payload: { 1143 titleL10n, 1144 url: lazy.MerinoTestUtils.WEATHER_SUGGESTION.url, 1145 bottomTextL10n: { 1146 id: "urlbar-result-weather-provider-sponsored", 1147 args: { provider: "AccuWeather®" }, 1148 }, 1149 source, 1150 provider, 1151 isSponsored: true, 1152 telemetryType: "weather", 1153 helpUrl: lazy.QuickSuggest.HELP_URL, 1154 }, 1155 }; 1156 } 1157 1158 /** 1159 * Asserts a result is a quick suggest result. 1160 * 1161 * @param {object} options 1162 * The options object. 1163 * @param {string} options.url 1164 * The expected URL. At least one of `url` and `originalUrl` must be given. 1165 * @param {string} options.originalUrl 1166 * The expected original URL (the URL with an unreplaced timestamp 1167 * template). At least one of `url` and `originalUrl` must be given. 1168 * @param {object} options.window 1169 * The window that should be used for this assertion 1170 * @param {number} [options.index] 1171 * The expected index of the quick suggest result. Pass -1 to use the index 1172 * of the last result. 1173 * @param {boolean} [options.isSponsored] 1174 * Whether the result is expected to be sponsored. 1175 * @param {boolean} [options.isBestMatch] 1176 * Whether the result is expected to be a best match. 1177 * @param {boolean} [options.isManageable] 1178 * Whether the result is expected to show Manage result menu item. 1179 * @param {boolean} [options.hasSponsoredLabel] 1180 * Whether the result is expected to show the "Sponsored" label below the 1181 * title. 1182 * @returns {Promise<object>} 1183 * The quick suggest result. 1184 */ 1185 async assertIsQuickSuggest({ 1186 url, 1187 originalUrl, 1188 window, 1189 index = -1, 1190 isSponsored = true, 1191 isBestMatch = false, 1192 isManageable = true, 1193 hasSponsoredLabel = isSponsored || isBestMatch, 1194 }) { 1195 this.Assert.ok( 1196 url || originalUrl, 1197 "At least one of url and originalUrl is specified" 1198 ); 1199 1200 if (index < 0) { 1201 let resultCount = lazy.UrlbarTestUtils.getResultCount(window); 1202 if (isBestMatch) { 1203 index = 1; 1204 this.Assert.greater( 1205 resultCount, 1206 1, 1207 "Sanity check: Result count should be > 1" 1208 ); 1209 } else { 1210 index = resultCount - 1; 1211 this.Assert.greater( 1212 resultCount, 1213 0, 1214 "Sanity check: Result count should be > 0" 1215 ); 1216 } 1217 } 1218 1219 let details = await lazy.UrlbarTestUtils.getDetailsOfResultAt( 1220 window, 1221 index 1222 ); 1223 let { result } = details; 1224 1225 this.#log( 1226 "assertIsQuickSuggest", 1227 `Checking actual result at index ${index}: ` + JSON.stringify(result) 1228 ); 1229 1230 this.Assert.equal( 1231 result.providerName, 1232 "UrlbarProviderQuickSuggest", 1233 "Result provider name is UrlbarProviderQuickSuggest" 1234 ); 1235 this.Assert.equal(details.type, lazy.UrlbarUtils.RESULT_TYPE.URL); 1236 this.Assert.equal(details.isSponsored, isSponsored, "Result isSponsored"); 1237 if (url) { 1238 this.Assert.equal(details.url, url, "Result URL"); 1239 } 1240 if (originalUrl) { 1241 this.Assert.equal( 1242 result.payload.originalUrl, 1243 originalUrl, 1244 "Result original URL" 1245 ); 1246 } 1247 1248 this.Assert.equal(!!result.isBestMatch, isBestMatch, "Result isBestMatch"); 1249 1250 let { row } = details.element; 1251 1252 let sponsoredElement = row._elements.get("description"); 1253 if (hasSponsoredLabel) { 1254 this.Assert.ok(sponsoredElement, "Result sponsored label element exists"); 1255 this.Assert.equal( 1256 sponsoredElement.textContent, 1257 isSponsored ? "Sponsored" : "", 1258 "Result sponsored label" 1259 ); 1260 } else { 1261 this.Assert.ok( 1262 !sponsoredElement?.textContent, 1263 "Result sponsored label element should not exist" 1264 ); 1265 } 1266 1267 this.Assert.equal( 1268 result.payload.isManageable, 1269 isManageable, 1270 "Result isManageable" 1271 ); 1272 1273 if (!isManageable) { 1274 this.Assert.equal( 1275 result.payload.helpUrl, 1276 lazy.QuickSuggest.HELP_URL, 1277 "Result helpURL" 1278 ); 1279 } 1280 1281 this.Assert.ok( 1282 row._buttons.get("result-menu"), 1283 "The menu button should be present" 1284 ); 1285 1286 return details; 1287 } 1288 1289 /** 1290 * Asserts a result is not a quick suggest result. 1291 * 1292 * @param {object} window 1293 * The window that should be used for this assertion 1294 * @param {number} index 1295 * The index of the result. 1296 */ 1297 async assertIsNotQuickSuggest(window, index) { 1298 let details = await lazy.UrlbarTestUtils.getDetailsOfResultAt( 1299 window, 1300 index 1301 ); 1302 this.Assert.notEqual( 1303 details.result.providerName, 1304 "UrlbarProviderQuickSuggest", 1305 `Result at index ${index} is not provided by UrlbarProviderQuickSuggest` 1306 ); 1307 } 1308 1309 /** 1310 * Asserts that none of the results are quick suggest results. 1311 * 1312 * @param {object} window 1313 * The window that should be used for this assertion 1314 */ 1315 async assertNoQuickSuggestResults(window) { 1316 for (let i = 0; i < lazy.UrlbarTestUtils.getResultCount(window); i++) { 1317 await this.assertIsNotQuickSuggest(window, i); 1318 } 1319 } 1320 1321 /** 1322 * Asserts that URLs in a result's payload have the timestamp template 1323 * substring replaced with real timestamps. 1324 * 1325 * @param {UrlbarResult} result The results to check 1326 * @param {object} urls 1327 * An object that contains the expected payload properties with template 1328 * substrings. For example: 1329 * ```js 1330 * { 1331 * url: "https://example.com/foo-%YYYYMMDDHH%", 1332 * sponsoredClickUrl: "https://example.com/bar-%YYYYMMDDHH%", 1333 * } 1334 * ``` 1335 */ 1336 assertTimestampsReplaced(result, urls) { 1337 let { TIMESTAMP_TEMPLATE, TIMESTAMP_LENGTH } = lazy.AmpSuggestions; 1338 1339 // Parse the timestamp strings from each payload property and save them in 1340 // `urls[key].timestamp`. 1341 urls = { ...urls }; 1342 for (let [key, url] of Object.entries(urls)) { 1343 let index = url.indexOf(TIMESTAMP_TEMPLATE); 1344 this.Assert.ok( 1345 index >= 0, 1346 `Timestamp template ${TIMESTAMP_TEMPLATE} is in URL ${url} for key ${key}` 1347 ); 1348 let value = result.payload[key]; 1349 this.Assert.ok(value, "Key is in result payload: " + key); 1350 let timestamp = value.substring(index, index + TIMESTAMP_LENGTH); 1351 1352 // Set `urls[key]` to an object that's helpful in the logged info message 1353 // below. 1354 urls[key] = { url, value, timestamp }; 1355 } 1356 1357 this.#log( 1358 "assertTimestampsReplaced", 1359 "Parsed timestamps: " + JSON.stringify(urls) 1360 ); 1361 1362 // Make a set of unique timestamp strings. There should only be one. 1363 let { timestamp } = Object.values(urls)[0]; 1364 this.Assert.deepEqual( 1365 [...new Set(Object.values(urls).map(o => o.timestamp))], 1366 [timestamp], 1367 "There's only one unique timestamp string" 1368 ); 1369 1370 // Parse the parts of the timestamp string. 1371 let year = timestamp.slice(0, -6); 1372 let month = timestamp.slice(-6, -4); 1373 let day = timestamp.slice(-4, -2); 1374 let hour = timestamp.slice(-2); 1375 let date = new Date(year, month - 1, day, hour); 1376 1377 // The timestamp should be no more than two hours in the past. Typically it 1378 // will be the same as the current hour, but since its resolution is in 1379 // terms of hours and it's possible the test may have crossed over into a 1380 // new hour as it was running, allow for the previous hour. 1381 this.Assert.less( 1382 Date.now() - 2 * 60 * 60 * 1000, 1383 date.getTime(), 1384 "Timestamp is within the past two hours" 1385 ); 1386 } 1387 1388 /** 1389 * Calls a callback while enrolled in a mock Nimbus experiment. The experiment 1390 * is automatically unenrolled and cleaned up after the callback returns. 1391 * 1392 * @param {object} options 1393 * Options for the mock experiment. 1394 * @param {Function} options.callback 1395 * The callback to call while enrolled in the mock experiment. 1396 * @param {object} options.valueOverrides 1397 * Values for feature variables. 1398 */ 1399 async withExperiment({ callback, ...options }) { 1400 let doExperimentCleanup = await this.enrollExperiment(options); 1401 await callback(); 1402 await doExperimentCleanup(); 1403 } 1404 1405 /** 1406 * Enrolls in a mock Nimbus experiment. 1407 * 1408 * @param {object} options 1409 * Options for the mock experiment. 1410 * @param {object} [options.valueOverrides] 1411 * Values for feature variables. 1412 * @returns {Promise<Function>} 1413 * The experiment cleanup function (async). 1414 */ 1415 async enrollExperiment({ valueOverrides = {} }) { 1416 this.#log("enrollExperiment", "Awaiting ExperimentAPI.ready"); 1417 await lazy.ExperimentAPI.ready(); 1418 1419 let doExperimentCleanup = 1420 await lazy.NimbusTestUtils.enrollWithFeatureConfig({ 1421 enabled: true, 1422 featureId: "urlbar", 1423 value: valueOverrides, 1424 }); 1425 1426 return async () => { 1427 this.#log("enrollExperiment.cleanup", "Awaiting experiment cleanup"); 1428 await doExperimentCleanup(); 1429 }; 1430 } 1431 1432 /** 1433 * Sets the app's home region and locale, calls your callback, and resets the 1434 * region and locale. 1435 * 1436 * @param {object} options 1437 * Options object. 1438 * @param {Function} options.callback 1439 * The callback to call. 1440 * @param {string} [options.region] 1441 * The region to set. See `setRegionAndLocale`. 1442 * @param {string} [options.locale] 1443 * The locale to set. See `setRegionAndLocale`. 1444 * @param {boolean} [options.skipSuggestReset] 1445 * Whether Suggest reset should be skipped after setting the new region and 1446 * locale. See `setRegionAndLocale`. 1447 */ 1448 async withRegionAndLocale({ 1449 callback, 1450 region = undefined, 1451 locale = undefined, 1452 skipSuggestReset = false, 1453 }) { 1454 this.#log("withRegionAndLocale", "Calling setRegionAndLocale"); 1455 let cleanup = await this.setRegionAndLocale({ 1456 region, 1457 locale, 1458 skipSuggestReset, 1459 }); 1460 1461 this.#log("withRegionAndLocale", "Calling callback"); 1462 await callback(); 1463 1464 this.#log("withRegionAndLocale", "Calling cleanup"); 1465 await cleanup(); 1466 1467 this.#log("withRegionAndLocale", "Done"); 1468 } 1469 1470 /** 1471 * Sets the app's home region and locale and waits for all relevant 1472 * notifications. Returns an async cleanup function that should be called to 1473 * restore the previous region and locale. 1474 * 1475 * See also `withRegionAndLocale`. 1476 * 1477 * @param {object} options 1478 * Options object. 1479 * @param {string} [options.region] 1480 * The home region to set. If falsey, the current region will remain 1481 * unchanged. 1482 * @param {string} [options.locale] 1483 * The locale to set. If falsey, the current locale will remain unchanged. 1484 * @param {Array} [options.availableLocales] 1485 * Normally this should be left undefined. If defined, 1486 * `Services.locale.availableLocales` will be set to this array. Otherwise 1487 * it will be set to `[locale]`. 1488 * @param {boolean} [options.skipSuggestReset] 1489 * Normally this function resets `QuickSuggest` after the new region and 1490 * locale are set, which will cause all Suggest prefs to be set according to 1491 * the new region and locale. That's undesirable in some cases, for example 1492 * when you're testing region/locale combinations where Suggest or one of 1493 * its features isn't enabled by default. Pass in true to skip reset. 1494 * @returns {Promise<Function>} 1495 * An async cleanup function. 1496 */ 1497 async setRegionAndLocale({ 1498 region = undefined, 1499 locale = undefined, 1500 availableLocales = undefined, 1501 skipSuggestReset = false, 1502 }) { 1503 let oldRegion = lazy.Region.home; 1504 let newRegion = region ?? oldRegion; 1505 let regionPromise = this.#waitForAllRegionChanges(newRegion); 1506 if (region) { 1507 this.#log("setRegionAndLocale", "Setting region: " + region); 1508 lazy.Region._setHomeRegion(region, true); 1509 } 1510 1511 let { 1512 availableLocales: oldAvailableLocales, 1513 requestedLocales: oldRequestedLocales, 1514 } = Services.locale; 1515 let newLocale = locale ?? oldRequestedLocales[0]; 1516 let localePromise = this.#waitForAllLocaleChanges(newLocale); 1517 if (locale) { 1518 this.#log("setRegionAndLocale", "Setting locale: " + locale); 1519 Services.locale.availableLocales = availableLocales ?? [locale]; 1520 Services.locale.requestedLocales = [locale]; 1521 } 1522 1523 this.#log("setRegionAndLocale", "Waiting for region and locale changes"); 1524 await Promise.all([regionPromise, localePromise]); 1525 1526 this.Assert.equal( 1527 lazy.Region.home, 1528 newRegion, 1529 "Region is now " + newRegion 1530 ); 1531 this.Assert.equal( 1532 Services.locale.appLocaleAsBCP47, 1533 newLocale, 1534 "App locale is now " + newLocale 1535 ); 1536 1537 if (!skipSuggestReset) { 1538 this.#log("setRegionAndLocale", "Waiting for _test_reset"); 1539 await lazy.QuickSuggest._test_reset(); 1540 } 1541 1542 if (this.#remoteSettingsServer) { 1543 this.#log("setRegionAndLocale", "Waiting for forceSync"); 1544 await this.forceSync(); 1545 } 1546 1547 this.#log("setRegionAndLocale", "Done"); 1548 1549 return async () => { 1550 this.#log( 1551 "setRegionAndLocale", 1552 "Cleanup started, calling setRegionAndLocale with old region and locale" 1553 ); 1554 await this.setRegionAndLocale({ 1555 skipSuggestReset, 1556 region: oldRegion, 1557 locale: oldRequestedLocales[0], 1558 availableLocales: oldAvailableLocales, 1559 }); 1560 this.#log("setRegionAndLocale", "Cleanup done"); 1561 }; 1562 } 1563 1564 async #waitForAllRegionChanges(region) { 1565 await lazy.TestUtils.waitForCondition( 1566 () => lazy.SharedRemoteSettingsService.country == region, 1567 "Waiting for SharedRemoteSettingsService.country to be " + region 1568 ); 1569 } 1570 1571 async #waitForAllLocaleChanges(locale) { 1572 let promises = [ 1573 lazy.TestUtils.waitForCondition( 1574 () => lazy.SharedRemoteSettingsService.locale == locale, 1575 "#waitForAllLocaleChanges: Waiting for SharedRemoteSettingsService.locale to be " + 1576 locale 1577 ), 1578 ]; 1579 1580 if (locale == Services.locale.requestedLocales[0]) { 1581 // "intl:app-locales-changed" isn't sent when the locale doesn't change. 1582 this.#log("#waitForAllLocaleChanges", "Locale is already " + locale); 1583 } else { 1584 this.#log( 1585 "#waitForAllLocaleChanges", 1586 "Waiting for intl:app-locales-changed" 1587 ); 1588 promises.push(lazy.TestUtils.topicObserved("intl:app-locales-changed")); 1589 1590 // Wait for the search service to reload engines. Otherwise tests can fail 1591 // in strange ways due to internal search service state during shutdown. 1592 // It won't always reload engines but it's hard to tell in advance when it 1593 // won't, so also set a timeout. 1594 this.#log("#waitForAllLocaleChanges", "Waiting for TOPIC_SEARCH_SERVICE"); 1595 promises.push( 1596 Promise.race([ 1597 lazy.TestUtils.topicObserved( 1598 lazy.SearchUtils.TOPIC_SEARCH_SERVICE, 1599 (subject, data) => { 1600 this.#log( 1601 "#waitForAllLocaleChanges", 1602 "Observed TOPIC_SEARCH_SERVICE with data: " + data 1603 ); 1604 return data == "engines-reloaded"; 1605 } 1606 ), 1607 new Promise(resolve => { 1608 lazy.setTimeout(() => { 1609 this.#log( 1610 "#waitForAllLocaleChanges", 1611 "Timed out waiting for TOPIC_SEARCH_SERVICE (not an error)" 1612 ); 1613 resolve(); 1614 }, 1000); 1615 }), 1616 ]) 1617 ); 1618 } 1619 1620 await Promise.all(promises); 1621 this.#log("#waitForAllLocaleChanges", "Done"); 1622 } 1623 1624 #log(fnName, msg) { 1625 this.info?.(`QuickSuggestTestUtils.${fnName} ${msg}`); 1626 } 1627 1628 #recordsByCollection(records) { 1629 // Make a Map from collection name to the array of records that should be 1630 // added to that collection. 1631 let recordsByCollection = records.reduce((memo, record) => { 1632 let collection = record.collection || this.RS_COLLECTION.OTHER; 1633 let recs = memo.get(collection); 1634 if (!recs) { 1635 recs = []; 1636 memo.set(collection, recs); 1637 } 1638 recs.push(record); 1639 return memo; 1640 }, new Map()); 1641 1642 // Make sure the two main collections, "quicksuggest-amp" and 1643 // "quicksuggest-other", are present. Otherwise the Rust component will log 1644 // 404 errors because it expects them to exist. The errors are harmless but 1645 // annoying. 1646 for (let collection of Object.values(this.RS_COLLECTION)) { 1647 if (!recordsByCollection.has(collection)) { 1648 recordsByCollection.set(collection, []); 1649 } 1650 } 1651 1652 return recordsByCollection; 1653 } 1654 1655 #remoteSettingsServer; 1656 } 1657 1658 export var QuickSuggestTestUtils = new _QuickSuggestTestUtils();