test_quicksuggest_merino.js (36105B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // Tests Merino integration with UrlbarProviderQuickSuggest. 6 7 "use strict"; 8 9 ChromeUtils.defineESModuleGetters(this, { 10 AmpSuggestions: 11 "moz-src:///browser/components/urlbar/private/AmpSuggestions.sys.mjs", 12 }); 13 14 const SEARCH_STRING = "frab"; 15 16 const { DEFAULT_SUGGESTION_SCORE } = UrlbarProviderQuickSuggest; 17 const { TIMESTAMP_TEMPLATE } = AmpSuggestions; 18 19 const REMOTE_SETTINGS_RESULTS = [ 20 QuickSuggestTestUtils.ampRemoteSettings({ 21 keywords: [SEARCH_STRING], 22 }), 23 ]; 24 25 const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = QuickSuggestTestUtils.ampResult({ 26 keyword: SEARCH_STRING, 27 suggestedIndex: -1, 28 }); 29 30 const EXPECTED_MERINO_URLBAR_RESULT = QuickSuggestTestUtils.ampResult({ 31 source: "merino", 32 provider: "adm", 33 requestId: "request_id", 34 suggestedIndex: -1, 35 }); 36 37 add_setup(async () => { 38 await MerinoTestUtils.server.start(); 39 40 // Set up the remote settings client with the test data. 41 await QuickSuggestTestUtils.ensureQuickSuggestInit({ 42 prefs: [ 43 ["suggest.quicksuggest.all", true], 44 ["suggest.quicksuggest.sponsored", true], 45 ["quicksuggest.ampTopPickCharThreshold", 0], 46 ], 47 }); 48 await resetRemoteSettingsData(); 49 50 Assert.equal( 51 typeof DEFAULT_SUGGESTION_SCORE, 52 "number", 53 "Sanity check: DEFAULT_SUGGESTION_SCORE is defined" 54 ); 55 }); 56 57 // Tests with the Merino endpoint URL set to an empty string, which disables 58 // fetching from Merino. 59 add_task(async function merinoDisabled() { 60 let mockEndpointUrl = UrlbarPrefs.get("merino.endpointURL"); 61 UrlbarPrefs.set("merino.endpointURL", ""); 62 UrlbarPrefs.set("quicksuggest.online.available", true); 63 UrlbarPrefs.set("quicksuggest.online.enabled", true); 64 65 // Clear the remote settings suggestions so that if Merino is actually queried 66 // -- which would be a bug -- we don't accidentally mask the Merino suggestion 67 // by also matching an RS suggestion with the same or higher score. 68 await QuickSuggestTestUtils.setRemoteSettingsRecords([]); 69 70 let context = createContext(SEARCH_STRING, { 71 providers: [UrlbarProviderQuickSuggest.name], 72 isPrivate: false, 73 }); 74 await check_results({ 75 context, 76 matches: [], 77 }); 78 79 UrlbarPrefs.set("merino.endpointURL", mockEndpointUrl); 80 81 await resetRemoteSettingsData(); 82 }); 83 84 // Results should be fetched from Merino only when online is both available and 85 // enabled. 86 add_task(async function onlineAvailableAndEnabled() { 87 // Clear the remote settings suggestions so that if Merino is actually queried 88 // -- which would be a bug -- we don't accidentally mask the Merino suggestion 89 // by also matching an RS suggestion with the same or higher score. 90 await QuickSuggestTestUtils.setRemoteSettingsRecords([]); 91 92 for (let onlineAvailable of [false, true]) { 93 for (let onlineEnabled of [false, true]) { 94 UrlbarPrefs.set("quicksuggest.online.available", onlineAvailable); 95 UrlbarPrefs.set("quicksuggest.online.enabled", onlineEnabled); 96 97 await check_results({ 98 context: createContext(SEARCH_STRING, { 99 providers: [UrlbarProviderQuickSuggest.name], 100 isPrivate: false, 101 }), 102 matches: 103 onlineAvailable && onlineEnabled 104 ? [EXPECTED_MERINO_URLBAR_RESULT] 105 : [], 106 }); 107 } 108 } 109 110 await resetRemoteSettingsData(); 111 }); 112 113 // When the Merino suggestion has a higher score than the remote settings 114 // suggestion, the Merino suggestion should be used. 115 add_task(async function higherScore() { 116 UrlbarPrefs.set("quicksuggest.online.available", true); 117 UrlbarPrefs.set("quicksuggest.online.enabled", true); 118 119 MerinoTestUtils.server.response.body.suggestions[0].score = 120 2 * DEFAULT_SUGGESTION_SCORE; 121 122 let context = createContext(SEARCH_STRING, { 123 providers: [UrlbarProviderQuickSuggest.name], 124 isPrivate: false, 125 }); 126 await check_results({ 127 context, 128 matches: [EXPECTED_MERINO_URLBAR_RESULT], 129 }); 130 131 MerinoTestUtils.server.reset(); 132 merinoClient().resetSession(); 133 }); 134 135 // When the Merino suggestion has a lower score than the remote settings 136 // suggestion, the remote settings suggestion should be used. 137 add_task(async function lowerScore() { 138 UrlbarPrefs.set("quicksuggest.online.available", true); 139 UrlbarPrefs.set("quicksuggest.online.enabled", true); 140 141 MerinoTestUtils.server.response.body.suggestions[0].score = 142 DEFAULT_SUGGESTION_SCORE / 2; 143 144 let context = createContext(SEARCH_STRING, { 145 providers: [UrlbarProviderQuickSuggest.name], 146 isPrivate: false, 147 }); 148 await check_results({ 149 context, 150 matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], 151 }); 152 153 MerinoTestUtils.server.reset(); 154 merinoClient().resetSession(); 155 }); 156 157 // When remote settings doesn't return a suggestion but Merino does, the Merino 158 // suggestion should be used. 159 add_task(async function noSuggestion_remoteSettings() { 160 UrlbarPrefs.set("quicksuggest.online.available", true); 161 UrlbarPrefs.set("quicksuggest.online.enabled", true); 162 163 let context = createContext("this doesn't match remote settings", { 164 providers: [UrlbarProviderQuickSuggest.name], 165 isPrivate: false, 166 }); 167 await check_results({ 168 context, 169 matches: [EXPECTED_MERINO_URLBAR_RESULT], 170 }); 171 172 MerinoTestUtils.server.reset(); 173 merinoClient().resetSession(); 174 }); 175 176 // When Merino doesn't return a suggestion but remote settings does, the remote 177 // settings suggestion should be used. 178 add_task(async function noSuggestion_merino() { 179 UrlbarPrefs.set("quicksuggest.online.available", true); 180 UrlbarPrefs.set("quicksuggest.online.enabled", true); 181 182 MerinoTestUtils.server.response.body.suggestions = []; 183 184 let context = createContext(SEARCH_STRING, { 185 providers: [UrlbarProviderQuickSuggest.name], 186 isPrivate: false, 187 }); 188 await check_results({ 189 context, 190 matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], 191 }); 192 193 MerinoTestUtils.server.reset(); 194 merinoClient().resetSession(); 195 }); 196 197 // When Merino returns multiple suggestions, the one with the largest score 198 // should be used. 199 add_task(async function multipleMerinoSuggestions() { 200 UrlbarPrefs.set("quicksuggest.online.available", true); 201 UrlbarPrefs.set("quicksuggest.online.enabled", true); 202 203 MerinoTestUtils.server.response.body.suggestions = [ 204 { 205 provider: "adm", 206 full_keyword: "multipleMerinoSuggestions 0 full_keyword", 207 title: "multipleMerinoSuggestions 0 title", 208 url: "multipleMerinoSuggestions 0 url", 209 icon: "multipleMerinoSuggestions 0 icon", 210 impression_url: "multipleMerinoSuggestions 0 impression_url", 211 click_url: "multipleMerinoSuggestions 0 click_url", 212 block_id: 0, 213 advertiser: "multipleMerinoSuggestions 0 advertiser", 214 iab_category: "22 - Shopping", 215 is_sponsored: true, 216 score: 0.1, 217 }, 218 { 219 provider: "adm", 220 full_keyword: "multipleMerinoSuggestions 1 full_keyword", 221 title: "multipleMerinoSuggestions 1 title", 222 url: "multipleMerinoSuggestions 1 url", 223 icon: "multipleMerinoSuggestions 1 icon", 224 impression_url: "multipleMerinoSuggestions 1 impression_url", 225 click_url: "multipleMerinoSuggestions 1 click_url", 226 block_id: 1, 227 advertiser: "multipleMerinoSuggestions 1 advertiser", 228 iab_category: "22 - Shopping", 229 is_sponsored: true, 230 score: 1, 231 }, 232 { 233 provider: "adm", 234 full_keyword: "multipleMerinoSuggestions 2 full_keyword", 235 title: "multipleMerinoSuggestions 2 title", 236 url: "multipleMerinoSuggestions 2 url", 237 icon: "multipleMerinoSuggestions 2 icon", 238 impression_url: "multipleMerinoSuggestions 2 impression_url", 239 click_url: "multipleMerinoSuggestions 2 click_url", 240 block_id: 2, 241 advertiser: "multipleMerinoSuggestions 2 advertiser", 242 iab_category: "22 - Shopping", 243 is_sponsored: true, 244 score: 0.2, 245 }, 246 ]; 247 248 let context = createContext("test", { 249 providers: [UrlbarProviderQuickSuggest.name], 250 isPrivate: false, 251 }); 252 await check_results({ 253 context, 254 matches: [ 255 QuickSuggestTestUtils.ampResult({ 256 keyword: "multipleMerinoSuggestions 1 full_keyword", 257 title: "multipleMerinoSuggestions 1 title", 258 url: "multipleMerinoSuggestions 1 url", 259 originalUrl: "multipleMerinoSuggestions 1 url", 260 icon: "multipleMerinoSuggestions 1 icon", 261 impressionUrl: "multipleMerinoSuggestions 1 impression_url", 262 clickUrl: "multipleMerinoSuggestions 1 click_url", 263 blockId: 1, 264 advertiser: "multipleMerinoSuggestions 1 advertiser", 265 requestId: "request_id", 266 source: "merino", 267 provider: "adm", 268 suggestedIndex: -1, 269 }), 270 ], 271 }); 272 273 MerinoTestUtils.server.reset(); 274 merinoClient().resetSession(); 275 }); 276 277 // Timestamp templates in URLs should be replaced with real timestamps. 278 add_task(async function timestamps() { 279 UrlbarPrefs.set("quicksuggest.online.available", true); 280 UrlbarPrefs.set("quicksuggest.online.enabled", true); 281 282 // Set up the Merino response with template URLs. 283 let suggestion = MerinoTestUtils.server.response.body.suggestions[0]; 284 suggestion.url = `http://example.com/time-${TIMESTAMP_TEMPLATE}`; 285 suggestion.click_url = `http://example.com/time-${TIMESTAMP_TEMPLATE}-foo`; 286 287 // Do a search. 288 let context = createContext("test", { 289 providers: [UrlbarProviderQuickSuggest.name], 290 isPrivate: false, 291 }); 292 let controller = UrlbarTestUtils.newMockController({ 293 input: { 294 isPrivate: context.isPrivate, 295 onFirstResult() { 296 return false; 297 }, 298 getSearchSource() { 299 return "dummy-search-source"; 300 }, 301 window: { 302 location: { 303 href: AppConstants.BROWSER_CHROME_URL, 304 }, 305 }, 306 }, 307 }); 308 await controller.startQuery(context); 309 310 // Should be one quick suggest result. 311 Assert.equal(context.results.length, 1, "One result returned"); 312 let result = context.results[0]; 313 314 QuickSuggestTestUtils.assertTimestampsReplaced(result, { 315 url: suggestion.click_url, 316 sponsoredClickUrl: suggestion.click_url, 317 }); 318 319 MerinoTestUtils.server.reset(); 320 merinoClient().resetSession(); 321 }); 322 323 // Tests dismissals of managed Merino suggestions (suggestions that are managed 324 // by a `SuggestFeature`). 325 add_task(async function dismissals_managed() { 326 UrlbarPrefs.set("quicksuggest.online.available", true); 327 UrlbarPrefs.set("quicksuggest.online.enabled", true); 328 329 // Set up a single Merino AMP suggestion with a unique URL. 330 let url = "https://example.com/merino-amp-url"; 331 MerinoTestUtils.server.response = 332 MerinoTestUtils.server.makeDefaultResponse(); 333 MerinoTestUtils.server.response.body.suggestions[0].url = url; 334 335 let expectedMerinoResult = QuickSuggestTestUtils.ampResult({ 336 url, 337 source: "merino", 338 provider: "adm", 339 requestId: "request_id", 340 suggestedIndex: -1, 341 }); 342 343 // Do a search. The Merino suggestion should be matched. 344 const context = createContext(SEARCH_STRING, { 345 providers: [UrlbarProviderQuickSuggest.name], 346 isPrivate: false, 347 }); 348 await check_results({ 349 context, 350 matches: [expectedMerinoResult], 351 }); 352 353 let result = context.results[0]; 354 Assert.ok( 355 QuickSuggest.getFeatureByResult(result), 356 "Sanity check: The actual result should be managed by a feature" 357 ); 358 359 // Dismiss the Merino result. 360 await QuickSuggest.dismissResult(result); 361 Assert.ok( 362 await QuickSuggest.isResultDismissed(result), 363 "isResultDismissed should return true after dismissing result" 364 ); 365 366 // Do another search. The remote settings suggestion should now be matched. 367 await check_results({ 368 context: createContext(SEARCH_STRING, { 369 providers: [UrlbarProviderQuickSuggest.name], 370 isPrivate: false, 371 }), 372 matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], 373 }); 374 375 // Clear dismissals. 376 await QuickSuggest.clearDismissedSuggestions(); 377 Assert.ok( 378 !(await QuickSuggest.isResultDismissed(result)), 379 "isResultDismissed should return false after clearing dismissals" 380 ); 381 382 // The Merino suggestion should be matched again. 383 await check_results({ 384 context: createContext(SEARCH_STRING, { 385 providers: [UrlbarProviderQuickSuggest.name], 386 isPrivate: false, 387 }), 388 matches: [expectedMerinoResult], 389 }); 390 391 MerinoTestUtils.server.reset(); 392 merinoClient().resetSession(); 393 }); 394 395 // Tests dismissals of Merino AMP suggestions, which have special handling 396 // around their dismissal keys. 397 add_task(async function dismissals_amp() { 398 UrlbarPrefs.set("quicksuggest.online.available", true); 399 UrlbarPrefs.set("quicksuggest.online.enabled", true); 400 401 UrlbarPrefs.set("suggest.quicksuggest.all", true); 402 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 403 await QuickSuggestTestUtils.forceSync(); 404 405 let tests = [ 406 { 407 suggestion: { 408 url: "https://example.com/0", 409 }, 410 expected: { 411 // dismissal key should be the `url` value 412 dismissalKey: "https://example.com/0", 413 }, 414 }, 415 { 416 suggestion: { 417 url: `https://example.com/1-${TIMESTAMP_TEMPLATE}`, 418 }, 419 expected: { 420 // dismissal key should be the original `url` value with the timestamp 421 // template 422 dismissalKey: `https://example.com/1-${TIMESTAMP_TEMPLATE}`, 423 }, 424 }, 425 { 426 suggestion: { 427 url: "https://example.com/2", 428 full_keyword: "full keyword 2", 429 }, 430 expected: { 431 // dismissal key should be the `url` value 432 dismissalKey: "https://example.com/2", 433 notDismissalKeys: ["full keyword 2"], 434 }, 435 }, 436 { 437 suggestion: { 438 url: `https://example.com/3-${TIMESTAMP_TEMPLATE}`, 439 full_keyword: "full keyword 3", 440 }, 441 expected: { 442 // dismissal key should be the `url` value 443 dismissalKey: `https://example.com/3-${TIMESTAMP_TEMPLATE}`, 444 notDismissalKeys: ["full keyword 3"], 445 }, 446 }, 447 { 448 suggestion: { 449 url: "https://example.com/4", 450 dismissal_key: "4-dismissal-key", 451 }, 452 expected: { 453 // dismissal key should be the `dismissal_key` value 454 dismissalKey: "4-dismissal-key", 455 notDismissalKeys: ["https://example.com/4"], 456 }, 457 }, 458 { 459 suggestion: { 460 url: `https://example.com/5-${TIMESTAMP_TEMPLATE}`, 461 dismissal_key: "5-dismissal-key", 462 }, 463 expected: { 464 // dismissal key should be the `dismissal_key` value 465 dismissalKey: "5-dismissal-key", 466 notDismissalKeys: [`https://example.com/5-${TIMESTAMP_TEMPLATE}`], 467 }, 468 }, 469 { 470 suggestion: { 471 url: "https://example.com/6", 472 full_keyword: "full keyword 6", 473 dismissal_key: "6-dismissal-key", 474 }, 475 expected: { 476 // dismissal key should be the `dismissal_key` value 477 dismissalKey: "6-dismissal-key", 478 notDismissalKeys: ["full keyword 6", "https://example.com/6"], 479 }, 480 }, 481 { 482 suggestion: { 483 url: `https://example.com/7-${TIMESTAMP_TEMPLATE}`, 484 full_keyword: "full keyword 7", 485 dismissal_key: "7-dismissal-key", 486 }, 487 expected: { 488 // dismissal key should be the `dismissal_key` value 489 dismissalKey: "7-dismissal-key", 490 notDismissalKeys: [ 491 "full keyword 7", 492 `https://example.com/7-${TIMESTAMP_TEMPLATE}`, 493 ], 494 }, 495 }, 496 ]; 497 498 for (let test of tests) { 499 info("Doing subtest: " + JSON.stringify(test)); 500 501 let { suggestion, expected } = test; 502 503 suggestion = { 504 provider: "adm", 505 title: "title", 506 icon: null, 507 impression_url: "https://example.com/impression", 508 click_url: "https://example.com/click", 509 block_id: 1, 510 advertiser: "advertiser", 511 iab_category: "22 - Shopping", 512 is_sponsored: true, 513 request_id: "request_id", 514 score: 1, 515 ...suggestion, 516 }; 517 518 MerinoTestUtils.server.response = 519 MerinoTestUtils.server.makeDefaultResponse(); 520 MerinoTestUtils.server.response.body.suggestions = [suggestion]; 521 522 let expectedResult = { 523 type: UrlbarUtils.RESULT_TYPE.URL, 524 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 525 heuristic: false, 526 payload: { 527 provider: suggestion.provider, 528 title: suggestion.full_keyword 529 ? `${suggestion.full_keyword} — ${suggestion.title}` 530 : suggestion.title, 531 url: suggestion.url, 532 originalUrl: suggestion.original_url || suggestion.url, 533 dismissalKey: suggestion.dismissal_key, 534 requestId: suggestion.request_id, 535 sponsoredImpressionUrl: suggestion.impression_url, 536 sponsoredClickUrl: suggestion.click_url, 537 sponsoredBlockId: suggestion.block_id, 538 sponsoredAdvertiser: suggestion.advertiser, 539 sponsoredIabCategory: suggestion.iab_category, 540 isBlockable: true, 541 isManageable: true, 542 isSponsored: true, 543 source: "merino", 544 telemetryType: "adm_sponsored", 545 descriptionL10n: { id: "urlbar-result-action-sponsored" }, 546 }, 547 }; 548 549 // Do a search. The Merino suggestion should be matched. 550 let context = createContext(SEARCH_STRING, { 551 providers: [UrlbarProviderQuickSuggest.name], 552 isPrivate: false, 553 }); 554 await check_results({ 555 context, 556 matches: [expectedResult], 557 // Ignore values related to the timestamp template. They're not important 558 // for this test. 559 conditionalPayloadProperties: { 560 url: { ignore: true }, 561 urlTimestampIndex: { ignore: true }, 562 }, 563 }); 564 565 let result = context.results[0]; 566 Assert.equal( 567 QuickSuggest.getFeatureByResult(result)?.name, 568 "AmpSuggestions", 569 "Sanity check: The actual result should be managed by AmpSuggestions" 570 ); 571 572 // Dismiss the Merino result. 573 await QuickSuggest.dismissResult(result); 574 Assert.ok( 575 await QuickSuggest.isResultDismissed(result), 576 "isResultDismissed should return true after dismissing result" 577 ); 578 579 Assert.ok( 580 await QuickSuggest.rustBackend.isDismissedByKey(expected.dismissalKey), 581 "isDismissedByKey should return true after dismissing result" 582 ); 583 if (expected.notDismissalKeys) { 584 for (let value of expected.notDismissalKeys) { 585 Assert.ok( 586 !(await QuickSuggest.rustBackend.isDismissedByKey(value)), 587 "isDismissedByKey should return false for notDismissalKey: " + value 588 ); 589 } 590 } 591 592 // Do another search. The remote settings suggestion should now be matched. 593 await check_results({ 594 context: createContext(SEARCH_STRING, { 595 providers: [UrlbarProviderQuickSuggest.name], 596 isPrivate: false, 597 }), 598 matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], 599 }); 600 601 // Clear dismissals. 602 await QuickSuggest.clearDismissedSuggestions(); 603 Assert.ok( 604 !(await QuickSuggest.isResultDismissed(result)), 605 "isResultDismissed should return false after clearing dismissals" 606 ); 607 608 // The Merino suggestion should be matched again. 609 await check_results({ 610 context: createContext(SEARCH_STRING, { 611 providers: [UrlbarProviderQuickSuggest.name], 612 isPrivate: false, 613 }), 614 matches: [expectedResult], 615 conditionalPayloadProperties: { 616 url: { ignore: true }, 617 urlTimestampIndex: { ignore: true }, 618 }, 619 }); 620 } 621 622 MerinoTestUtils.server.reset(); 623 merinoClient().resetSession(); 624 }); 625 626 // Tests dismissals of unmanaged Merino suggestions (suggestions that are not 627 // managed by a `SuggestFeature`). 628 add_task(async function dismissals_unmanaged_1() { 629 UrlbarPrefs.set("quicksuggest.online.available", true); 630 UrlbarPrefs.set("quicksuggest.online.enabled", true); 631 632 // The "top_picks" provider is the only supported unmanaged suggestion. 633 let provider = "top_picks"; 634 635 let tests = [ 636 { 637 suggestion: { 638 provider, 639 url: "https://example.com/0", 640 score: 1, 641 }, 642 expected: { 643 // dismissal key should be the `url` value 644 dismissalKey: "https://example.com/0", 645 }, 646 }, 647 { 648 suggestion: { 649 provider, 650 url: "https://example.com/1", 651 original_url: "https://example.com/1-original-url", 652 score: 1, 653 }, 654 expected: { 655 // dismissal key should be the `original_url` value 656 dismissalKey: "https://example.com/1-original-url", 657 notDismissalKeys: ["https://example.com/1"], 658 }, 659 }, 660 { 661 suggestion: { 662 provider, 663 url: "https://example.com/2", 664 original_url: "https://example.com/2-original-url", 665 dismissal_key: "2-dismissal-key", 666 score: 1, 667 }, 668 expected: { 669 // dismissal key should be the `dismissal_key` value 670 dismissalKey: "2-dismissal-key", 671 notDismissalKeys: [ 672 "https://example.com/2", 673 "https://example.com/2-original-url", 674 ], 675 }, 676 }, 677 ]; 678 679 for (let test of tests) { 680 info("Doing subtest: " + JSON.stringify(test)); 681 682 let { suggestion, expected } = test; 683 684 MerinoTestUtils.server.response = 685 MerinoTestUtils.server.makeDefaultResponse(); 686 MerinoTestUtils.server.response.body.suggestions = [suggestion]; 687 688 let expectedResult = { 689 type: UrlbarUtils.RESULT_TYPE.URL, 690 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 691 heuristic: false, 692 payload: { 693 provider, 694 url: suggestion.url, 695 originalUrl: suggestion.original_url, 696 dismissalKey: suggestion.dismissal_key, 697 source: "merino", 698 isSponsored: false, 699 shouldShowUrl: true, 700 isBlockable: true, 701 isManageable: true, 702 telemetryType: provider, 703 }, 704 }; 705 706 // Do a search. The Merino suggestion should be matched. 707 let context = createContext(SEARCH_STRING, { 708 providers: [UrlbarProviderQuickSuggest.name], 709 isPrivate: false, 710 }); 711 await check_results({ 712 context, 713 matches: [expectedResult], 714 }); 715 716 let result = context.results[0]; 717 Assert.ok( 718 !QuickSuggest.getFeatureByResult(result), 719 "Sanity check: The actual result should not be managed by a feature" 720 ); 721 722 // Dismiss the Merino result. 723 await QuickSuggest.dismissResult(result); 724 Assert.ok( 725 await QuickSuggest.isResultDismissed(result), 726 "isResultDismissed should return true after dismissing result" 727 ); 728 729 Assert.ok( 730 await QuickSuggest.rustBackend.isDismissedByKey(expected.dismissalKey), 731 "isDismissedByKey should return true after dismissing result" 732 ); 733 if (expected.notDismissalKeys) { 734 for (let value of expected.notDismissalKeys) { 735 Assert.ok( 736 !(await QuickSuggest.rustBackend.isDismissedByKey(value)), 737 "isDismissedByKey should return false for notDismissalKey: " + value 738 ); 739 } 740 } 741 742 // Do another search. The remote settings suggestion should now be matched. 743 await check_results({ 744 context: createContext(SEARCH_STRING, { 745 providers: [UrlbarProviderQuickSuggest.name], 746 isPrivate: false, 747 }), 748 matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], 749 }); 750 751 // Clear dismissals. 752 await QuickSuggest.clearDismissedSuggestions(); 753 Assert.ok( 754 !(await QuickSuggest.isResultDismissed(result)), 755 "isResultDismissed should return false after clearing dismissals" 756 ); 757 758 // The Merino suggestion should be matched again. 759 await check_results({ 760 context: createContext(SEARCH_STRING, { 761 providers: [UrlbarProviderQuickSuggest.name], 762 isPrivate: false, 763 }), 764 matches: [expectedResult], 765 }); 766 } 767 768 MerinoTestUtils.server.reset(); 769 merinoClient().resetSession(); 770 }); 771 772 // Tests dismissals of unmanaged Merino suggestions (suggestions that are not 773 // managed by a `SuggestFeature`) that all have the same URL but different 774 // original URLs and dismissal keys. 775 add_task(async function dismissals_unmanaged_2() { 776 UrlbarPrefs.set("quicksuggest.online.available", true); 777 UrlbarPrefs.set("quicksuggest.online.enabled", true); 778 779 // The "top_picks" provider is the only supported unmanaged suggestion. 780 let provider = "top_picks"; 781 782 MerinoTestUtils.server.response = 783 MerinoTestUtils.server.makeDefaultResponse(); 784 MerinoTestUtils.server.response.body.suggestions = [ 785 // all three: url, original_url, dismissal_key 786 { 787 provider, 788 url: "https://example.com/url", 789 original_url: "https://example.com/original_url", 790 dismissal_key: "dismissal-key", 791 score: 1.0, 792 }, 793 // two: url, original_url 794 { 795 provider, 796 url: "https://example.com/url", 797 original_url: "https://example.com/original_url", 798 score: 0.9, 799 }, 800 // only one: url 801 { 802 provider, 803 url: "https://example.com/url", 804 score: 0.8, 805 }, 806 ]; 807 808 let expectedBaseResult = { 809 type: UrlbarUtils.RESULT_TYPE.URL, 810 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 811 heuristic: false, 812 payload: { 813 provider, 814 url: "https://example.com/url", 815 source: "merino", 816 isSponsored: false, 817 shouldShowUrl: true, 818 isBlockable: true, 819 isManageable: true, 820 telemetryType: provider, 821 }, 822 }; 823 824 // Do a search. The first Merino suggestion should be matched. 825 info("Doing search 1"); 826 let context = createContext(SEARCH_STRING, { 827 providers: [UrlbarProviderQuickSuggest.name], 828 isPrivate: false, 829 }); 830 await check_results({ 831 context, 832 matches: [ 833 { 834 ...expectedBaseResult, 835 payload: { 836 ...expectedBaseResult.payload, 837 originalUrl: "https://example.com/original_url", 838 dismissalKey: "dismissal-key", 839 }, 840 }, 841 ], 842 }); 843 844 let result = context.results[0]; 845 Assert.ok( 846 !QuickSuggest.getFeatureByResult(result), 847 "Sanity check: The actual result should not be managed by a feature" 848 ); 849 850 // Dismiss it. 851 await QuickSuggest.dismissResult(result); 852 Assert.ok( 853 await QuickSuggest.isResultDismissed(result), 854 "isResultDismissed should return true after dismissing result 1" 855 ); 856 857 Assert.ok( 858 await QuickSuggest.rustBackend.isDismissedByKey("dismissal-key"), 859 "isDismissedByKey should return true after dismissing suggestion 1" 860 ); 861 862 for (let value of [ 863 "https://example.com/url", 864 "https://example.com/original_url", 865 ]) { 866 Assert.ok( 867 !(await QuickSuggest.rustBackend.isDismissedByKey(value)), 868 "isDismissedByKey should return false after dismissing suggestion 1: " + 869 value 870 ); 871 } 872 873 // Do another search. The second suggestion should be matched. 874 info("Doing search 2"); 875 context = createContext(SEARCH_STRING, { 876 providers: [UrlbarProviderQuickSuggest.name], 877 isPrivate: false, 878 }); 879 await check_results({ 880 context, 881 matches: [ 882 { 883 ...expectedBaseResult, 884 payload: { 885 ...expectedBaseResult.payload, 886 originalUrl: "https://example.com/original_url", 887 // no dismissal_key 888 }, 889 }, 890 ], 891 }); 892 893 // Dismiss it. 894 result = context.results[0]; 895 await QuickSuggest.dismissResult(result); 896 Assert.ok( 897 await QuickSuggest.isResultDismissed(result), 898 "isResultDismissed should return true after dismissing result 2" 899 ); 900 901 for (let value of ["dismissal-key", "https://example.com/original_url"]) { 902 Assert.ok( 903 await QuickSuggest.rustBackend.isDismissedByKey(value), 904 "isDismissedByKey should return true after dismissing suggestion 2: " + 905 value 906 ); 907 } 908 909 Assert.ok( 910 !(await QuickSuggest.rustBackend.isDismissedByKey( 911 "https://example.com/url" 912 )), 913 "isDismissedByKey should return false after dismissing suggestion 2" 914 ); 915 916 // Do another search. The third suggestion should be matched. 917 info("Doing search 3"); 918 context = createContext(SEARCH_STRING, { 919 providers: [UrlbarProviderQuickSuggest.name], 920 isPrivate: false, 921 }); 922 await check_results({ 923 context, 924 matches: [ 925 // no dismissal_key or original_url 926 expectedBaseResult, 927 ], 928 }); 929 930 // Dismiss it. 931 result = context.results[0]; 932 await QuickSuggest.dismissResult(result); 933 Assert.ok( 934 await QuickSuggest.isResultDismissed(result), 935 "isResultDismissed should return true after dismissing result 3" 936 ); 937 938 for (let value of [ 939 "dismissal-key", 940 "https://example.com/original_url", 941 "https://example.com/url", 942 ]) { 943 Assert.ok( 944 await QuickSuggest.rustBackend.isDismissedByKey(value), 945 "isDismissedByKey should return true after dismissing suggestion 3: " + 946 value 947 ); 948 } 949 950 await QuickSuggest.clearDismissedSuggestions(); 951 MerinoTestUtils.server.reset(); 952 merinoClient().resetSession(); 953 }); 954 955 // Tests a Merino suggestion that is a top pick/best match. 956 add_task(async function bestMatch() { 957 UrlbarPrefs.set("quicksuggest.online.available", true); 958 UrlbarPrefs.set("quicksuggest.online.enabled", true); 959 960 // Set up a suggestion with `is_top_pick` and the "top_picks" provider so that 961 // UrlbarProviderQuickSuggest will make a default result for it. 962 let provider = "top_picks"; 963 MerinoTestUtils.server.response.body.suggestions = [ 964 { 965 is_top_pick: true, 966 provider, 967 full_keyword: "full_keyword", 968 title: "title", 969 url: "url", 970 icon: null, 971 score: 1, 972 }, 973 ]; 974 975 let context = createContext(SEARCH_STRING, { 976 providers: [UrlbarProviderQuickSuggest.name], 977 isPrivate: false, 978 }); 979 await check_results({ 980 context, 981 matches: [ 982 { 983 isBestMatch: true, 984 type: UrlbarUtils.RESULT_TYPE.URL, 985 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 986 heuristic: false, 987 payload: { 988 telemetryType: provider, 989 title: "full_keyword — title", 990 url: "url", 991 icon: null, 992 isSponsored: false, 993 isBlockable: true, 994 isManageable: true, 995 source: "merino", 996 provider, 997 }, 998 }, 999 ], 1000 }); 1001 1002 // This isn't necessary since `check_results()` checks `isBestMatch`, but 1003 // check it here explicitly for good measure. 1004 Assert.ok(context.results[0].isBestMatch, "Result is a best match"); 1005 1006 MerinoTestUtils.server.reset(); 1007 merinoClient().resetSession(); 1008 }); 1009 1010 // Tests a sponsored suggestion that isn't managed by a feature. When the `all` 1011 // pref is disabled, a result for the suggestion should not be added. 1012 add_task(async function unmanaged_sponsored_allDisabled() { 1013 await doUnmanagedTest({ 1014 pref: "suggest.quicksuggest.all", 1015 suggestion: { 1016 title: "Sponsored without feature", 1017 url: "https://example.com/sponsored-without-feature", 1018 is_sponsored: true, 1019 }, 1020 shouldBeAdded: false, 1021 }); 1022 }); 1023 1024 // Tests a sponsored suggestion that isn't managed by a feature. When the 1025 // sponsored pref is disabled, a result for the suggestion should not be added. 1026 add_task(async function unmanaged_sponsored_sponsoredDisabled() { 1027 await doUnmanagedTest({ 1028 pref: "suggest.quicksuggest.sponsored", 1029 suggestion: { 1030 title: "Sponsored without feature", 1031 url: "https://example.com/sponsored-without-feature", 1032 is_sponsored: true, 1033 }, 1034 shouldBeAdded: false, 1035 }); 1036 }); 1037 1038 // Tests a nonsponsored suggestion that isn't managed by a feature. When the 1039 // `all` pref is disabled, a result for the suggestion should not be added. 1040 add_task(async function unmanaged_nonsponsored_allDisabled() { 1041 await doUnmanagedTest({ 1042 pref: "suggest.quicksuggest.all", 1043 suggestion: { 1044 title: "Nonsponsored without feature", 1045 url: "https://example.com/nonsponsored-without-feature", 1046 // no is_sponsored 1047 }, 1048 shouldBeAdded: false, 1049 }); 1050 }); 1051 1052 // Tests a nonsponsored suggestion that isn't managed by a feature. When the 1053 // `all` pref is enabled and the sponsored pref is disabled, a result for the 1054 // suggestion should be added. 1055 add_task(async function unmanaged_nonsponsored_sponsoredDisabled() { 1056 await doUnmanagedTest({ 1057 pref: "suggest.quicksuggest.sponsored", 1058 suggestion: { 1059 title: "Nonsponsored without feature", 1060 url: "https://example.com/nonsponsored-without-feature", 1061 // no is_sponsored 1062 }, 1063 shouldBeAdded: true, 1064 }); 1065 }); 1066 1067 async function doUnmanagedTest({ pref, suggestion, shouldBeAdded }) { 1068 // The "top_picks" provider is the only supported unmanaged suggestion. 1069 suggestion.provider = "top_picks"; 1070 1071 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1072 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1073 await QuickSuggestTestUtils.forceSync(); 1074 1075 UrlbarPrefs.set("quicksuggest.online.available", true); 1076 UrlbarPrefs.set("quicksuggest.online.enabled", true); 1077 MerinoTestUtils.server.response.body.suggestions = [suggestion]; 1078 1079 let expectedResult = { 1080 type: UrlbarUtils.RESULT_TYPE.URL, 1081 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 1082 heuristic: false, 1083 payload: { 1084 title: suggestion.title, 1085 url: suggestion.url, 1086 provider: suggestion.provider, 1087 telemetryType: suggestion.provider, 1088 isSponsored: !!suggestion.is_sponsored, 1089 source: "merino", 1090 isBlockable: true, 1091 isManageable: true, 1092 shouldShowUrl: true, 1093 }, 1094 }; 1095 1096 // Do an initial search. The `all` pref and sponsored suggestions are both 1097 // enabled, so the suggestion should be matched. 1098 info("Doing search 1"); 1099 await check_results({ 1100 context: createContext("test", { 1101 providers: [UrlbarProviderQuickSuggest.name], 1102 isPrivate: false, 1103 }), 1104 matches: [expectedResult], 1105 }); 1106 1107 // Set the passed-in pref to false and do another search. The suggestion 1108 // should be matched as expected. 1109 UrlbarPrefs.set(pref, false); 1110 await QuickSuggestTestUtils.forceSync(); 1111 1112 info("Doing search 2"); 1113 await check_results({ 1114 context: createContext("test", { 1115 providers: [UrlbarProviderQuickSuggest.name], 1116 isPrivate: false, 1117 }), 1118 matches: shouldBeAdded ? [expectedResult] : [], 1119 }); 1120 1121 // Flip the pref back to true and do a third search. 1122 UrlbarPrefs.set(pref, true); 1123 await QuickSuggestTestUtils.forceSync(); 1124 1125 info("Doing search 3"); 1126 let context = createContext("test", { 1127 providers: [UrlbarProviderQuickSuggest.name], 1128 isPrivate: false, 1129 }); 1130 await check_results({ 1131 context, 1132 matches: [expectedResult], 1133 }); 1134 1135 // Trigger the dismiss command on the result. 1136 let dismissalPromise = TestUtils.topicObserved( 1137 "quicksuggest-dismissals-changed" 1138 ); 1139 triggerCommand({ 1140 feature: UrlbarProvidersManager.getProvider( 1141 UrlbarProviderQuickSuggest.name 1142 ), 1143 command: "dismiss", 1144 result: context.results[0], 1145 expectedCountsByCall: { 1146 removeResult: 1, 1147 }, 1148 }); 1149 await dismissalPromise; 1150 1151 Assert.ok( 1152 await QuickSuggest.isResultDismissed(context.results[0]), 1153 "The result should be dismissed" 1154 ); 1155 1156 await QuickSuggest.clearDismissedSuggestions(); 1157 MerinoTestUtils.server.reset(); 1158 merinoClient().resetSession(); 1159 } 1160 1161 // An unmanaged suggestion with an unrecognized Merino provider (i.e., not 1162 // "top_picks") should not be added. 1163 add_task(async function unmanaged_unrecognized() { 1164 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1165 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1166 await QuickSuggestTestUtils.forceSync(); 1167 1168 UrlbarPrefs.set("quicksuggest.online.available", true); 1169 UrlbarPrefs.set("quicksuggest.online.enabled", true); 1170 MerinoTestUtils.server.response.body.suggestions = [ 1171 { 1172 title: "Some unrecognized suggestion", 1173 url: "https://example.com/unmanaged_unrecognized", 1174 provider: "unmanaged-unrecognized-provider", 1175 }, 1176 ]; 1177 1178 await check_results({ 1179 context: createContext("test", { 1180 providers: [UrlbarProviderQuickSuggest.name], 1181 isPrivate: false, 1182 }), 1183 matches: [], 1184 }); 1185 }); 1186 1187 function merinoClient() { 1188 return QuickSuggest.getFeature("SuggestBackendMerino")?.client; 1189 } 1190 1191 async function resetRemoteSettingsData() { 1192 await QuickSuggestTestUtils.setRemoteSettingsRecords([ 1193 { 1194 collection: QuickSuggestTestUtils.RS_COLLECTION.AMP, 1195 type: QuickSuggestTestUtils.RS_TYPE.AMP, 1196 attachment: REMOTE_SETTINGS_RESULTS, 1197 }, 1198 ]); 1199 }