test_quicksuggest.js (78412B)
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 for AMP and Wikipedia suggestions and some aspects of the Suggest 6 // urlbar provider that aren't tested elsewhere. See also 7 // `test_quicksuggest_merino.js`. 8 9 "use strict"; 10 11 ChromeUtils.defineESModuleGetters(this, { 12 AmpMatchingStrategy: 13 "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs", 14 AmpSuggestions: 15 "moz-src:///browser/components/urlbar/private/AmpSuggestions.sys.mjs", 16 SuggestBackendRust: 17 "moz-src:///browser/components/urlbar/private/SuggestBackendRust.sys.mjs", 18 SuggestionProvider: 19 "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs", 20 }); 21 22 const SPONSORED_SEARCH_STRING = "amp"; 23 const NONSPONSORED_SEARCH_STRING = "wikipedia"; 24 const SPONSORED_AND_NONSPONSORED_SEARCH_STRING = "sponsored and non-sponsored"; 25 26 const HTTP_SEARCH_STRING = "http prefix"; 27 const HTTPS_SEARCH_STRING = "https prefix"; 28 const PREFIX_SUGGESTIONS_STRIPPED_URL = "example.com/prefix-test"; 29 30 const ONE_CHAR_SEARCH_STRINGS = ["x", "x ", " x", " x "]; 31 32 const { TIMESTAMP_TEMPLATE, TIMESTAMP_LENGTH } = AmpSuggestions; 33 const TIMESTAMP_SEARCH_STRING = "timestamp"; 34 const TIMESTAMP_SUGGESTION_URL = `http://example.com/timestamp-${TIMESTAMP_TEMPLATE}`; 35 const TIMESTAMP_SUGGESTION_CLICK_URL = `http://click.reporting.test.com/timestamp-${TIMESTAMP_TEMPLATE}-foo`; 36 37 const REMOTE_SETTINGS_RESULTS = [ 38 QuickSuggestTestUtils.ampRemoteSettings({ 39 keywords: [ 40 SPONSORED_SEARCH_STRING, 41 SPONSORED_AND_NONSPONSORED_SEARCH_STRING, 42 ], 43 }), 44 QuickSuggestTestUtils.wikipediaRemoteSettings({ 45 keywords: [ 46 NONSPONSORED_SEARCH_STRING, 47 SPONSORED_AND_NONSPONSORED_SEARCH_STRING, 48 ], 49 }), 50 { 51 id: 3, 52 url: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL, 53 title: "HTTP Suggestion", 54 keywords: [HTTP_SEARCH_STRING], 55 full_keywords: [[HTTP_SEARCH_STRING, 1]], 56 click_url: "http://example.com/http-click", 57 impression_url: "http://example.com/http-impression", 58 advertiser: "HttpAdvertiser", 59 iab_category: "22 - Shopping", 60 icon: "1234", 61 serp_categories: [2], 62 }, 63 { 64 id: 4, 65 url: "https://" + PREFIX_SUGGESTIONS_STRIPPED_URL, 66 title: "https suggestion", 67 keywords: [HTTPS_SEARCH_STRING], 68 full_keywords: [[HTTPS_SEARCH_STRING, 1]], 69 click_url: "http://click.reporting.test.com/prefix", 70 impression_url: "http://impression.reporting.test.com/prefix", 71 advertiser: "TestAdvertiserPrefix", 72 iab_category: "22 - Shopping", 73 icon: "1234", 74 }, 75 { 76 id: 5, 77 url: TIMESTAMP_SUGGESTION_URL, 78 title: "Timestamp suggestion", 79 keywords: [TIMESTAMP_SEARCH_STRING], 80 full_keywords: [[TIMESTAMP_SEARCH_STRING, 1]], 81 click_url: TIMESTAMP_SUGGESTION_CLICK_URL, 82 impression_url: "http://impression.reporting.test.com/timestamp", 83 advertiser: "TestAdvertiserTimestamp", 84 iab_category: "22 - Shopping", 85 icon: "1234", 86 }, 87 QuickSuggestTestUtils.ampRemoteSettings({ 88 keywords: [...ONE_CHAR_SEARCH_STRINGS, "12", "a longer keyword"], 89 title: "Suggestion with 1-char keyword", 90 url: "http://example.com/1-char-keyword", 91 }), 92 QuickSuggestTestUtils.ampRemoteSettings({ 93 keywords: [ 94 "amp full key", 95 "amp full keyw", 96 "amp full keywo", 97 "amp full keywor", 98 "amp full keyword", 99 "xyz", 100 ], 101 full_keywords: [ 102 ["amp full keyword", 5], 103 ["xyz", 1], 104 ], 105 title: "AMP suggestion with full keyword and prefix keywords", 106 url: "https://example.com/amp-full-keyword", 107 }), 108 QuickSuggestTestUtils.wikipediaRemoteSettings({ 109 keywords: [ 110 "wikipedia full key", 111 "wikipedia full keyw", 112 "wikipedia full keywo", 113 "wikipedia full keywor", 114 "wikipedia full keyword", 115 ], 116 full_keywords: [["wikipedia full keyword", 5]], 117 title: "Wikipedia suggestion with full keyword and prefix keywords", 118 url: "https://example.com/wikipedia-full-keyword", 119 }), 120 ]; 121 122 const MERINO_SUGGESTIONS = [ 123 { 124 title: "Amp Suggestion", 125 url: "https://example.com/amp", 126 provider: "adm", 127 is_sponsored: true, 128 score: 0.31, 129 icon: "https://example.com/amp-icon", 130 iab_category: "22 - Shopping", 131 block_id: 1, 132 full_keyword: "amp", 133 advertiser: "Amp", 134 impression_url: "https://example.com/amp-impression", 135 click_url: "https://example.com/amp-click", 136 }, 137 { 138 title: "Wikipedia Suggestion", 139 url: "https://example.com/wikipedia", 140 is_sponsored: false, 141 score: 0.23, 142 description: "description", 143 icon: "https://example.com/wikipedia-icon", 144 full_keyword: "wikipedia", 145 advertiser: "dynamic-wikipedia", 146 block_id: 0, 147 provider: "wikipedia", 148 categories: [6], // Education 149 }, 150 ]; 151 152 let gMaxResultsSuggestionsCount; 153 154 function expectedSponsoredPriorityResult() { 155 return { 156 ...QuickSuggestTestUtils.ampResult(), 157 isBestMatch: true, 158 suggestedIndex: 1, 159 isSuggestedIndexRelativeToGroup: false, 160 }; 161 } 162 163 function expectedHttpResult() { 164 let suggestion = REMOTE_SETTINGS_RESULTS[2]; 165 return QuickSuggestTestUtils.ampResult({ 166 keyword: HTTP_SEARCH_STRING, 167 title: suggestion.title, 168 url: suggestion.url, 169 originalUrl: suggestion.url, 170 impressionUrl: suggestion.impression_url, 171 clickUrl: suggestion.click_url, 172 blockId: suggestion.id, 173 advertiser: suggestion.advertiser, 174 categories: suggestion.serp_categories, 175 suggestedIndex: -1, 176 }); 177 } 178 179 function expectedHttpsResult() { 180 let suggestion = REMOTE_SETTINGS_RESULTS[3]; 181 return QuickSuggestTestUtils.ampResult({ 182 keyword: HTTPS_SEARCH_STRING, 183 title: suggestion.title, 184 url: suggestion.url, 185 originalUrl: suggestion.url, 186 impressionUrl: suggestion.impression_url, 187 clickUrl: suggestion.click_url, 188 blockId: suggestion.id, 189 advertiser: suggestion.advertiser, 190 suggestedIndex: -1, 191 }); 192 } 193 194 add_setup(async function init() { 195 // Add a bunch of suggestions that have the same keyword so we can verify the 196 // provider respects its `queryContext.maxResults` cap when adding results. 197 let maxResults = UrlbarPrefs.get("maxRichResults"); 198 Assert.greater(maxResults, 0, "This test expects maxRichResults to be > 0"); 199 gMaxResultsSuggestionsCount = 2 * maxResults; 200 for (let i = 0; i < gMaxResultsSuggestionsCount; i++) { 201 REMOTE_SETTINGS_RESULTS.push( 202 QuickSuggestTestUtils.ampRemoteSettings({ 203 keywords: ["maxresults"], 204 title: "maxresults " + i, 205 url: "https://example.com/maxresults/" + i, 206 }) 207 ); 208 } 209 210 // Install a default test engine. 211 let engine = await addTestSuggestionsEngine(); 212 await Services.search.setDefault( 213 engine, 214 Ci.nsISearchService.CHANGE_REASON_UNKNOWN 215 ); 216 217 UrlbarPrefs.set("quicksuggest.ampTopPickCharThreshold", 0); 218 219 await QuickSuggestTestUtils.ensureQuickSuggestInit(); 220 await resetRemoteSettingsData(); 221 }); 222 223 add_task(async function offline_telemetryType_amp() { 224 Assert.equal( 225 QuickSuggest.getFeature("AmpSuggestions").getSuggestionTelemetryType({ 226 source: "rust", 227 }), 228 "adm_sponsored", 229 "Telemetry type should be 'adm_sponsored'" 230 ); 231 }); 232 233 add_task(async function offline_telemetryType_wikipedia() { 234 Assert.equal( 235 QuickSuggest.getFeature("WikipediaSuggestions").getSuggestionTelemetryType({ 236 source: "rust", 237 }), 238 "adm_nonsponsored", 239 "Telemetry type should be 'adm_nonsponsored'" 240 ); 241 }); 242 243 add_task(async function online_telemetryType_amp() { 244 Assert.equal( 245 QuickSuggest.getFeature("AmpSuggestions").getSuggestionTelemetryType({ 246 source: "merino", 247 }), 248 "adm_sponsored", 249 "Telemetry type should be 'adm_sponsored'" 250 ); 251 }); 252 253 add_task(async function online_telemetryType_wikipedia() { 254 Assert.equal( 255 QuickSuggest.getFeature("WikipediaSuggestions").getSuggestionTelemetryType({ 256 source: "merino", 257 }), 258 "wikipedia", 259 "Telemetry type should be 'wikipedia'" 260 ); 261 }); 262 263 // Tests with both `all` and sponsored enabled with a sponsored search string. 264 // Sponsored suggestions should be matched. 265 add_task(async function allEnabled_sponsoredEnabled_sponsoredSearch() { 266 UrlbarPrefs.set("suggest.quicksuggest.all", true); 267 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 268 await QuickSuggestTestUtils.forceSync(); 269 270 let context = createContext(SPONSORED_SEARCH_STRING, { 271 providers: [UrlbarProviderQuickSuggest.name], 272 isPrivate: false, 273 }); 274 await check_results({ 275 context, 276 matches: [QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 })], 277 }); 278 279 // The title should include the full keyword and em dash, and the part of the 280 // title that the search string does not match should be highlighted. 281 let result = context.results[0]; 282 let { value, highlights } = result.getDisplayableValueAndHighlights("title", { 283 tokens: context.tokens, 284 }); 285 Assert.equal( 286 value, 287 `${SPONSORED_SEARCH_STRING} — Amp Suggestion`, 288 "The title should be correct" 289 ); 290 Assert.deepEqual(highlights, [], "The highlights should be correct"); 291 }); 292 293 // Tests with both `all` and sponsored enabled with a non-sponsored search 294 // string. Non-sponsored suggestions should be matched. 295 add_task(async function allEnabled_sponsoredEnabled_nonsponsoredSearch() { 296 UrlbarPrefs.set("suggest.quicksuggest.all", true); 297 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 298 await QuickSuggestTestUtils.forceSync(); 299 300 let context = createContext(NONSPONSORED_SEARCH_STRING, { 301 providers: [UrlbarProviderQuickSuggest.name], 302 isPrivate: false, 303 }); 304 await check_results({ 305 context, 306 matches: [QuickSuggestTestUtils.wikipediaResult()], 307 }); 308 309 // The title should include the full keyword and em dash, and the part of the 310 // title that the search string does not match should be highlighted. 311 let result = context.results[0]; 312 let { value, highlights } = result.getDisplayableValueAndHighlights("title", { 313 tokens: context.tokens, 314 }); 315 Assert.equal( 316 value, 317 `${NONSPONSORED_SEARCH_STRING} — Wikipedia Suggestion`, 318 "The title should be correct" 319 ); 320 Assert.deepEqual(highlights, [], "The highlights should be correct"); 321 }); 322 323 // Tests with both `all` and sponsored enabled with a search string that doesn't 324 // match anything. 325 add_task(async function allEnabled_sponsoredEnabled_nonmatchingSearch() { 326 UrlbarPrefs.set("suggest.quicksuggest.all", true); 327 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 328 await QuickSuggestTestUtils.forceSync(); 329 330 let context = createContext("this doesn't match anything", { 331 providers: [UrlbarProviderQuickSuggest.name], 332 isPrivate: false, 333 }); 334 await check_results({ context, matches: [] }); 335 }); 336 337 // Tests with `all` enabled and sponsored disabled with a sponsored search 338 // string. No suggestions should be matched. 339 add_task(async function allEnabled_sponsoredDisabled_sponsoredSearch() { 340 UrlbarPrefs.set("suggest.quicksuggest.all", true); 341 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 342 await QuickSuggestTestUtils.forceSync(); 343 344 let context = createContext(SPONSORED_SEARCH_STRING, { 345 providers: [UrlbarProviderQuickSuggest.name], 346 isPrivate: false, 347 }); 348 await check_results({ context, matches: [] }); 349 }); 350 351 // Tests with `all` enabled and sponsored disabled with a non-sponsored search 352 // string. Non-sponsored suggestions should be matched. 353 add_task(async function allEnabled_sponsoredDisabled_nonsponsoredSearch() { 354 UrlbarPrefs.set("suggest.quicksuggest.all", true); 355 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 356 await QuickSuggestTestUtils.forceSync(); 357 358 let context = createContext(NONSPONSORED_SEARCH_STRING, { 359 providers: [UrlbarProviderQuickSuggest.name], 360 isPrivate: false, 361 }); 362 await check_results({ 363 context, 364 matches: [QuickSuggestTestUtils.wikipediaResult()], 365 }); 366 }); 367 368 // Tests with `all` disabled and sponsored enabled with a sponsored search 369 // string. No suggestions should be matched. The settings UI does not make this 370 // case possible, but the prefs are independent, so it's technically possible. 371 add_task(async function allDisabled_sponsoredEnabled_sponsoredSearch() { 372 UrlbarPrefs.set("suggest.quicksuggest.all", false); 373 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 374 await QuickSuggestTestUtils.forceSync(); 375 376 let context = createContext(SPONSORED_SEARCH_STRING, { 377 providers: [UrlbarProviderQuickSuggest.name], 378 isPrivate: false, 379 }); 380 await check_results({ context, matches: [] }); 381 }); 382 383 // Tests with `all` disabled and sponsored enabled with a non-sponsored search 384 // string. No suggestions should be matched. The settings UI does not make this 385 // case possible, but the prefs are independent, so it's technically possible. 386 add_task(async function allDisabled_sponsoredEnabled_nonsponsoredSearch() { 387 UrlbarPrefs.set("suggest.quicksuggest.all", false); 388 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 389 await QuickSuggestTestUtils.forceSync(); 390 391 let context = createContext(NONSPONSORED_SEARCH_STRING, { 392 providers: [UrlbarProviderQuickSuggest.name], 393 isPrivate: false, 394 }); 395 await check_results({ context, matches: [] }); 396 }); 397 398 // Tests with both `all` and sponsored disabled with a sponsored search string. 399 // No suggestions should be matched. 400 add_task(async function allDisabled_sponsoredDisabled_sponsoredSearch() { 401 UrlbarPrefs.set("suggest.quicksuggest.all", false); 402 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 403 await QuickSuggestTestUtils.forceSync(); 404 405 let context = createContext(SPONSORED_SEARCH_STRING, { 406 providers: [UrlbarProviderQuickSuggest.name], 407 isPrivate: false, 408 }); 409 await check_results({ context, matches: [] }); 410 }); 411 412 // Tests with both `all` and sponsored disabled with a non-sponsored search 413 // string. No suggestions should be matched. 414 add_task(async function allDisabled_sponsoredDisabled_nonsponsoredSearch() { 415 UrlbarPrefs.set("suggest.quicksuggest.all", false); 416 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 417 await QuickSuggestTestUtils.forceSync(); 418 419 let context = createContext(NONSPONSORED_SEARCH_STRING, { 420 providers: [UrlbarProviderQuickSuggest.name], 421 isPrivate: false, 422 }); 423 await check_results({ context, matches: [] }); 424 }); 425 426 // Search string matching should be case insensitive and ignore leading spaces. 427 add_task(async function caseInsensitiveAndLeadingSpaces() { 428 UrlbarPrefs.set("suggest.quicksuggest.all", true); 429 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 430 await QuickSuggestTestUtils.forceSync(); 431 432 let context = createContext(" " + SPONSORED_SEARCH_STRING.toUpperCase(), { 433 providers: [UrlbarProviderQuickSuggest.name], 434 isPrivate: false, 435 }); 436 await check_results({ 437 context, 438 matches: [QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 })], 439 }); 440 }); 441 442 // The provider should not be active for search strings that are empty or 443 // contain only spaces. 444 add_task(async function emptySearchStringsAndSpaces() { 445 UrlbarPrefs.set("suggest.quicksuggest.all", true); 446 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 447 await QuickSuggestTestUtils.forceSync(); 448 449 let searchStrings = ["", " ", " ", " "]; 450 for (let str of searchStrings) { 451 let msg = JSON.stringify(str) + ` (length = ${str.length})`; 452 info("Testing search string: " + msg); 453 454 let context = createContext(str, { 455 providers: [UrlbarProviderQuickSuggest.name], 456 isPrivate: false, 457 }); 458 await check_results({ 459 context, 460 matches: [], 461 }); 462 Assert.ok( 463 !(await UrlbarProvidersManager.getProvider( 464 UrlbarProviderQuickSuggest.name 465 ).isActive(context)), 466 "Provider should not be active for search string: " + msg 467 ); 468 } 469 }); 470 471 // Results should be returned even when `browser.search.suggest.enabled` is 472 // false. 473 add_task(async function browser_search_suggest_disabled() { 474 UrlbarPrefs.set("suggest.quicksuggest.all", true); 475 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 476 UrlbarPrefs.set("browser.search.suggest.enabled", false); 477 await QuickSuggestTestUtils.forceSync(); 478 479 let context = createContext(SPONSORED_SEARCH_STRING, { 480 providers: [UrlbarProviderQuickSuggest.name], 481 isPrivate: false, 482 }); 483 await check_results({ 484 context, 485 matches: [QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 })], 486 }); 487 488 UrlbarPrefs.clear("browser.search.suggest.enabled"); 489 }); 490 491 // Results should be returned even when `browser.urlbar.suggest.searches` is 492 // false. 493 add_task(async function browser_suggest_searches_disabled() { 494 UrlbarPrefs.set("suggest.quicksuggest.all", true); 495 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 496 UrlbarPrefs.set("suggest.searches", false); 497 await QuickSuggestTestUtils.forceSync(); 498 499 let context = createContext(SPONSORED_SEARCH_STRING, { 500 providers: [UrlbarProviderQuickSuggest.name], 501 isPrivate: false, 502 }); 503 await check_results({ 504 context, 505 matches: [QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 })], 506 }); 507 508 UrlbarPrefs.clear("suggest.searches"); 509 }); 510 511 // Neither sponsored nor non-sponsored results should appear in private contexts 512 // even when suggestions in private windows are enabled. 513 add_task(async function privateContext() { 514 UrlbarPrefs.set("suggest.quicksuggest.all", true); 515 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 516 await QuickSuggestTestUtils.forceSync(); 517 518 for (let privateSuggestionsEnabled of [true, false]) { 519 UrlbarPrefs.set( 520 "browser.search.suggest.enabled.private", 521 privateSuggestionsEnabled 522 ); 523 let context = createContext(SPONSORED_SEARCH_STRING, { 524 providers: [UrlbarProviderQuickSuggest.name], 525 isPrivate: true, 526 }); 527 await check_results({ 528 context, 529 matches: [], 530 }); 531 } 532 533 UrlbarPrefs.clear("browser.search.suggest.enabled.private"); 534 }); 535 536 // When search suggestions come before general results and the only general 537 // result is a quick suggest result, it should come last. 538 add_task(async function suggestionsBeforeGeneral_only() { 539 UrlbarPrefs.set("suggest.quicksuggest.all", true); 540 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 541 UrlbarPrefs.set("browser.search.suggest.enabled", true); 542 UrlbarPrefs.set("suggest.searches", true); 543 UrlbarPrefs.set("showSearchSuggestionsFirst", true); 544 await QuickSuggestTestUtils.forceSync(); 545 546 let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); 547 await check_results({ 548 context, 549 matches: [ 550 makeSearchResult(context, { 551 heuristic: true, 552 query: SPONSORED_SEARCH_STRING, 553 engineName: Services.search.defaultEngine.name, 554 }), 555 makeSearchResult(context, { 556 query: SPONSORED_SEARCH_STRING, 557 suggestion: SPONSORED_SEARCH_STRING + " foo", 558 engineName: Services.search.defaultEngine.name, 559 }), 560 makeSearchResult(context, { 561 query: SPONSORED_SEARCH_STRING, 562 suggestion: SPONSORED_SEARCH_STRING + " bar", 563 engineName: Services.search.defaultEngine.name, 564 }), 565 QuickSuggestTestUtils.ampResult(), 566 ], 567 }); 568 569 UrlbarPrefs.clear("browser.search.suggest.enabled"); 570 UrlbarPrefs.clear("suggest.searches"); 571 UrlbarPrefs.clear("showSearchSuggestionsFirst"); 572 }); 573 574 // When search suggestions come before general results and there are other 575 // general results besides quick suggest, the quick suggest result should come 576 // last. 577 add_task(async function suggestionsBeforeGeneral_others() { 578 UrlbarPrefs.set("suggest.quicksuggest.all", true); 579 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 580 UrlbarPrefs.set("browser.search.suggest.enabled", true); 581 UrlbarPrefs.set("suggest.searches", true); 582 UrlbarPrefs.set("showSearchSuggestionsFirst", true); 583 await QuickSuggestTestUtils.forceSync(); 584 585 let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); 586 587 // Add some history that will match our query below. 588 let maxResults = UrlbarPrefs.get("maxRichResults"); 589 let historyResults = []; 590 for (let i = 0; i < maxResults; i++) { 591 let url = "http://example.com/" + SPONSORED_SEARCH_STRING + i; 592 historyResults.push( 593 makeVisitResult(context, { 594 uri: url, 595 title: "test visit for " + url, 596 }) 597 ); 598 await PlacesTestUtils.addVisits(url); 599 } 600 historyResults = historyResults.reverse().slice(0, historyResults.length - 4); 601 602 await check_results({ 603 context, 604 matches: [ 605 makeSearchResult(context, { 606 heuristic: true, 607 query: SPONSORED_SEARCH_STRING, 608 engineName: Services.search.defaultEngine.name, 609 }), 610 makeSearchResult(context, { 611 query: SPONSORED_SEARCH_STRING, 612 suggestion: SPONSORED_SEARCH_STRING + " foo", 613 engineName: Services.search.defaultEngine.name, 614 }), 615 makeSearchResult(context, { 616 query: SPONSORED_SEARCH_STRING, 617 suggestion: SPONSORED_SEARCH_STRING + " bar", 618 engineName: Services.search.defaultEngine.name, 619 }), 620 QuickSuggestTestUtils.ampResult(), 621 ...historyResults, 622 ], 623 }); 624 625 UrlbarPrefs.clear("browser.search.suggest.enabled"); 626 UrlbarPrefs.clear("suggest.searches"); 627 UrlbarPrefs.clear("showSearchSuggestionsFirst"); 628 await PlacesUtils.history.clear(); 629 }); 630 631 // When general results come before search suggestions and the only general 632 // result is a quick suggest result, it should come before suggestions. 633 add_task(async function generalBeforeSuggestions_only() { 634 UrlbarPrefs.set("suggest.quicksuggest.all", true); 635 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 636 UrlbarPrefs.set("browser.search.suggest.enabled", true); 637 UrlbarPrefs.set("suggest.searches", true); 638 UrlbarPrefs.set("showSearchSuggestionsFirst", false); 639 await QuickSuggestTestUtils.forceSync(); 640 641 let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); 642 await check_results({ 643 context, 644 matches: [ 645 makeSearchResult(context, { 646 heuristic: true, 647 query: SPONSORED_SEARCH_STRING, 648 engineName: Services.search.defaultEngine.name, 649 }), 650 QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 }), 651 makeSearchResult(context, { 652 query: SPONSORED_SEARCH_STRING, 653 suggestion: SPONSORED_SEARCH_STRING + " foo", 654 engineName: Services.search.defaultEngine.name, 655 }), 656 makeSearchResult(context, { 657 query: SPONSORED_SEARCH_STRING, 658 suggestion: SPONSORED_SEARCH_STRING + " bar", 659 engineName: Services.search.defaultEngine.name, 660 }), 661 ], 662 }); 663 664 UrlbarPrefs.clear("browser.search.suggest.enabled"); 665 UrlbarPrefs.clear("suggest.searches"); 666 UrlbarPrefs.clear("showSearchSuggestionsFirst"); 667 }); 668 669 // When general results come before search suggestions and there are other 670 // general results besides quick suggest, the quick suggest result should be the 671 // last general result. 672 add_task(async function generalBeforeSuggestions_others() { 673 UrlbarPrefs.set("suggest.quicksuggest.all", true); 674 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 675 UrlbarPrefs.set("browser.search.suggest.enabled", true); 676 UrlbarPrefs.set("suggest.searches", true); 677 UrlbarPrefs.set("showSearchSuggestionsFirst", false); 678 await QuickSuggestTestUtils.forceSync(); 679 680 let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); 681 682 // Add some history that will match our query below. 683 let maxResults = UrlbarPrefs.get("maxRichResults"); 684 let historyResults = []; 685 for (let i = 0; i < maxResults; i++) { 686 let url = "http://example.com/" + SPONSORED_SEARCH_STRING + i; 687 historyResults.push( 688 makeVisitResult(context, { 689 uri: url, 690 title: "test visit for " + url, 691 }) 692 ); 693 await PlacesTestUtils.addVisits(url); 694 } 695 historyResults = historyResults.reverse().slice(0, historyResults.length - 4); 696 697 await check_results({ 698 context, 699 matches: [ 700 makeSearchResult(context, { 701 heuristic: true, 702 query: SPONSORED_SEARCH_STRING, 703 engineName: Services.search.defaultEngine.name, 704 }), 705 ...historyResults, 706 QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 }), 707 makeSearchResult(context, { 708 query: SPONSORED_SEARCH_STRING, 709 suggestion: SPONSORED_SEARCH_STRING + " foo", 710 engineName: Services.search.defaultEngine.name, 711 }), 712 makeSearchResult(context, { 713 query: SPONSORED_SEARCH_STRING, 714 suggestion: SPONSORED_SEARCH_STRING + " bar", 715 engineName: Services.search.defaultEngine.name, 716 }), 717 ], 718 }); 719 720 UrlbarPrefs.clear("browser.search.suggest.enabled"); 721 UrlbarPrefs.clear("suggest.searches"); 722 UrlbarPrefs.clear("showSearchSuggestionsFirst"); 723 await PlacesUtils.history.clear(); 724 }); 725 726 // The provider should not add more than `queryContext.maxResults` results. 727 add_task(async function maxResults() { 728 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 729 await QuickSuggestTestUtils.forceSync(); 730 731 let searchString = "maxresults"; 732 let suggestions = await QuickSuggest.rustBackend.query(searchString); 733 Assert.equal( 734 suggestions.length, 735 gMaxResultsSuggestionsCount, 736 "The backend should return all matching suggestions" 737 ); 738 739 let context = createContext(searchString, { 740 providers: [UrlbarProviderQuickSuggest.name], 741 isPrivate: false, 742 }); 743 744 // Spy on `muxer.sort()` so we can verify the provider limited the number of 745 // results it added to the query. 746 let muxerName = context.muxer || "UnifiedComplete"; 747 let muxer = UrlbarProvidersManager.muxers.get(muxerName); 748 Assert.ok(!!muxer, "Muxer should exist"); 749 750 let sandbox = sinon.createSandbox(); 751 let spy = sandbox.spy(muxer, "sort"); 752 753 // Use `check_results()` to do the query. 754 await check_results({ 755 context, 756 matches: [ 757 QuickSuggestTestUtils.ampResult({ 758 keyword: "maxresults", 759 title: "maxresults 0", 760 url: "https://example.com/maxresults/0", 761 suggestedIndex: -1, 762 }), 763 ], 764 }); 765 766 // Check the `sort()` calls. 767 let calls = spy.getCalls(); 768 Assert.greater( 769 calls.length, 770 0, 771 "muxer.sort() should have been called at least once" 772 ); 773 774 for (let c of calls) { 775 let unsortedResults = c.args[1]; 776 Assert.lessOrEqual( 777 unsortedResults.length, 778 UrlbarPrefs.get("maxRichResults"), 779 "Provider should have added no more than maxRichResults results" 780 ); 781 } 782 783 sandbox.restore(); 784 }); 785 786 // When the Suggest provider adds more than one result and they are not hidden 787 // exposures, the muxer should add the first one to the final results list and 788 // discard the rest, and the discarded results should not prevent the muxer from 789 // adding other non-Suggest results. 790 add_task(async function manySuggestResults_visible() { 791 await doManySuggestResultsTest({ 792 expectedSuggestResults: [ 793 QuickSuggestTestUtils.ampResult({ 794 keyword: "maxresults", 795 title: "maxresults 0", 796 url: "https://example.com/maxresults/0", 797 suggestedIndex: -1, 798 }), 799 ], 800 expectedOtherResultsCount: UrlbarPrefs.get("maxRichResults") - 1, 801 }); 802 }); 803 804 // When the Suggest provider adds more than one result and they are hidden 805 // exposures, the muxer should add up to `queryContext.maxResults` of them to 806 // the final results list, and they should not prevent the muxer from adding 807 // other non-Suggest results. 808 add_task(async function manySuggestResults_hiddenExposures() { 809 UrlbarPrefs.set("exposureResults", "rust_adm_sponsored"); 810 UrlbarPrefs.set("showExposureResults", false); 811 812 // Build the list of expected Suggest results. 813 let results = []; 814 let maxResults = UrlbarPrefs.get("maxRichResults"); 815 let suggestResultsCount = Math.min(gMaxResultsSuggestionsCount, maxResults); 816 for (let i = 0; i < suggestResultsCount; i++) { 817 let index = maxResults - 1 - i; 818 results.push({ 819 ...QuickSuggestTestUtils.ampResult({ 820 keyword: "maxresults", 821 title: "maxresults " + index, 822 url: "https://example.com/maxresults/" + index, 823 suggestedIndex: -1, 824 }), 825 exposureTelemetry: UrlbarUtils.EXPOSURE_TELEMETRY.HIDDEN, 826 }); 827 } 828 829 await doManySuggestResultsTest({ 830 expectedSuggestResults: results, 831 expectedOtherResultsCount: maxResults, 832 }); 833 834 UrlbarPrefs.clear("exposureResults"); 835 UrlbarPrefs.clear("showExposureResults"); 836 }); 837 838 async function doManySuggestResultsTest({ 839 expectedSuggestResults, 840 expectedOtherResultsCount, 841 }) { 842 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 843 await QuickSuggestTestUtils.forceSync(); 844 845 // Make sure many Suggest suggestions match the search string. 846 let searchString = "maxresults"; 847 let suggestions = await QuickSuggest.rustBackend.query(searchString); 848 Assert.equal( 849 suggestions.length, 850 gMaxResultsSuggestionsCount, 851 "Sanity check: The backend should return all matching suggestions" 852 ); 853 Assert.greater( 854 suggestions.length, 855 1, 856 "Sanity check: There should be more than 1 matching suggestion" 857 ); 858 859 // Register a test provider that adds a bunch of history results. 860 let otherResults = []; 861 let maxResults = UrlbarPrefs.get("maxRichResults"); 862 for (let i = 0; i < maxResults; i++) { 863 otherResults.push( 864 new UrlbarResult({ 865 type: UrlbarUtils.RESULT_TYPE.URL, 866 source: UrlbarUtils.RESULT_SOURCE.HISTORY, 867 payload: { url: "http://example.com/history/" + i }, 868 }) 869 ); 870 } 871 872 let provider = new UrlbarTestUtils.TestProvider({ results: otherResults }); 873 UrlbarProvidersManager.registerProvider(provider); 874 875 // Do a search that matches all the Suggest suggestions and the test 876 // provider's results. The Suggest suggestion(s) should be first since its 877 // `suggestedIndex` is 0. 878 await check_results({ 879 context: createContext(searchString, { 880 providers: [UrlbarProviderQuickSuggest.name, provider.name], 881 isPrivate: false, 882 }), 883 matches: [ 884 ...otherResults.slice(0, expectedOtherResultsCount), 885 ...expectedSuggestResults, 886 ], 887 }); 888 889 UrlbarProvidersManager.unregisterProvider(provider); 890 } 891 892 add_task(async function dedupeAgainstURL_samePrefix() { 893 await doDedupeAgainstURLTest({ 894 searchString: HTTP_SEARCH_STRING, 895 expectedQuickSuggestResult: expectedHttpResult(), 896 otherPrefix: "http://", 897 expectOther: false, 898 }); 899 }); 900 901 add_task(async function dedupeAgainstURL_higherPrefix() { 902 await doDedupeAgainstURLTest({ 903 searchString: HTTPS_SEARCH_STRING, 904 expectedQuickSuggestResult: expectedHttpsResult(), 905 otherPrefix: "http://", 906 expectOther: false, 907 }); 908 }); 909 910 add_task(async function dedupeAgainstURL_lowerPrefix() { 911 await doDedupeAgainstURLTest({ 912 searchString: HTTP_SEARCH_STRING, 913 expectedQuickSuggestResult: expectedHttpResult(), 914 otherPrefix: "https://", 915 expectOther: true, 916 }); 917 }); 918 919 /** 920 * Tests how the muxer dedupes URL results against quick suggest results. 921 * Depending on prefix rank, quick suggest results should be preferred over 922 * other URL results with the same stripped URL: Other results should be 923 * discarded when their prefix rank is lower than the prefix rank of the quick 924 * suggest. They should not be discarded when their prefix rank is higher, and 925 * in that case both results should be included. 926 * 927 * This function adds a visit to the URL formed by the given `otherPrefix` and 928 * `PREFIX_SUGGESTIONS_STRIPPED_URL`. The visit's title will be set to the given 929 * `searchString` so that both the visit and the quick suggest will match it. 930 * 931 * @param {object} options 932 * Options object. 933 * @param {string} options.searchString 934 * The search string that should trigger one of the mock prefix-test quick 935 * suggest results. 936 * @param {object} options.expectedQuickSuggestResult 937 * The expected quick suggest result. 938 * @param {string} options.otherPrefix 939 * The visit will be created with a URL with this prefix, e.g., "http://". 940 * @param {boolean} options.expectOther 941 * Whether the visit result should appear in the final results. 942 */ 943 async function doDedupeAgainstURLTest({ 944 searchString, 945 expectedQuickSuggestResult, 946 otherPrefix, 947 expectOther, 948 }) { 949 // Disable search suggestions. This means the expected suggestedIndex for 950 // sponsored suggestions will now be -1. We assume expectedQuickSuggestResult 951 // is sponsored, so set its suggestedIndex now. 952 UrlbarPrefs.set("suggest.searches", false); 953 expectedQuickSuggestResult.suggestedIndex = -1; 954 955 // Add a visit that will match our query below. 956 let otherURL = otherPrefix + PREFIX_SUGGESTIONS_STRIPPED_URL; 957 await PlacesTestUtils.addVisits({ uri: otherURL, title: searchString }); 958 959 // First, do a search with quick suggest disabled to make sure the search 960 // string matches the visit. 961 info("Doing first query"); 962 UrlbarPrefs.set("suggest.quicksuggest.all", false); 963 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 964 let context = createContext(searchString, { isPrivate: false }); 965 await check_results({ 966 context, 967 matches: [ 968 makeSearchResult(context, { 969 heuristic: true, 970 query: searchString, 971 engineName: Services.search.defaultEngine.name, 972 }), 973 makeVisitResult(context, { 974 uri: otherURL, 975 title: searchString, 976 }), 977 ], 978 }); 979 980 // Now do another search with quick suggest enabled. 981 UrlbarPrefs.set("suggest.quicksuggest.all", true); 982 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 983 await QuickSuggestTestUtils.forceSync(); 984 985 context = createContext(searchString, { isPrivate: false }); 986 987 let expectedResults = [ 988 makeSearchResult(context, { 989 heuristic: true, 990 query: searchString, 991 engineName: Services.search.defaultEngine.name, 992 }), 993 ]; 994 995 if (expectOther) { 996 expectedResults.push( 997 makeVisitResult(context, { 998 uri: otherURL, 999 title: searchString, 1000 }) 1001 ); 1002 } 1003 1004 // The expected result is last since its expected suggestedIndex is -1. 1005 expectedResults.push(expectedQuickSuggestResult); 1006 1007 info("Doing second query"); 1008 await check_results({ context, matches: expectedResults }); 1009 1010 UrlbarPrefs.clear("suggest.quicksuggest.all"); 1011 UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); 1012 await QuickSuggestTestUtils.forceSync(); 1013 1014 UrlbarPrefs.clear("suggest.searches"); 1015 await PlacesUtils.history.clear(); 1016 } 1017 1018 // Timestamp templates in URLs should be replaced with real timestamps. 1019 add_task(async function timestamps() { 1020 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1021 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1022 await QuickSuggestTestUtils.forceSync(); 1023 1024 // Do a search. 1025 let context = createContext(TIMESTAMP_SEARCH_STRING, { 1026 providers: [UrlbarProviderQuickSuggest.name], 1027 isPrivate: false, 1028 }); 1029 let controller = UrlbarTestUtils.newMockController(); 1030 await controller.startQuery(context); 1031 1032 // Should be one quick suggest result. 1033 Assert.equal(context.results.length, 1, "One result returned"); 1034 let result = context.results[0]; 1035 1036 QuickSuggestTestUtils.assertTimestampsReplaced(result, { 1037 url: TIMESTAMP_SUGGESTION_URL, 1038 sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL, 1039 }); 1040 }); 1041 1042 // Real quick suggest URLs include a timestamp template that 1043 // UrlbarProviderQuickSuggest fills in when it fetches suggestions. When the 1044 // user picks a quick suggest, its URL with its particular timestamp is added to 1045 // history. If the user triggers the quick suggest again later, its new 1046 // timestamp may be different from the one in the user's history. In that case, 1047 // the two URLs should be treated as dupes and only the quick suggest should be 1048 // shown, not the URL from history. 1049 add_task(async function dedupeAgainstURL_timestamps() { 1050 // Disable search suggestions. This means the expected suggestedIndex for 1051 // sponsored suggestions will now be -1. 1052 UrlbarPrefs.set("suggest.searches", false); 1053 1054 // Add a visit that will match the query below and dupe the quick suggest. 1055 let dupeURL = TIMESTAMP_SUGGESTION_URL.replace( 1056 TIMESTAMP_TEMPLATE, 1057 "2013051113" 1058 ); 1059 1060 // Add other visits that will match the query and almost dupe the quick 1061 // suggest but not quite because they have invalid timestamps. 1062 let badTimestamps = [ 1063 // not numeric digits 1064 "x".repeat(TIMESTAMP_LENGTH), 1065 // too few digits 1066 "5".repeat(TIMESTAMP_LENGTH - 1), 1067 // empty string, too few digits 1068 "", 1069 ]; 1070 let badTimestampURLs = badTimestamps.map(str => 1071 TIMESTAMP_SUGGESTION_URL.replace(TIMESTAMP_TEMPLATE, str) 1072 ); 1073 1074 await PlacesTestUtils.addVisits( 1075 [dupeURL, ...badTimestampURLs].map(uri => ({ 1076 uri, 1077 title: TIMESTAMP_SEARCH_STRING, 1078 })) 1079 ); 1080 1081 // First, do a search with quick suggest disabled to make sure the search 1082 // string matches all the other URLs. 1083 info("Doing first query"); 1084 UrlbarPrefs.set("suggest.quicksuggest.all", false); 1085 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 1086 let context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false }); 1087 1088 let expectedHeuristic = makeSearchResult(context, { 1089 heuristic: true, 1090 query: TIMESTAMP_SEARCH_STRING, 1091 engineName: Services.search.defaultEngine.name, 1092 }); 1093 let expectedDupeResult = makeVisitResult(context, { 1094 uri: dupeURL, 1095 title: TIMESTAMP_SEARCH_STRING, 1096 }); 1097 let expectedBadTimestampResults = [...badTimestampURLs].reverse().map(uri => 1098 makeVisitResult(context, { 1099 uri, 1100 title: TIMESTAMP_SEARCH_STRING, 1101 }) 1102 ); 1103 1104 await check_results({ 1105 context, 1106 matches: [ 1107 expectedHeuristic, 1108 ...expectedBadTimestampResults, 1109 expectedDupeResult, 1110 ], 1111 }); 1112 1113 // Now do another search with quick suggest enabled. 1114 info("Doing second query"); 1115 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1116 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1117 await QuickSuggestTestUtils.forceSync(); 1118 context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false }); 1119 1120 let expectedQuickSuggest = QuickSuggestTestUtils.ampResult({ 1121 originalUrl: TIMESTAMP_SUGGESTION_URL, 1122 keyword: TIMESTAMP_SEARCH_STRING, 1123 title: "Timestamp suggestion", 1124 impressionUrl: "http://impression.reporting.test.com/timestamp", 1125 blockId: 5, 1126 advertiser: "TestAdvertiserTimestamp", 1127 iabCategory: "22 - Shopping", 1128 // suggestedIndex is -1 since search suggestions are disabled. 1129 suggestedIndex: -1, 1130 }); 1131 1132 let expectedResults = [expectedHeuristic, ...expectedBadTimestampResults]; 1133 1134 const QUICK_SUGGEST_INDEX = expectedResults.length; 1135 expectedResults.push(expectedQuickSuggest); 1136 1137 let controller = UrlbarTestUtils.newMockController(); 1138 await controller.startQuery(context); 1139 info("Actual results: " + JSON.stringify(context.results)); 1140 1141 Assert.equal( 1142 context.results.length, 1143 expectedResults.length, 1144 "Found the expected number of results" 1145 ); 1146 1147 function getPayload(result, { ignore = [] } = {}) { 1148 ignore.push("suggestionObject"); 1149 let payload = {}; 1150 for (let [key, value] of Object.entries(result.payload)) { 1151 if (value !== undefined && !ignore.includes(key)) { 1152 payload[key] = value; 1153 } 1154 } 1155 return payload; 1156 } 1157 1158 // Check actual vs. expected result properties. 1159 for (let i = 0; i < expectedResults.length; i++) { 1160 let actual = context.results[i]; 1161 let expected = expectedResults[i]; 1162 info( 1163 `Comparing results at index ${i}:` + 1164 " actual=" + 1165 JSON.stringify(actual) + 1166 " expected=" + 1167 JSON.stringify(expected) 1168 ); 1169 Assert.equal( 1170 actual.type, 1171 expected.type, 1172 `result.type at result index ${i}` 1173 ); 1174 Assert.equal( 1175 actual.source, 1176 expected.source, 1177 `result.source at result index ${i}` 1178 ); 1179 Assert.equal( 1180 actual.heuristic, 1181 expected.heuristic, 1182 `result.heuristic at result index ${i}` 1183 ); 1184 1185 // Check payloads except for the quick suggest. 1186 if (i != QUICK_SUGGEST_INDEX) { 1187 Assert.deepEqual( 1188 getPayload(context.results[i], { ignore: ["frecency", "lastVisit"] }), 1189 getPayload(expectedResults[i], { ignore: ["frecency", "lastVisit"] }), 1190 "Payload at index " + i 1191 ); 1192 } 1193 } 1194 1195 // Check the quick suggest's payload excluding the timestamp-related 1196 // properties. 1197 let actualQuickSuggest = context.results[QUICK_SUGGEST_INDEX]; 1198 let ignore = ["sponsoredClickUrl", "url", "urlTimestampIndex"]; 1199 Assert.deepEqual( 1200 getPayload(actualQuickSuggest, { ignore }), 1201 getPayload(expectedQuickSuggest, { ignore }), 1202 "Quick suggest payload excluding timestamp-related keys" 1203 ); 1204 1205 // Now check the timestamps in the payload. 1206 QuickSuggestTestUtils.assertTimestampsReplaced(actualQuickSuggest, { 1207 url: TIMESTAMP_SUGGESTION_URL, 1208 sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL, 1209 }); 1210 1211 // Clean up. 1212 UrlbarPrefs.clear("suggest.quicksuggest.all"); 1213 UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); 1214 await QuickSuggestTestUtils.forceSync(); 1215 1216 UrlbarPrefs.clear("suggest.searches"); 1217 await PlacesUtils.history.clear(); 1218 }); 1219 1220 // Tests `UrlbarResult` dismissal. 1221 add_task(async function dismissResult() { 1222 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1223 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1224 await QuickSuggestTestUtils.forceSync(); 1225 1226 let tests = [ 1227 // [suggestion, expected result] 1228 [ 1229 REMOTE_SETTINGS_RESULTS[0], 1230 QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 }), 1231 ], 1232 [REMOTE_SETTINGS_RESULTS[1], QuickSuggestTestUtils.wikipediaResult()], 1233 [REMOTE_SETTINGS_RESULTS[2], expectedHttpResult()], 1234 [REMOTE_SETTINGS_RESULTS[3], expectedHttpsResult()], 1235 ]; 1236 1237 for (let [suggestion, expectedResult] of tests) { 1238 info("Testing suggestion: " + JSON.stringify(suggestion)); 1239 1240 // Do a search to get a real `UrlbarResult` created for the suggestion. 1241 let context = createContext(suggestion.keywords[0], { 1242 providers: [UrlbarProviderQuickSuggest.name], 1243 isPrivate: false, 1244 }); 1245 await check_results({ 1246 context, 1247 matches: [expectedResult], 1248 }); 1249 1250 // Dismiss it. 1251 await QuickSuggest.dismissResult(context.results[0]); 1252 Assert.ok( 1253 await QuickSuggest.isResultDismissed(context.results[0]), 1254 "isResultDismissed should return true" 1255 ); 1256 Assert.ok( 1257 await QuickSuggest.canClearDismissedSuggestions(), 1258 "canClearDismissedSuggestions should return true" 1259 ); 1260 1261 // Do another search. The result shouldn't be added. 1262 await check_results({ 1263 context: createContext(suggestion.keywords[0], { 1264 providers: [UrlbarProviderQuickSuggest.name], 1265 isPrivate: false, 1266 }), 1267 matches: [], 1268 }); 1269 1270 await QuickSuggest.clearDismissedSuggestions(); 1271 Assert.ok( 1272 !(await QuickSuggest.isResultDismissed(context.results[0])), 1273 "isResultDismissed should return false" 1274 ); 1275 Assert.ok( 1276 !(await QuickSuggest.canClearDismissedSuggestions()), 1277 "canClearDismissedSuggestions should return false" 1278 ); 1279 } 1280 }); 1281 1282 // Tests dismissing a `UrlbarResult` whose URL has a timestamp template. 1283 add_task(async function dismissResultWithTimestamp() { 1284 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1285 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1286 await QuickSuggestTestUtils.forceSync(); 1287 1288 // Do a search. 1289 let context = createContext(TIMESTAMP_SEARCH_STRING, { 1290 providers: [UrlbarProviderQuickSuggest.name], 1291 isPrivate: false, 1292 }); 1293 let controller = UrlbarTestUtils.newMockController(); 1294 await controller.startQuery(context); 1295 1296 // Should be one quick suggest result. 1297 Assert.equal(context.results.length, 1, "One result returned"); 1298 let result = context.results[0]; 1299 1300 QuickSuggestTestUtils.assertTimestampsReplaced(result, { 1301 url: TIMESTAMP_SUGGESTION_URL, 1302 sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL, 1303 }); 1304 1305 Assert.ok(result.payload.originalUrl, "The actual result has an originalUrl"); 1306 Assert.equal( 1307 result.payload.originalUrl, 1308 REMOTE_SETTINGS_RESULTS[4].url, 1309 "The actual result's originalUrl should be the raw suggestion URL with a timestamp template" 1310 ); 1311 1312 // Dismiss the result. 1313 await QuickSuggest.dismissResult(result); 1314 Assert.ok( 1315 await QuickSuggest.isResultDismissed(result), 1316 "isResultDismissed should return true" 1317 ); 1318 Assert.ok( 1319 await QuickSuggest.canClearDismissedSuggestions(), 1320 "canClearDismissedSuggestions should return true" 1321 ); 1322 1323 // Do another search. The result shouldn't be added. 1324 await check_results({ 1325 context: createContext(TIMESTAMP_SEARCH_STRING, { 1326 providers: [UrlbarProviderQuickSuggest.name], 1327 isPrivate: false, 1328 }), 1329 matches: [], 1330 }); 1331 1332 await QuickSuggest.clearDismissedSuggestions(); 1333 Assert.ok( 1334 !(await QuickSuggest.isResultDismissed(context.results[0])), 1335 "isResultDismissed should return false" 1336 ); 1337 Assert.ok( 1338 !(await QuickSuggest.canClearDismissedSuggestions()), 1339 "canClearDismissedSuggestions should return false" 1340 ); 1341 }); 1342 1343 add_task(async function sponsoredPriority_normal() { 1344 await doSponsoredPriorityTest({ 1345 searchWord: SPONSORED_SEARCH_STRING, 1346 remoteSettingsData: [REMOTE_SETTINGS_RESULTS[0]], 1347 expectedMatches: [expectedSponsoredPriorityResult()], 1348 }); 1349 }); 1350 1351 add_task(async function sponsoredPriority_nonsponsoredSuggestion() { 1352 // Not affect to except sponsored suggestion. 1353 await doSponsoredPriorityTest({ 1354 searchWord: NONSPONSORED_SEARCH_STRING, 1355 remoteSettingsData: [REMOTE_SETTINGS_RESULTS[1]], 1356 expectedMatches: [QuickSuggestTestUtils.wikipediaResult()], 1357 }); 1358 }); 1359 1360 add_task(async function sponsoredPriority_sponsoredIndex() { 1361 await doSponsoredPriorityTest({ 1362 nimbusSettings: { quickSuggestSponsoredIndex: 2 }, 1363 searchWord: SPONSORED_SEARCH_STRING, 1364 remoteSettingsData: [REMOTE_SETTINGS_RESULTS[0]], 1365 expectedMatches: [expectedSponsoredPriorityResult()], 1366 }); 1367 }); 1368 1369 async function doSponsoredPriorityTest({ 1370 remoteSettingsConfig = {}, 1371 nimbusSettings = {}, 1372 searchWord, 1373 remoteSettingsData, 1374 expectedMatches, 1375 }) { 1376 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1377 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1378 await QuickSuggestTestUtils.forceSync(); 1379 1380 const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({ 1381 ...nimbusSettings, 1382 quickSuggestSponsoredPriority: true, 1383 }); 1384 1385 await resetRemoteSettingsData(remoteSettingsData); 1386 await QuickSuggestTestUtils.setConfig(remoteSettingsConfig); 1387 1388 await check_results({ 1389 context: createContext(searchWord, { 1390 providers: [UrlbarProviderQuickSuggest.name], 1391 isPrivate: false, 1392 }), 1393 matches: expectedMatches, 1394 }); 1395 1396 await cleanUpNimbusEnable(); 1397 await resetRemoteSettingsData(); 1398 await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG); 1399 } 1400 1401 // When a Suggest best match and a tab-to-search (TTS) are shown in the same 1402 // search, both will have a `suggestedIndex` value of 1. The TTS should appear 1403 // first. 1404 add_task(async function tabToSearch() { 1405 // We'll use a sponsored priority result as the best match result. Different 1406 // types of Suggest results can appear as best matches, and they all should 1407 // have the same behavior. 1408 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1409 await QuickSuggestTestUtils.forceSync(); 1410 UrlbarPrefs.set("suggest.quickactions", false); 1411 1412 Services.prefs.setBoolPref( 1413 "browser.urlbar.quicksuggest.sponsoredPriority", 1414 true 1415 ); 1416 1417 // Disable tab-to-search onboarding results so we get a regular TTS result, 1418 // which we can test a little more easily with `makeSearchResult()`. 1419 UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 0); 1420 1421 // Disable search suggestions so we don't need to expect them below. 1422 Services.prefs.setBoolPref("browser.search.suggest.enabled", false); 1423 1424 // Install a test engine. The main part of its domain name needs to match the 1425 // best match result too so we can trigger both its TTS and the best match. 1426 let engineURL = `https://foo.${SPONSORED_SEARCH_STRING}.com/`; 1427 let extension = await SearchTestUtils.installSearchExtension( 1428 { 1429 name: "Test", 1430 search_url: engineURL, 1431 }, 1432 { skipUnload: true } 1433 ); 1434 let engine = Services.search.getEngineByName("Test"); 1435 1436 // Also need to add a visit to trigger TTS. 1437 await PlacesTestUtils.addVisits(engineURL); 1438 1439 let context = createContext(SPONSORED_SEARCH_STRING, { 1440 isPrivate: false, 1441 }); 1442 await check_results({ 1443 context, 1444 matches: [ 1445 // search heuristic 1446 makeSearchResult(context, { 1447 engineName: Services.search.defaultEngine.name, 1448 engineIconUri: await Services.search.defaultEngine.getIconURL(), 1449 heuristic: true, 1450 }), 1451 // tab to search 1452 makeSearchResult(context, { 1453 engineName: engine.name, 1454 engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, 1455 searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost( 1456 engine.searchUrlDomain 1457 ), 1458 providesSearchMode: true, 1459 query: "", 1460 providerName: "UrlbarProviderTabToSearch", 1461 satisfiesAutofillThreshold: true, 1462 }), 1463 // Suggest best match 1464 expectedSponsoredPriorityResult(), 1465 // visit 1466 makeVisitResult(context, { 1467 uri: engineURL, 1468 title: `test visit for ${engineURL}`, 1469 }), 1470 ], 1471 }); 1472 1473 await cleanupPlaces(); 1474 await extension.unload(); 1475 1476 UrlbarPrefs.clear("tabToSearch.onboard.interactionsLeft"); 1477 UrlbarPrefs.clear("suggest.quickactions"); 1478 Services.prefs.clearUserPref("browser.search.suggest.enabled"); 1479 Services.prefs.clearUserPref("browser.urlbar.quicksuggest.sponsoredPriority"); 1480 }); 1481 1482 // When a Suggest best match and a global action are shown in the same 1483 // search, both will have a `suggestedIndex` value of 1. The global action should 1484 // appear first. 1485 add_task(async function globalAction() { 1486 // We'll use a sponsored priority result as the best match result. Different 1487 // types of Suggest results can appear as best matches, and they all should 1488 // have the same behavior. 1489 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1490 await QuickSuggestTestUtils.forceSync(); 1491 1492 Services.prefs.setBoolPref( 1493 "browser.urlbar.quicksuggest.sponsoredPriority", 1494 true 1495 ); 1496 1497 // Disable search suggestions so we don't need to expect them below. 1498 Services.prefs.setBoolPref("browser.search.suggest.enabled", false); 1499 1500 // Set prefs to prevent quick actions onboarding label from showing. 1501 UrlbarPrefs.set("quickactions.timesToShowOnboardingLabel", 3); 1502 UrlbarPrefs.set("quickactions.timesShownOnboardingLabel", 3); 1503 1504 let engineURL = `https://example.com/`; 1505 let extension = await SearchTestUtils.installSearchExtension( 1506 { 1507 name: "Amp", 1508 search_url: engineURL, 1509 }, 1510 { skipUnload: true } 1511 ); 1512 1513 await PlacesTestUtils.addVisits(engineURL); 1514 1515 let context = createContext(SPONSORED_SEARCH_STRING, { 1516 isPrivate: false, 1517 }); 1518 1519 await check_results({ 1520 context, 1521 matches: [ 1522 // search heuristic 1523 makeSearchResult(context, { 1524 engineName: Services.search.defaultEngine.name, 1525 engineIconUri: await Services.search.defaultEngine.getIconURL(), 1526 heuristic: true, 1527 }), 1528 1529 // "Search with engine" global action. 1530 makeGlobalActionsResult({ 1531 actionsResults: [ 1532 { 1533 providerName: "ActionsProviderContextualSearch", 1534 }, 1535 ], 1536 query: "", 1537 input: "", 1538 inputLength: context.searchString.length, 1539 showOnboardingLabel: false, 1540 }), 1541 1542 // Suggest best match 1543 expectedSponsoredPriorityResult(), 1544 1545 // visit 1546 makeVisitResult(context, { 1547 uri: engineURL, 1548 title: `test visit for ${engineURL}`, 1549 }), 1550 ], 1551 }); 1552 1553 await cleanupPlaces(); 1554 await extension.unload(); 1555 1556 UrlbarPrefs.clear("quickactions.timesToShowOnboardingLabel"); 1557 UrlbarPrefs.clear("quickactions.timesShownOnboardingLabel"); 1558 Services.prefs.clearUserPref("browser.search.suggest.enabled"); 1559 Services.prefs.clearUserPref("browser.urlbar.quicksuggest.sponsoredPriority"); 1560 }); 1561 1562 // The `Amp` and `Wikipedia` Rust providers should be passed to the Rust 1563 // component when querying depending on whether sponsored and non-sponsored 1564 // suggestions are enabled. 1565 add_task(async function rustProviders() { 1566 await doRustProvidersTests({ 1567 searchString: SPONSORED_AND_NONSPONSORED_SEARCH_STRING, 1568 tests: [ 1569 { 1570 prefs: { 1571 "suggest.quicksuggest.all": true, 1572 "suggest.quicksuggest.sponsored": true, 1573 }, 1574 expectedUrls: [ 1575 "https://example.com/amp", 1576 "https://example.com/wikipedia", 1577 ], 1578 }, 1579 { 1580 prefs: { 1581 "suggest.quicksuggest.all": true, 1582 "suggest.quicksuggest.sponsored": false, 1583 }, 1584 expectedUrls: ["https://example.com/wikipedia"], 1585 }, 1586 { 1587 prefs: { 1588 "suggest.quicksuggest.all": false, 1589 "suggest.quicksuggest.sponsored": true, 1590 }, 1591 expectedUrls: [], 1592 }, 1593 { 1594 prefs: { 1595 "suggest.quicksuggest.all": false, 1596 "suggest.quicksuggest.sponsored": false, 1597 }, 1598 expectedUrls: [], 1599 }, 1600 ], 1601 }); 1602 }); 1603 1604 // Tests the keyword/search-string-length threshold. Keywords/search strings 1605 // must be at least two characters long to be matched. 1606 add_task(async function keywordLengthThreshold() { 1607 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1608 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1609 await QuickSuggestTestUtils.forceSync(); 1610 1611 let tests = [ 1612 ...ONE_CHAR_SEARCH_STRINGS.map(keyword => ({ keyword, expected: false })), 1613 { keyword: "12", expected: true }, 1614 { keyword: "a longer keyword", expected: true }, 1615 ]; 1616 1617 for (let { keyword, expected } of tests) { 1618 await check_results({ 1619 context: createContext(keyword, { 1620 providers: [UrlbarProviderQuickSuggest.name], 1621 isPrivate: false, 1622 }), 1623 matches: !expected 1624 ? [] 1625 : [ 1626 QuickSuggestTestUtils.ampResult({ 1627 keyword, 1628 title: "Suggestion with 1-char keyword", 1629 url: "http://example.com/1-char-keyword", 1630 originalUrl: "http://example.com/1-char-keyword", 1631 suggestedIndex: -1, 1632 }), 1633 ], 1634 }); 1635 } 1636 }); 1637 1638 // AMP should be a top pick when `quicksuggest.ampTopPickCharThreshold` is 1639 // non-zero and the query length meets the threshold; otherwise it should not be 1640 // a top pick. It shouldn't matter whether the query is one of the suggestion's 1641 // full keywords. 1642 add_task(async function ampTopPickCharThreshold() { 1643 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1644 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1645 await QuickSuggestTestUtils.forceSync(); 1646 1647 UrlbarPrefs.set( 1648 "quicksuggest.ampTopPickCharThreshold", 1649 "amp full keywo".length 1650 ); 1651 1652 let tests = [ 1653 // No top pick: Matches an AMP suggestion but the query is shorter than the 1654 // threshold. 1655 { keyword: "amp full key", amp: true, isTopPick: false }, 1656 { keyword: "amp full keyw", amp: true, isTopPick: false }, 1657 { keyword: " amp full key", amp: true, isTopPick: false }, 1658 { keyword: " amp full keyw", amp: true, isTopPick: false }, 1659 1660 // Top pick: Matches an AMP suggestion and the query meets the threshold. 1661 { keyword: "amp full keywo", amp: true, isTopPick: true }, 1662 { keyword: "amp full keywor", amp: true, isTopPick: true }, 1663 { keyword: "amp full keyword", amp: true, isTopPick: true }, 1664 { keyword: "AmP FuLl KeYwOrD", amp: true, isTopPick: true }, 1665 { keyword: " amp full keywo", amp: true, isTopPick: true }, 1666 { keyword: " amp full keywor", amp: true, isTopPick: true }, 1667 { keyword: " amp full keyword", amp: true, isTopPick: true }, 1668 { keyword: " AmP FuLl KeYwOrD", amp: true, isTopPick: true }, 1669 1670 // No top pick: Matches an AMP suggestion but the query is shorter than the 1671 // threshold. It doesn't matter that the query is equal to the suggestion's 1672 // full keyword. 1673 { keyword: "xyz", fullKeyword: "xyz", amp: true, isTopPick: false }, 1674 { keyword: "XyZ", fullKeyword: "xyz", amp: true, isTopPick: false }, 1675 { 1676 keyword: " xyz", 1677 fullKeyword: "xyz", 1678 amp: true, 1679 isTopPick: false, 1680 }, 1681 { 1682 keyword: " XyZ", 1683 fullKeyword: "xyz", 1684 amp: true, 1685 isTopPick: false, 1686 }, 1687 1688 // No top pick: Matches a Wikipedia suggestion and some queries meet the 1689 // threshold, but Wikipedia should not be top pick. 1690 { keyword: "wikipedia full key", isTopPick: false }, 1691 { keyword: "wikipedia full keyw", isTopPick: false }, 1692 { keyword: "wikipedia full keywo", isTopPick: false }, 1693 { keyword: "wikipedia full keywor", isTopPick: false }, 1694 { keyword: "wikipedia full keyword", isTopPick: false }, 1695 1696 // No match: These shouldn't match anything at all since they have extra 1697 // spaces at the end, but they're included for completeness. 1698 { keyword: " amp full key ", noMatch: true }, 1699 { keyword: " amp full keyw ", noMatch: true }, 1700 { keyword: " amp full keywo ", noMatch: true }, 1701 { keyword: " amp full keywor ", noMatch: true }, 1702 { keyword: " amp full keyword ", noMatch: true }, 1703 { keyword: " AmP FuLl KeYwOrD ", noMatch: true }, 1704 { keyword: " xyz ", noMatch: true }, 1705 { keyword: " XyZ ", noMatch: true }, 1706 ]; 1707 1708 for (let { keyword, fullKeyword, amp, isTopPick, noMatch } of tests) { 1709 fullKeyword ??= amp ? "amp full keyword" : "wikipedia full keyword"; 1710 info( 1711 "Running subtest: " + 1712 JSON.stringify({ keyword, fullKeyword, amp, isTopPick }) 1713 ); 1714 1715 let expectedResult; 1716 if (!noMatch) { 1717 if (!amp) { 1718 expectedResult = QuickSuggestTestUtils.wikipediaResult({ 1719 keyword, 1720 fullKeyword, 1721 title: "Wikipedia suggestion with full keyword and prefix keywords", 1722 url: "https://example.com/wikipedia-full-keyword", 1723 }); 1724 } else if (isTopPick) { 1725 expectedResult = QuickSuggestTestUtils.ampResult({ 1726 keyword, 1727 fullKeyword, 1728 title: "AMP suggestion with full keyword and prefix keywords", 1729 url: "https://example.com/amp-full-keyword", 1730 suggestedIndex: 1, 1731 isSuggestedIndexRelativeToGroup: false, 1732 isBestMatch: true, 1733 descriptionL10n: null, 1734 }); 1735 } else { 1736 expectedResult = QuickSuggestTestUtils.ampResult({ 1737 suggestedIndex: -1, 1738 keyword, 1739 fullKeyword, 1740 title: "AMP suggestion with full keyword and prefix keywords", 1741 url: "https://example.com/amp-full-keyword", 1742 }); 1743 } 1744 } 1745 1746 await check_results({ 1747 context: createContext(keyword, { 1748 providers: [UrlbarProviderQuickSuggest.name], 1749 isPrivate: false, 1750 }), 1751 matches: expectedResult ? [expectedResult] : [], 1752 }); 1753 } 1754 1755 UrlbarPrefs.clear("quicksuggest.ampTopPickCharThreshold"); 1756 }); 1757 1758 // AMP should not be shown as a top pick when the threshold is zero. 1759 add_task(async function ampTopPickCharThreshold_zero() { 1760 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1761 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1762 await QuickSuggestTestUtils.forceSync(); 1763 1764 UrlbarPrefs.set("quicksuggest.ampTopPickCharThreshold", 0); 1765 1766 let tests = [ 1767 { keyword: "amp full key", amp: true }, 1768 { keyword: "amp full keyw", amp: true }, 1769 { keyword: "amp full keywo", amp: true }, 1770 { keyword: "amp full keywor", amp: true }, 1771 { keyword: "amp full keyword", amp: true }, 1772 { keyword: "AmP FuLl KeYwOrD", amp: true }, 1773 { keyword: "xyz", fullKeyword: "xyz", amp: true }, 1774 { keyword: "XyZ", fullKeyword: "xyz", amp: true }, 1775 { keyword: "wikipedia full key" }, 1776 { keyword: "wikipedia full keyw" }, 1777 { keyword: "wikipedia full keywo" }, 1778 { keyword: "wikipedia full keywor" }, 1779 { keyword: "wikipedia full keyword" }, 1780 ]; 1781 1782 for (let { keyword, fullKeyword, amp } of tests) { 1783 fullKeyword ??= amp ? "amp full keyword" : "wikipedia full keyword"; 1784 info("Running subtest: " + JSON.stringify({ keyword, fullKeyword, amp })); 1785 1786 let expectedResult; 1787 if (!amp) { 1788 expectedResult = QuickSuggestTestUtils.wikipediaResult({ 1789 keyword, 1790 fullKeyword, 1791 title: "Wikipedia suggestion with full keyword and prefix keywords", 1792 url: "https://example.com/wikipedia-full-keyword", 1793 }); 1794 } else { 1795 expectedResult = QuickSuggestTestUtils.ampResult({ 1796 keyword, 1797 fullKeyword, 1798 title: "AMP suggestion with full keyword and prefix keywords", 1799 url: "https://example.com/amp-full-keyword", 1800 suggestedIndex: -1, 1801 }); 1802 } 1803 1804 await check_results({ 1805 context: createContext(keyword, { 1806 providers: [UrlbarProviderQuickSuggest.name], 1807 isPrivate: false, 1808 }), 1809 matches: [expectedResult], 1810 }); 1811 } 1812 1813 UrlbarPrefs.clear("quicksuggest.ampTopPickCharThreshold"); 1814 }); 1815 1816 // Tests `ampMatchingStrategy`. 1817 add_task(async function ampMatchingStrategy() { 1818 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1819 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1820 await QuickSuggestTestUtils.forceSync(); 1821 1822 // Test each strategy in `AmpMatchingStrategy`. There are only a few. 1823 for (let [key, value] of Object.entries(AmpMatchingStrategy)) { 1824 await doAmpMatchingStrategyTest({ key, value }); 1825 1826 // Reset back to the default strategy just to make sure that works. 1827 await doAmpMatchingStrategyTest({ 1828 key: "(default)", 1829 value: 0, 1830 }); 1831 } 1832 1833 // Test an invalid strategy integer value. The default strategy should 1834 // actually be used. First we need to set a valid non-default strategy. 1835 await doAmpMatchingStrategyTest({ 1836 key: "FTS_AGAINST_TITLE", 1837 value: AmpMatchingStrategy.FTS_AGAINST_TITLE, 1838 }); 1839 await doAmpMatchingStrategyTest({ 1840 key: "(invalid)", 1841 value: 99, 1842 expectedStrategy: 0, 1843 }); 1844 1845 Services.prefs.clearUserPref( 1846 "browser.urlbar.quicksuggest.ampMatchingStrategy" 1847 ); 1848 await QuickSuggestTestUtils.forceSync(); 1849 }); 1850 1851 async function doAmpMatchingStrategyTest({ 1852 key, 1853 value, 1854 expectedStrategy = value, 1855 }) { 1856 info("Doing ampMatchingStrategy test: " + JSON.stringify({ key, value })); 1857 1858 let sandbox = sinon.createSandbox(); 1859 let ingestSpy = sandbox.spy(QuickSuggest.rustBackend._test_store, "ingest"); 1860 1861 // Set the strategy. It should trigger ingest. (Assuming it's different from 1862 // the current strategy. If it's not, ingest won't happen.) 1863 Services.prefs.setIntPref( 1864 "browser.urlbar.quicksuggest.ampMatchingStrategy", 1865 value 1866 ); 1867 1868 let ingestCall = await TestUtils.waitForCondition(() => { 1869 return ingestSpy.getCalls().find(call => { 1870 let ingestConstraints = call.args[0]; 1871 return ingestConstraints?.providers[0] == SuggestionProvider.AMP; 1872 }); 1873 }, "Waiting for ingest() to be called with Amp provider"); 1874 1875 // Check the provider constraints in the ingest constraints. 1876 let { providerConstraints } = ingestCall.args[0]; 1877 if (!expectedStrategy) { 1878 Assert.ok( 1879 !providerConstraints, 1880 "ingest() should not have been called with provider constraints" 1881 ); 1882 } else { 1883 Assert.ok( 1884 providerConstraints, 1885 "ingest() should have been called with provider constraints" 1886 ); 1887 Assert.strictEqual( 1888 providerConstraints.ampAlternativeMatching, 1889 expectedStrategy, 1890 "ampAlternativeMatching should have been set" 1891 ); 1892 } 1893 1894 // Now do a query to make sure it also uses the correct provider constraints. 1895 // No need to use `check_results()`. We only need to trigger a query, and 1896 // checking the right results unnecessarily complicates things. 1897 let querySpy = sandbox.spy( 1898 QuickSuggest.rustBackend._test_store, 1899 "queryWithMetrics" 1900 ); 1901 1902 let controller = UrlbarTestUtils.newMockController(); 1903 await controller.startQuery( 1904 createContext(SPONSORED_SEARCH_STRING, { 1905 providers: [UrlbarProviderQuickSuggest.name], 1906 isPrivate: false, 1907 }) 1908 ); 1909 1910 let queryCalls = querySpy.getCalls(); 1911 Assert.equal(queryCalls.length, 1, "query() should have been called once"); 1912 1913 let query = queryCalls[0].args[0]; 1914 Assert.ok(query, "query() should have been called with a query object"); 1915 Assert.ok( 1916 query.providerConstraints, 1917 "query() should have been called with provider constraints" 1918 ); 1919 1920 if (!expectedStrategy) { 1921 Assert.strictEqual( 1922 query.providerConstraints.ampAlternativeMatching, 1923 null, 1924 "ampAlternativeMatching should not have been set on query provider constraints" 1925 ); 1926 } else { 1927 Assert.strictEqual( 1928 query.providerConstraints.ampAlternativeMatching, 1929 expectedStrategy, 1930 "ampAlternativeMatching should have been set on query provider constraints" 1931 ); 1932 } 1933 1934 sandbox.restore(); 1935 } 1936 1937 add_task(async function offline_amp_disabled() { 1938 for (let pref of [ 1939 "suggest.quicksuggest.all", 1940 "suggest.quicksuggest.sponsored", 1941 "amp.featureGate", 1942 "suggest.amp", 1943 ]) { 1944 info("Testing with pref: " + pref); 1945 1946 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1947 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 1948 await QuickSuggestTestUtils.forceSync(); 1949 1950 // First make sure we can match an AMP suggestion. 1951 await check_results({ 1952 context: createContext(SPONSORED_SEARCH_STRING, { 1953 providers: [UrlbarProviderQuickSuggest.name], 1954 isPrivate: false, 1955 }), 1956 matches: [QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 })], 1957 }); 1958 1959 // Now disable the pref and try again. 1960 UrlbarPrefs.set(pref, false); 1961 await QuickSuggestTestUtils.forceSync(); 1962 1963 await check_results({ 1964 context: createContext(SPONSORED_SEARCH_STRING, { 1965 providers: [UrlbarProviderQuickSuggest.name], 1966 isPrivate: false, 1967 }), 1968 matches: [], 1969 }); 1970 1971 UrlbarPrefs.clear(pref); 1972 } 1973 1974 await QuickSuggestTestUtils.forceSync(); 1975 }); 1976 1977 add_task(async function offline_wikipedia_disabled() { 1978 for (let pref of [ 1979 "suggest.quicksuggest.all", 1980 "wikipedia.featureGate", 1981 "suggest.wikipedia", 1982 ]) { 1983 info("Testing with pref: " + pref); 1984 1985 UrlbarPrefs.set("suggest.quicksuggest.all", true); 1986 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 1987 await QuickSuggestTestUtils.forceSync(); 1988 1989 // First make sure we can match a Wikipedia suggestion. 1990 await check_results({ 1991 context: createContext(NONSPONSORED_SEARCH_STRING, { 1992 providers: [UrlbarProviderQuickSuggest.name], 1993 isPrivate: false, 1994 }), 1995 matches: [QuickSuggestTestUtils.wikipediaResult()], 1996 }); 1997 1998 // Now disable the pref and try again. 1999 UrlbarPrefs.set(pref, false); 2000 await QuickSuggestTestUtils.forceSync(); 2001 2002 await check_results({ 2003 context: createContext(NONSPONSORED_SEARCH_STRING, { 2004 providers: [UrlbarProviderQuickSuggest.name], 2005 isPrivate: false, 2006 }), 2007 matches: [], 2008 }); 2009 2010 UrlbarPrefs.clear(pref); 2011 } 2012 2013 await QuickSuggestTestUtils.forceSync(); 2014 }); 2015 2016 add_task(async function online_amp_disabled() { 2017 await doMerinoTest(async () => { 2018 for (let pref of [ 2019 "suggest.quicksuggest.all", 2020 "suggest.quicksuggest.sponsored", 2021 "amp.featureGate", 2022 "suggest.amp", 2023 ]) { 2024 info("Testing with pref: " + pref); 2025 2026 UrlbarPrefs.set("suggest.quicksuggest.all", true); 2027 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 2028 await QuickSuggestTestUtils.forceSync(); 2029 2030 // First make sure we can match an AMP suggestion. 2031 await check_results({ 2032 context: createContext("test", { 2033 providers: [UrlbarProviderQuickSuggest.name], 2034 isPrivate: false, 2035 }), 2036 matches: [ 2037 QuickSuggestTestUtils.ampResult({ 2038 source: "merino", 2039 provider: "adm", 2040 icon: "https://example.com/amp-icon", 2041 iabCategory: "22 - Shopping", 2042 requestId: "request_id", 2043 suggestedIndex: -1, 2044 }), 2045 ], 2046 }); 2047 2048 // Now disable the pref and try again. 2049 UrlbarPrefs.set(pref, false); 2050 await QuickSuggestTestUtils.forceSync(); 2051 2052 // Unless the pref was `all`, the Wikipedia Merino suggestion should now 2053 // be matched. 2054 let expected = 2055 pref == "suggest.quicksuggest.all" 2056 ? [] 2057 : [ 2058 QuickSuggestTestUtils.wikipediaResult({ 2059 source: "merino", 2060 provider: "wikipedia", 2061 telemetryType: "wikipedia", 2062 icon: "https://example.com/wikipedia-icon", 2063 }), 2064 ]; 2065 await check_results({ 2066 context: createContext("test", { 2067 providers: [UrlbarProviderQuickSuggest.name], 2068 isPrivate: false, 2069 }), 2070 matches: expected, 2071 }); 2072 2073 UrlbarPrefs.clear(pref); 2074 } 2075 2076 await QuickSuggestTestUtils.forceSync(); 2077 }); 2078 }); 2079 2080 add_task(async function online_wikipedia_disabled() { 2081 await doMerinoTest(async () => { 2082 for (let pref of [ 2083 "suggest.quicksuggest.all", 2084 "wikipedia.featureGate", 2085 "suggest.wikipedia", 2086 ]) { 2087 info("Testing with pref: " + pref); 2088 2089 UrlbarPrefs.set("suggest.quicksuggest.all", true); 2090 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 2091 await QuickSuggestTestUtils.forceSync(); 2092 2093 // First make sure we can match a Wikipedia suggestion. 2094 await check_results({ 2095 context: createContext("test", { 2096 providers: [UrlbarProviderQuickSuggest.name], 2097 isPrivate: false, 2098 }), 2099 matches: [ 2100 QuickSuggestTestUtils.wikipediaResult({ 2101 source: "merino", 2102 provider: "wikipedia", 2103 telemetryType: "wikipedia", 2104 icon: "https://example.com/wikipedia-icon", 2105 }), 2106 ], 2107 }); 2108 2109 // Now disable the pref and try again. 2110 UrlbarPrefs.set(pref, false); 2111 await QuickSuggestTestUtils.forceSync(); 2112 2113 await check_results({ 2114 context: createContext("test", { 2115 providers: [UrlbarProviderQuickSuggest.name], 2116 isPrivate: false, 2117 }), 2118 matches: [], 2119 }); 2120 2121 UrlbarPrefs.clear(pref); 2122 } 2123 2124 await QuickSuggestTestUtils.forceSync(); 2125 }); 2126 }); 2127 2128 async function doMerinoTest(callback) { 2129 UrlbarPrefs.set("quicksuggest.online.available", true); 2130 UrlbarPrefs.set("quicksuggest.online.enabled", true); 2131 await MerinoTestUtils.server.start(); 2132 2133 MerinoTestUtils.server.response.body.suggestions = MERINO_SUGGESTIONS; 2134 2135 await callback(); 2136 2137 await MerinoTestUtils.server.stop(); 2138 UrlbarPrefs.clear("quicksuggest.online.available"); 2139 UrlbarPrefs.clear("quicksuggest.online.enabled"); 2140 } 2141 2142 add_task(async function mergeRustProviderConstraints() { 2143 let tests = [ 2144 { 2145 a: null, 2146 b: null, 2147 expected: null, 2148 }, 2149 2150 // b is null 2151 { 2152 a: {}, 2153 b: null, 2154 expected: {}, 2155 }, 2156 { 2157 a: { ampAlternativeMatching: 1 }, 2158 b: null, 2159 expected: { ampAlternativeMatching: 1 }, 2160 }, 2161 { 2162 a: { dynamicSuggestionTypes: [] }, 2163 b: null, 2164 expected: { dynamicSuggestionTypes: [] }, 2165 }, 2166 { 2167 a: { dynamicSuggestionTypes: ["aaa"] }, 2168 b: null, 2169 expected: { dynamicSuggestionTypes: ["aaa"] }, 2170 }, 2171 { 2172 a: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2173 b: null, 2174 expected: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2175 }, 2176 { 2177 a: { dynamicSuggestionTypes: ["aaa", "bbb"], ampAlternativeMatching: 1 }, 2178 b: null, 2179 expected: { 2180 dynamicSuggestionTypes: ["aaa", "bbb"], 2181 ampAlternativeMatching: 1, 2182 }, 2183 }, 2184 2185 // b is an empty object 2186 { 2187 a: {}, 2188 b: {}, 2189 expected: {}, 2190 }, 2191 { 2192 a: { ampAlternativeMatching: 1 }, 2193 b: {}, 2194 expected: { ampAlternativeMatching: 1 }, 2195 }, 2196 { 2197 a: { dynamicSuggestionTypes: [] }, 2198 b: {}, 2199 expected: { dynamicSuggestionTypes: [] }, 2200 }, 2201 { 2202 a: { dynamicSuggestionTypes: ["aaa"] }, 2203 b: {}, 2204 expected: { dynamicSuggestionTypes: ["aaa"] }, 2205 }, 2206 { 2207 a: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2208 b: {}, 2209 expected: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2210 }, 2211 { 2212 a: { dynamicSuggestionTypes: ["aaa", "bbb"], ampAlternativeMatching: 1 }, 2213 b: {}, 2214 expected: { 2215 dynamicSuggestionTypes: ["aaa", "bbb"], 2216 ampAlternativeMatching: 1, 2217 }, 2218 }, 2219 2220 // b is { ampAlternativeMatching: 1 } 2221 { 2222 a: {}, 2223 b: { ampAlternativeMatching: 1 }, 2224 expected: { ampAlternativeMatching: 1 }, 2225 }, 2226 { 2227 a: { ampAlternativeMatching: 1 }, 2228 b: { ampAlternativeMatching: 1 }, 2229 expected: { ampAlternativeMatching: 1 }, 2230 }, 2231 { 2232 a: { dynamicSuggestionTypes: [] }, 2233 b: { ampAlternativeMatching: 1 }, 2234 expected: { dynamicSuggestionTypes: [], ampAlternativeMatching: 1 }, 2235 }, 2236 { 2237 a: { dynamicSuggestionTypes: ["aaa"] }, 2238 b: { ampAlternativeMatching: 1 }, 2239 expected: { dynamicSuggestionTypes: ["aaa"], ampAlternativeMatching: 1 }, 2240 }, 2241 { 2242 a: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2243 b: { ampAlternativeMatching: 1 }, 2244 expected: { 2245 dynamicSuggestionTypes: ["aaa", "bbb"], 2246 ampAlternativeMatching: 1, 2247 }, 2248 }, 2249 { 2250 a: { dynamicSuggestionTypes: ["aaa", "bbb"], ampAlternativeMatching: 1 }, 2251 b: { ampAlternativeMatching: 1 }, 2252 expected: { 2253 dynamicSuggestionTypes: ["aaa", "bbb"], 2254 ampAlternativeMatching: 1, 2255 }, 2256 }, 2257 2258 // b is { dynamicSuggestionTypes: [] } 2259 { 2260 a: {}, 2261 b: { dynamicSuggestionTypes: [] }, 2262 expected: { dynamicSuggestionTypes: [] }, 2263 }, 2264 { 2265 a: { ampAlternativeMatching: 1 }, 2266 b: { dynamicSuggestionTypes: [] }, 2267 expected: { dynamicSuggestionTypes: [], ampAlternativeMatching: 1 }, 2268 }, 2269 { 2270 a: { dynamicSuggestionTypes: [] }, 2271 b: { dynamicSuggestionTypes: [] }, 2272 expected: { dynamicSuggestionTypes: [] }, 2273 }, 2274 { 2275 a: { dynamicSuggestionTypes: ["aaa"] }, 2276 b: { dynamicSuggestionTypes: [] }, 2277 expected: { dynamicSuggestionTypes: ["aaa"] }, 2278 }, 2279 { 2280 a: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2281 b: { dynamicSuggestionTypes: [] }, 2282 expected: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2283 }, 2284 { 2285 a: { dynamicSuggestionTypes: ["aaa", "bbb"], ampAlternativeMatching: 1 }, 2286 b: { dynamicSuggestionTypes: [] }, 2287 expected: { 2288 dynamicSuggestionTypes: ["aaa", "bbb"], 2289 ampAlternativeMatching: 1, 2290 }, 2291 }, 2292 2293 // b is { dynamicSuggestionTypes: ["bbb"] } 2294 { 2295 a: {}, 2296 b: { dynamicSuggestionTypes: ["bbb"] }, 2297 expected: { dynamicSuggestionTypes: ["bbb"] }, 2298 }, 2299 { 2300 a: { ampAlternativeMatching: 1 }, 2301 b: { dynamicSuggestionTypes: ["bbb"] }, 2302 expected: { dynamicSuggestionTypes: ["bbb"], ampAlternativeMatching: 1 }, 2303 }, 2304 { 2305 a: { dynamicSuggestionTypes: [] }, 2306 b: { dynamicSuggestionTypes: ["bbb"] }, 2307 expected: { dynamicSuggestionTypes: ["bbb"] }, 2308 }, 2309 { 2310 a: { dynamicSuggestionTypes: ["aaa"] }, 2311 b: { dynamicSuggestionTypes: ["bbb"] }, 2312 expected: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2313 }, 2314 { 2315 a: { dynamicSuggestionTypes: ["bbb"] }, 2316 b: { dynamicSuggestionTypes: ["bbb"] }, 2317 expected: { dynamicSuggestionTypes: ["bbb"] }, 2318 }, 2319 { 2320 a: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2321 b: { dynamicSuggestionTypes: ["bbb"] }, 2322 expected: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2323 }, 2324 { 2325 a: { dynamicSuggestionTypes: ["aaa", "bbb"], ampAlternativeMatching: 1 }, 2326 b: { dynamicSuggestionTypes: ["bbb"] }, 2327 expected: { 2328 dynamicSuggestionTypes: ["aaa", "bbb"], 2329 ampAlternativeMatching: 1, 2330 }, 2331 }, 2332 2333 // b is { dynamicSuggestionTypes: ["bbb", "ddd"] } 2334 { 2335 a: {}, 2336 b: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2337 expected: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2338 }, 2339 { 2340 a: { ampAlternativeMatching: 1 }, 2341 b: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2342 expected: { 2343 dynamicSuggestionTypes: ["bbb", "ddd"], 2344 ampAlternativeMatching: 1, 2345 }, 2346 }, 2347 { 2348 a: { dynamicSuggestionTypes: [] }, 2349 b: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2350 expected: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2351 }, 2352 { 2353 a: { dynamicSuggestionTypes: ["aaa"] }, 2354 b: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2355 expected: { dynamicSuggestionTypes: ["aaa", "bbb", "ddd"] }, 2356 }, 2357 { 2358 a: { dynamicSuggestionTypes: ["bbb"] }, 2359 b: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2360 expected: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2361 }, 2362 { 2363 a: { dynamicSuggestionTypes: ["aaa", "bbb"] }, 2364 b: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2365 expected: { dynamicSuggestionTypes: ["aaa", "bbb", "ddd"] }, 2366 }, 2367 { 2368 a: { dynamicSuggestionTypes: ["aaa", "bbb", "ccc"] }, 2369 b: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2370 expected: { dynamicSuggestionTypes: ["aaa", "bbb", "ccc", "ddd"] }, 2371 }, 2372 { 2373 a: { 2374 dynamicSuggestionTypes: ["aaa", "bbb", "ccc"], 2375 ampAlternativeMatching: 1, 2376 }, 2377 b: { dynamicSuggestionTypes: ["bbb", "ddd"] }, 2378 expected: { 2379 dynamicSuggestionTypes: ["aaa", "bbb", "ccc", "ddd"], 2380 ampAlternativeMatching: 1, 2381 }, 2382 }, 2383 ]; 2384 2385 for (let { a, b, expected } of tests) { 2386 for (let [first, second] of [ 2387 [a, b], 2388 [b, a], 2389 ]) { 2390 info("Doing test: " + JSON.stringify({ first, second })); 2391 let actual = SuggestBackendRust.mergeProviderConstraints(first, second); 2392 Assert.deepEqual( 2393 actual, 2394 expected, 2395 "Expected merged constraints with " + JSON.stringify({ first, second }) 2396 ); 2397 } 2398 } 2399 }); 2400 2401 async function resetRemoteSettingsData(data = REMOTE_SETTINGS_RESULTS) { 2402 let isAmp = suggestion => suggestion.iab_category == "22 - Shopping"; 2403 await QuickSuggestTestUtils.setRemoteSettingsRecords([ 2404 { 2405 collection: QuickSuggestTestUtils.RS_COLLECTION.AMP, 2406 type: QuickSuggestTestUtils.RS_TYPE.AMP, 2407 attachment: data.filter(isAmp), 2408 }, 2409 { 2410 collection: QuickSuggestTestUtils.RS_COLLECTION.OTHER, 2411 type: QuickSuggestTestUtils.RS_TYPE.WIKIPEDIA, 2412 attachment: data.filter(s => !isAmp(s)), 2413 }, 2414 ]); 2415 }