head.js (26779B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* import-globals-from ../../unit/head.js */ 5 /* eslint-disable jsdoc/require-param */ 6 7 ChromeUtils.defineESModuleGetters(this, { 8 Preferences: "resource://gre/modules/Preferences.sys.mjs", 9 QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs", 10 SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", 11 TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", 12 UrlbarProviderAutofill: 13 "moz-src:///browser/components/urlbar/UrlbarProviderAutofill.sys.mjs", 14 UrlbarProviderQuickSuggest: 15 "moz-src:///browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs", 16 UrlbarSearchUtils: 17 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 18 }); 19 20 add_setup(async function setUpQuickSuggestXpcshellTest() { 21 // Initializing TelemetryEnvironment in an xpcshell environment requires 22 // jumping through a bunch of hoops. Suggest's use of TelemetryEnvironment is 23 // tested in browser tests, and there's no other necessary reason to wait for 24 // TelemetryEnvironment initialization in xpcshell tests, so just skip it. 25 QuickSuggest._testSkipTelemetryEnvironmentInit = true; 26 }); 27 28 /** 29 * Sets up a test so it can use `doMigrateTest`. The app's region and locale 30 * will be set to US and en-US. Use `QuickSuggestTestUtils.withRegionAndLocale` 31 * or `setRegionAndLocale` if you need to test migration in a different region 32 * or locale. 33 */ 34 async function setUpMigrateTest() { 35 await UrlbarTestUtils.initNimbusFeature(); 36 await QuickSuggestTestUtils.setRegionAndLocale({ 37 region: "US", 38 locale: "en-US", 39 }); 40 } 41 42 /** 43 * Tests a single Suggest prefs migration, from one version to the next. Call 44 * `setUpMigrateTest` in your setup task before using this. To test migration in 45 * a region and locale other than US and en-US, wrap your `doMigrateTest` call 46 * in `QuickSuggestTestUtils.withRegionAndLocale`. 47 * 48 * @param {object} options 49 * The options object. 50 * @param {number} options.toVersion 51 * The version to test. Migration from `toVersion - 1` to `toVersion` will be 52 * performed. 53 * @param {object} [options.preMigrationUserPrefs] 54 * Prefs to set on the user branch before migration. An object that maps pref 55 * names relative to `browser.urlbar.` to values. 56 * @param {object} [options.expectedPostMigrationUserPrefs] 57 * Prefs that are expected to be set on the user branch after migration. An 58 * object that maps pref names relative to `browser.urlbar.` to values. If a 59 * pref is expected to be set on the user branch before migration but cleared 60 * after migration, set its value to `null`. 61 */ 62 async function doMigrateTest({ 63 toVersion, 64 preMigrationUserPrefs = {}, 65 expectedPostMigrationUserPrefs = {}, 66 }) { 67 info( 68 "Testing migration: " + 69 JSON.stringify({ 70 toVersion, 71 preMigrationUserPrefs, 72 expectedPostMigrationUserPrefs, 73 }) 74 ); 75 76 // Prefs whose user-branch values we should always make sure to check. 77 // Includes obsolete prefs since they're relevant to some older migrations. 78 let userPrefsToAlwaysCheck = [ 79 "quicksuggest.dataCollection.enabled", 80 "quicksuggest.enabled", 81 "suggest.quicksuggest", 82 "suggest.quicksuggest.nonsponsored", 83 "suggest.quicksuggest.sponsored", 84 ]; 85 86 let userBranch = new Preferences({ 87 branch: "browser.urlbar.", 88 defaultBranch: false, 89 }); 90 91 // Set the last-seen migration version to `toVersion - 1`. 92 if (toVersion == 1) { 93 userBranch.reset("quicksuggest.migrationVersion"); 94 } else { 95 userBranch.set("quicksuggest.migrationVersion", toVersion - 1); 96 } 97 98 // Set pre-migration user prefs. 99 for (let [name, value] of Object.entries(preMigrationUserPrefs)) { 100 userBranch.set(name, value); 101 } 102 103 // Record values for prefs in `userPrefsToAlwaysCheck` that weren't just set 104 // above, so that we can use them later. 105 for (let name of userPrefsToAlwaysCheck) { 106 if (!preMigrationUserPrefs.hasOwnProperty(name)) { 107 preMigrationUserPrefs[name] = userBranch.isSet(name) 108 ? userBranch.get(name) 109 : null; 110 } 111 } 112 113 // The entire set of prefs that should be checked after migration. 114 let userPrefsToCheckPostMigration = new Set([ 115 ...Object.keys(preMigrationUserPrefs), 116 ...Object.keys(expectedPostMigrationUserPrefs), 117 ]); 118 119 // Reinitialize Suggest and check prefs twice. The first time the migration 120 // should happen, and the second time the migration should not happen and 121 // all the prefs should stay the same. 122 for (let i = 0; i < 2; i++) { 123 info(`Reinitializing Suggest, i=${i}`); 124 125 // Reinitialize Suggest, which includes migration. 126 await QuickSuggest._test_reset({ 127 migrationVersion: toVersion, 128 }); 129 130 for (let name of userPrefsToCheckPostMigration) { 131 // The expected value is the expected post-migration value, if any; 132 // otherwise it's the pre-migration value. 133 let expectedValue = expectedPostMigrationUserPrefs.hasOwnProperty(name) 134 ? expectedPostMigrationUserPrefs[name] 135 : preMigrationUserPrefs[name]; 136 if (expectedValue === null) { 137 Assert.ok( 138 !userBranch.isSet(name), 139 "Pref should not have a user value after migration: " + name 140 ); 141 } else { 142 Assert.ok( 143 userBranch.isSet(name), 144 "Pref should have a user value after migration: " + name 145 ); 146 Assert.equal( 147 userBranch.get(name), 148 expectedValue, 149 "Pref should have been set to the expected value after migration: " + 150 name 151 ); 152 } 153 } 154 155 Assert.equal( 156 userBranch.get("quicksuggest.migrationVersion"), 157 toVersion, 158 "quicksuggest.migrationVersion should be updated after migration" 159 ); 160 } 161 162 // Clean up. 163 userBranch.reset("quicksuggest.migrationVersion"); 164 for (let name of userPrefsToCheckPostMigration) { 165 userBranch.reset(name); 166 } 167 } 168 169 /** 170 * Does a test that dismisses a single result by triggering a command on it. 171 * 172 * @param {object} options 173 * Options object. 174 * @param {SuggestFeature} options.feature 175 * The feature that provides the dismissed result. 176 * @param {UrlbarResult} options.result 177 * The result to trigger the command on. 178 * @param {string} options.command 179 * The name of the command to trigger. It should dismiss one result. 180 * @param {Array} options.queriesForDismissals 181 * Array of objects: `{ query, expectedResults }` 182 * For each object, the test will perform a search with `query` as the search 183 * string. After dismissing the result, the query shouldn't match any results. 184 * After clearing dismissals, the query should match the results in 185 * `expectedResults`. If `expectedResults` is omitted, `[result]` will be 186 * used. 187 * @param {Array} options.queriesForOthers 188 * Array of objects: `{ query, expectedResults }` 189 * For each object, the test will perform a search with `query` as the search 190 * string. The query should always match `expectedResults`. 191 * @param {string[]} [options.providers] 192 * The providers to query. 193 */ 194 async function doDismissOneTest({ 195 feature, 196 result, 197 command, 198 queriesForDismissals, 199 queriesForOthers, 200 providers = [UrlbarProviderQuickSuggest.name], 201 }) { 202 await QuickSuggest.clearDismissedSuggestions(); 203 await QuickSuggestTestUtils.forceSync(); 204 Assert.ok( 205 !(await QuickSuggest.canClearDismissedSuggestions()), 206 "Sanity check: canClearDismissedSuggestions should return false initially" 207 ); 208 209 let changedPromise = TestUtils.topicObserved( 210 "quicksuggest-dismissals-changed" 211 ); 212 213 let actualResult = await getActualResult({ 214 providers, 215 query: queriesForDismissals[0].query, 216 expectedResult: result, 217 }); 218 219 triggerCommand({ 220 command, 221 feature, 222 result: actualResult, 223 expectedCountsByCall: { 224 removeResult: 1, 225 }, 226 }); 227 228 info("Awaiting dismissals-changed promise"); 229 await changedPromise; 230 231 Assert.ok( 232 await QuickSuggest.canClearDismissedSuggestions(), 233 "canClearDismissedSuggestions should return true after triggering command" 234 ); 235 Assert.ok( 236 await QuickSuggest.isResultDismissed(actualResult), 237 "The result should be dismissed" 238 ); 239 240 for (let { query } of queriesForDismissals) { 241 info("Doing search for dismissed suggestions: " + JSON.stringify(query)); 242 await check_results({ 243 context: createContext(query, { 244 providers, 245 isPrivate: false, 246 }), 247 matches: [], 248 }); 249 } 250 251 for (let { query, expectedResults } of queriesForOthers) { 252 info( 253 "Doing search for non-dismissed suggestions: " + JSON.stringify(query) 254 ); 255 await check_results({ 256 context: createContext(query, { 257 providers, 258 isPrivate: false, 259 }), 260 matches: expectedResults, 261 }); 262 } 263 264 let clearedPromise = TestUtils.topicObserved( 265 "quicksuggest-dismissals-cleared" 266 ); 267 268 info("Clearing dismissals"); 269 await QuickSuggest.clearDismissedSuggestions(); 270 271 // It's not necessary to await this -- awaiting `clearDismissedSuggestions()` 272 // is sufficient -- but we do it to make sure the notification is sent. 273 info("Awaiting dismissals-cleared promise"); 274 await clearedPromise; 275 276 Assert.ok( 277 !(await QuickSuggest.canClearDismissedSuggestions()), 278 "canClearDismissedSuggestions should return false after clearing dismissals" 279 ); 280 281 for (let { query, expectedResults = [result] } of queriesForDismissals) { 282 info("Doing search after clearing dismissals: " + JSON.stringify(query)); 283 await check_results({ 284 context: createContext(query, { 285 providers, 286 isPrivate: false, 287 }), 288 matches: expectedResults, 289 }); 290 } 291 } 292 293 /** 294 * Does a test that dismisses a suggestion type (i.e., all suggestions of a 295 * certain type) by triggering a command on a result. 296 * 297 * @param {object} options 298 * Options object. 299 * @param {SuggestFeature} options.feature 300 * The feature that provides the suggestion type. 301 * @param {UrlbarResult} options.result 302 * The result to trigger the command on. 303 * @param {string} options.command 304 * The name of the command to trigger. It should dismiss all results of a 305 * suggestion type. 306 * @param {string} options.pref 307 * The name of the user-controlled pref (relative to `browser.urlbar.`) that 308 * controls the suggestion type. Should be included in 309 * `feature.primaryUserControlledPreferences`. 310 * @param {Array} options.queries 311 * Array of objects: `{ query, expectedResults }` 312 * For each object, the test will perform a search with `query` as the search 313 * string. After dismissing the suggestion type, the query shouldn't match any 314 * results. After clearing dismissals, the query should match the results in 315 * `expectedResults`. If `expectedResults` is omitted, `[result]` will be 316 * used. 317 * @param {string[]} [options.providers] 318 * The providers to query. 319 */ 320 async function doDismissAllTest({ 321 feature, 322 result, 323 command, 324 pref, 325 queries, 326 providers = [UrlbarProviderQuickSuggest.name], 327 }) { 328 await QuickSuggest.clearDismissedSuggestions(); 329 await QuickSuggestTestUtils.forceSync(); 330 Assert.ok( 331 !(await QuickSuggest.canClearDismissedSuggestions()), 332 "Sanity check: canClearDismissedSuggestions should return false initially" 333 ); 334 335 let changedPromise = TestUtils.topicObserved( 336 "quicksuggest-dismissals-changed" 337 ); 338 339 let actualResult = await getActualResult({ 340 providers, 341 query: queries[0].query, 342 expectedResult: result, 343 }); 344 345 triggerCommand({ 346 command, 347 feature, 348 result: actualResult, 349 expectedCountsByCall: { 350 removeResult: 1, 351 }, 352 }); 353 354 info("Awaiting dismissals-changed promise"); 355 await changedPromise; 356 357 Assert.ok( 358 await QuickSuggest.canClearDismissedSuggestions(), 359 "canClearDismissedSuggestions should return true after triggering command" 360 ); 361 Assert.ok( 362 !UrlbarPrefs.get(pref), 363 "Pref should be false after triggering command: " + pref 364 ); 365 366 for (let { query } of queries) { 367 info("Doing search after triggering command: " + JSON.stringify(query)); 368 await check_results({ 369 context: createContext(query, { 370 providers, 371 isPrivate: false, 372 }), 373 matches: [], 374 }); 375 } 376 377 let clearedPromise = TestUtils.topicObserved( 378 "quicksuggest-dismissals-cleared" 379 ); 380 381 info("Clearing dismissals"); 382 await QuickSuggest.clearDismissedSuggestions(); 383 384 // It's not necessary to await this -- awaiting `clearDismissedSuggestions()` 385 // is sufficient -- but we do it to make sure the notification is sent. 386 info("Awaiting dismissals-cleared promise"); 387 await clearedPromise; 388 389 Assert.ok( 390 !(await QuickSuggest.canClearDismissedSuggestions()), 391 "canClearDismissedSuggestions should return false after clearing dismissals" 392 ); 393 Assert.ok( 394 UrlbarPrefs.get(pref), 395 "Pref should be true after clearing it: " + pref 396 ); 397 398 // Clearing the pref will trigger a sync, so wait for it. 399 await QuickSuggestTestUtils.forceSync(); 400 401 for (let { query, expectedResults = [result] } of queries) { 402 info("Doing search after clearing dismissals: " + JSON.stringify(query)); 403 await check_results({ 404 context: createContext(query, { 405 providers, 406 isPrivate: false, 407 }), 408 matches: expectedResults, 409 }); 410 } 411 } 412 413 /** 414 * Does a search, asserts an actual result exists that matches the given result, 415 * and returns it. 416 * 417 * @param {object} options 418 * Options object. 419 * @param {SuggestFeature} options.query 420 * The search string. 421 * @param {UrlbarResult} options.expectedResult 422 * The expected result. 423 * @param {string[]} [options.providers] 424 * The providers to query. 425 */ 426 async function getActualResult({ 427 query, 428 expectedResult, 429 providers = [UrlbarProviderQuickSuggest.name], 430 }) { 431 info("Doing search to get an actual result: " + JSON.stringify(query)); 432 let context = createContext(query, { 433 providers, 434 isPrivate: false, 435 }); 436 await check_results({ 437 context, 438 matches: [expectedResult], 439 }); 440 441 let actualResult = context.results.find( 442 r => 443 r.providerName == UrlbarProviderQuickSuggest.name && 444 r.payload.provider == expectedResult.payload.provider 445 ); 446 Assert.ok(actualResult, "Search should have returned a matching result"); 447 448 return actualResult; 449 } 450 451 /** 452 * Does some "show less frequently" tests where the cap is set in remote 453 * settings and Nimbus. See `doOneShowLessFrequentlyTest()`. This function 454 * assumes the matching behavior implemented by the given `SuggestFeature` is 455 * based on matching prefixes of the given keyword starting at the first word. 456 * It also assumes the `SuggestFeature` provides suggestions in remote settings. 457 * 458 * @param {object} options 459 * Options object. 460 * @param {SuggestFeature} options.feature 461 * The feature that provides the suggestion matched by the searches. 462 * @param {*} options.expectedResult 463 * The expected result that should be matched, for searches that are expected 464 * to match a result. Can also be a function; it's passed the current search 465 * string and it should return the expected result. 466 * @param {string} options.showLessFrequentlyCountPref 467 * The name of the pref that stores the "show less frequently" count being 468 * tested. 469 * @param {string} options.nimbusCapVariable 470 * The name of the Nimbus variable that stores the "show less frequently" cap 471 * being tested. 472 * @param {object} options.keyword 473 * The primary keyword to use during the test. 474 * @param {number} options.keywordBaseIndex 475 * The index in `keyword` to base substring checks around. This function will 476 * test substrings starting at the beginning of keyword and ending at the 477 * following indexes: one index before `keywordBaseIndex`, 478 * `keywordBaseIndex`, `keywordBaseIndex` + 1, `keywordBaseIndex` + 2, and 479 * `keywordBaseIndex` + 3. 480 */ 481 async function doShowLessFrequentlyTests({ 482 feature, 483 expectedResult, 484 showLessFrequentlyCountPref, 485 nimbusCapVariable, 486 keyword, 487 keywordBaseIndex = keyword.indexOf(" "), 488 }) { 489 // Do some sanity checks on the keyword. Any checks that fail are errors in 490 // the test. 491 if (keywordBaseIndex <= 0) { 492 throw new Error( 493 "keywordBaseIndex must be > 0, but it's " + keywordBaseIndex 494 ); 495 } 496 if (keyword.length < keywordBaseIndex + 3) { 497 throw new Error( 498 "keyword must have at least two chars after keywordBaseIndex" 499 ); 500 } 501 502 let tests = [ 503 { 504 showLessFrequentlyCount: 0, 505 canShowLessFrequently: true, 506 newSearches: { 507 [keyword.substring(0, keywordBaseIndex - 1)]: false, 508 [keyword.substring(0, keywordBaseIndex)]: true, 509 [keyword.substring(0, keywordBaseIndex + 1)]: true, 510 [keyword.substring(0, keywordBaseIndex + 2)]: true, 511 [keyword.substring(0, keywordBaseIndex + 3)]: true, 512 }, 513 }, 514 { 515 showLessFrequentlyCount: 1, 516 canShowLessFrequently: true, 517 newSearches: { 518 [keyword.substring(0, keywordBaseIndex)]: false, 519 }, 520 }, 521 { 522 showLessFrequentlyCount: 2, 523 canShowLessFrequently: true, 524 newSearches: { 525 [keyword.substring(0, keywordBaseIndex + 1)]: false, 526 }, 527 }, 528 { 529 showLessFrequentlyCount: 3, 530 canShowLessFrequently: false, 531 newSearches: { 532 [keyword.substring(0, keywordBaseIndex + 2)]: false, 533 }, 534 }, 535 { 536 showLessFrequentlyCount: 3, 537 canShowLessFrequently: false, 538 newSearches: {}, 539 }, 540 ]; 541 542 info("Testing 'show less frequently' with cap in remote settings"); 543 await doOneShowLessFrequentlyTest({ 544 tests, 545 feature, 546 expectedResult, 547 showLessFrequentlyCountPref, 548 rs: { 549 show_less_frequently_cap: 3, 550 }, 551 }); 552 553 // Nimbus should override remote settings. 554 info("Testing 'show less frequently' with cap in Nimbus and remote settings"); 555 await doOneShowLessFrequentlyTest({ 556 tests, 557 feature, 558 expectedResult, 559 showLessFrequentlyCountPref, 560 rs: { 561 show_less_frequently_cap: 10, 562 }, 563 nimbus: { 564 [nimbusCapVariable]: 3, 565 }, 566 }); 567 } 568 569 /** 570 * Does a group of searches, increments the "show less frequently" count, and 571 * repeats until all groups are done. The cap can be set by remote settings 572 * config and/or Nimbus. 573 * 574 * @param {object} options 575 * Options object. 576 * @param {SuggestFeature} options.feature 577 * The feature that provides the suggestion matched by the searches. 578 * @param {*} options.expectedResult 579 * The expected result that should be matched, for searches that are expected 580 * to match a result. Can also be a function; it's passed the current search 581 * string and it should return the expected result. 582 * @param {string} options.showLessFrequentlyCountPref 583 * The name of the pref that stores the "show less frequently" count being 584 * tested. 585 * @param {object} options.tests 586 * An array where each item describes a group of new searches to perform and 587 * expected state. Each item should look like this: 588 * `{ showLessFrequentlyCount, canShowLessFrequently, newSearches }` 589 * 590 * {number} showLessFrequentlyCount 591 * The expected value of `showLessFrequentlyCount` before the group of 592 * searches is performed. 593 * {boolean} canShowLessFrequently 594 * The expected value of `canShowLessFrequently` before the group of 595 * searches is performed. 596 * {object} newSearches 597 * An object that maps each search string to a boolean that indicates 598 * whether the first remote settings suggestion should be triggered by the 599 * search string. Searches are cumulative: The intended use is to pass a 600 * large initial group of searches in the first search group, and then each 601 * following `newSearches` is a diff against the previous. 602 * @param {object} options.rs 603 * The remote settings config to set. 604 * @param {object} options.nimbus 605 * The Nimbus variables to set. 606 */ 607 async function doOneShowLessFrequentlyTest({ 608 feature, 609 expectedResult, 610 showLessFrequentlyCountPref, 611 tests, 612 rs = {}, 613 nimbus = {}, 614 }) { 615 // Disable Merino so we trigger only remote settings suggestions. The 616 // `SuggestFeature` is expected to add remote settings suggestions using 617 // keywords start starting with the first word in each full keyword, but the 618 // mock Merino server will always return whatever suggestion it's told to 619 // return regardless of the search string. That means Merino will return a 620 // suggestion for a keyword that's smaller than the first full word. 621 UrlbarPrefs.set("quicksuggest.online.enabled", false); 622 623 // Set Nimbus variables and RS config. 624 let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(nimbus); 625 await QuickSuggestTestUtils.withConfig({ 626 config: rs, 627 callback: async () => { 628 let cumulativeSearches = {}; 629 630 for (let { 631 showLessFrequentlyCount, 632 canShowLessFrequently, 633 newSearches, 634 } of tests) { 635 info( 636 "Starting subtest: " + 637 JSON.stringify({ 638 showLessFrequentlyCount, 639 canShowLessFrequently, 640 newSearches, 641 }) 642 ); 643 644 Assert.equal( 645 feature.showLessFrequentlyCount, 646 showLessFrequentlyCount, 647 "showLessFrequentlyCount should be correct initially" 648 ); 649 Assert.equal( 650 UrlbarPrefs.get(showLessFrequentlyCountPref), 651 showLessFrequentlyCount, 652 "Pref should be correct initially" 653 ); 654 Assert.equal( 655 feature.canShowLessFrequently, 656 canShowLessFrequently, 657 "canShowLessFrequently should be correct initially" 658 ); 659 660 // Merge the current `newSearches` object into the cumulative object. 661 cumulativeSearches = { 662 ...cumulativeSearches, 663 ...newSearches, 664 }; 665 666 for (let [searchString, isExpected] of Object.entries( 667 cumulativeSearches 668 )) { 669 info("Doing search: " + JSON.stringify({ searchString, isExpected })); 670 671 let results = []; 672 if (isExpected) { 673 results.push( 674 typeof expectedResult == "function" 675 ? expectedResult(searchString) 676 : expectedResult 677 ); 678 } 679 680 await check_results({ 681 context: createContext(searchString, { 682 providers: [UrlbarProviderQuickSuggest.name], 683 isPrivate: false, 684 }), 685 matches: results, 686 }); 687 } 688 689 feature.incrementShowLessFrequentlyCount(); 690 } 691 }, 692 }); 693 694 await cleanUpNimbus(); 695 UrlbarPrefs.clear(showLessFrequentlyCountPref); 696 UrlbarPrefs.clear("quicksuggest.online.enabled"); 697 } 698 699 /** 700 * Queries the Rust component directly and checks the returned suggestions. The 701 * point is to make sure the Rust backend passes the correct providers to the 702 * Rust component depending on the types of enabled suggestions. Assuming the 703 * Rust component isn't buggy, it should return suggestions only for the 704 * passed-in providers. 705 * 706 * @param {object} options 707 * Options object 708 * @param {string} options.searchString 709 * The search string. 710 * @param {Array} options.tests 711 * Array of test objects: `{ prefs, expectedUrls }` 712 * 713 * For each object, the given prefs are set, the Rust component is queried 714 * using the given search string, and the URLs of the returned suggestions are 715 * compared to the given expected URLs (order doesn't matter). 716 * 717 * {object} prefs 718 * An object mapping pref names (relative to `browser.urlbar`) to values. 719 * These prefs will be set before querying and should be used to enable or 720 * disable particular types of suggestions. 721 * {Array} expectedUrls 722 * An array of the URLs of the suggestions that are expected to be returned. 723 * The order doesn't matter. 724 */ 725 async function doRustProvidersTests({ searchString, tests }) { 726 for (let { prefs, expectedUrls } of tests) { 727 info( 728 "Starting Rust providers test: " + JSON.stringify({ prefs, expectedUrls }) 729 ); 730 731 info("Setting prefs and forcing sync"); 732 for (let [name, value] of Object.entries(prefs)) { 733 UrlbarPrefs.set(name, value); 734 } 735 await QuickSuggestTestUtils.forceSync(); 736 737 info("Querying with search string: " + JSON.stringify(searchString)); 738 let suggestions = await QuickSuggest.rustBackend.query(searchString); 739 info("Got suggestions: " + JSON.stringify(suggestions)); 740 741 Assert.deepEqual( 742 suggestions.map(s => s.url).sort(), 743 expectedUrls.sort(), 744 "query() should return the expected suggestions (by URL)" 745 ); 746 747 info("Clearing prefs and forcing sync"); 748 for (let name of Object.keys(prefs)) { 749 UrlbarPrefs.clear(name); 750 } 751 await QuickSuggestTestUtils.forceSync(); 752 } 753 } 754 755 /** 756 * Simulates performing a command for a feature by calling its `onEngagement()`. 757 * 758 * @param {object} options 759 * Options object. 760 * @param {SuggestFeature} options.feature 761 * The feature whose command will be triggered. 762 * @param {string} options.command 763 * The name of the command to trigger. 764 * @param {UrlbarResult} options.result 765 * The result that the command will act on. 766 * @param {string} options.searchString 767 * The search string to pass to `onEngagement()`. 768 * @param {object} options.expectedCountsByCall 769 * If non-null, this should map controller and view method names to the number 770 * of times they should be called in response to the command. 771 * @returns {Map} 772 * A map from names of methods on the controller and view to the number of 773 * times they were called. 774 */ 775 function triggerCommand({ 776 feature, 777 command, 778 result, 779 searchString = "", 780 expectedCountsByCall = null, 781 }) { 782 info(`Calling ${feature.name}.onEngagement() to trigger command: ${command}`); 783 784 let countsByCall = new Map(); 785 let addCall = name => { 786 if (!countsByCall.has(name)) { 787 countsByCall.set(name, 0); 788 } 789 countsByCall.set(name, countsByCall.get(name) + 1); 790 }; 791 792 feature.onEngagement( 793 // query context 794 {}, 795 // controller 796 { 797 removeResult() { 798 addCall("removeResult"); 799 }, 800 input: { 801 startQuery() { 802 addCall("startQuery"); 803 }, 804 }, 805 view: { 806 acknowledgeFeedback() { 807 addCall("acknowledgeFeedback"); 808 }, 809 invalidateResultMenuCommands() { 810 addCall("invalidateResultMenuCommands"); 811 }, 812 }, 813 }, 814 // details 815 { result, selType: command }, 816 searchString 817 ); 818 819 if (expectedCountsByCall) { 820 for (let [name, expectedCount] of Object.entries(expectedCountsByCall)) { 821 Assert.equal( 822 countsByCall.get(name) ?? 0, 823 expectedCount, 824 "Function should have been called the expected number of times: " + name 825 ); 826 } 827 } 828 829 return countsByCall; 830 }