test_quicksuggest_dynamicSuggestions.js (16720B)
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 dynamic Rust suggestions. 6 7 const REMOTE_SETTINGS_RECORDS = [ 8 // nonsponsored, no `bypassSuggestAll` 9 { 10 type: "dynamic-suggestions", 11 suggestion_type: "aaa", 12 score: 0.9, 13 attachment: [ 14 { 15 keywords: ["aaa keyword", "aaa bbb keyword", "wikipedia"], 16 data: { 17 result: { 18 payload: { 19 title: "aaa title", 20 url: "https://example.com/aaa", 21 }, 22 }, 23 }, 24 }, 25 ], 26 }, 27 28 // sponsored, no `bypassSuggestAll` 29 { 30 type: "dynamic-suggestions", 31 suggestion_type: "bbb", 32 score: 0.1, 33 attachment: [ 34 { 35 keywords: ["bbb keyword", "aaa bbb keyword", "wikipedia"], 36 dismissal_key: "bbb-dismissal-key", 37 data: { 38 result: { 39 isBestMatch: true, 40 suggestedIndex: 1, 41 isSuggestedIndexRelativeToGroup: false, 42 isRichSuggestion: true, 43 payload: { 44 title: "bbb title", 45 url: "https://example.com/bbb", 46 isSponsored: true, 47 telemetryType: "bbb_telemetry_type", 48 }, 49 }, 50 }, 51 }, 52 ], 53 }, 54 55 // nonsponsored, `bypassSuggestAll: true` 56 { 57 type: "dynamic-suggestions", 58 suggestion_type: "ccc", 59 score: 0.9, 60 attachment: [ 61 { 62 keywords: ["ccc keyword", "ccc ddd keyword"], 63 data: { 64 result: { 65 bypassSuggestAll: true, 66 payload: { 67 title: "ccc title", 68 url: "https://example.com/ccc", 69 }, 70 }, 71 }, 72 }, 73 ], 74 }, 75 76 // sponsored, `bypassSuggestAll: true` 77 { 78 type: "dynamic-suggestions", 79 suggestion_type: "ddd", 80 score: 0.9, 81 attachment: [ 82 { 83 keywords: ["ddd keyword", "ccc ddd keyword"], 84 data: { 85 result: { 86 bypassSuggestAll: true, 87 payload: { 88 title: "ddd title", 89 url: "https://example.com/ddd", 90 isSponsored: true, 91 }, 92 }, 93 }, 94 }, 95 ], 96 }, 97 98 { 99 type: QuickSuggestTestUtils.RS_TYPE.WIKIPEDIA, 100 attachment: [QuickSuggestTestUtils.wikipediaRemoteSettings()], 101 }, 102 ]; 103 104 const EXPECTED_AAA_RESULT = makeExpectedResult({ 105 title: "aaa title", 106 url: "https://example.com/aaa", 107 telemetryType: "aaa", 108 suggestionType: "aaa", 109 }); 110 111 const EXPECTED_BBB_RESULT = makeExpectedResult({ 112 title: "bbb title", 113 url: "https://example.com/bbb", 114 isSponsored: true, 115 telemetryType: "bbb_telemetry_type", 116 suggestionType: "bbb", 117 isBestMatch: true, 118 suggestedIndex: 1, 119 isSuggestedIndexRelativeToGroup: false, 120 isRichSuggestion: true, 121 }); 122 123 const EXPECTED_CCC_RESULT = makeExpectedResult({ 124 title: "ccc title", 125 url: "https://example.com/ccc", 126 telemetryType: "ccc", 127 suggestionType: "ccc", 128 }); 129 130 const EXPECTED_DDD_RESULT = makeExpectedResult({ 131 title: "ddd title", 132 url: "https://example.com/ddd", 133 isSponsored: true, 134 telemetryType: "ddd", 135 suggestionType: "ddd", 136 }); 137 138 add_setup(async function () { 139 await QuickSuggestTestUtils.ensureQuickSuggestInit({ 140 remoteSettingsRecords: REMOTE_SETTINGS_RECORDS, 141 prefs: [ 142 ["quicksuggest.dynamicSuggestionTypes", "aaa,bbb,ccc,ddd"], 143 ["suggest.quicksuggest.all", true], 144 ["suggest.quicksuggest.sponsored", true], 145 ["quicksuggest.ampTopPickCharThreshold", 0], 146 ], 147 }); 148 }); 149 150 // When a dynamic suggestion doesn't include `telemetryType`, its 151 // `suggestionType` should be used as the telemetry type. 152 add_task(async function telemetryType_default() { 153 Assert.equal( 154 QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({ 155 suggestionType: "abcdefg", 156 }), 157 "abcdefg", 158 "Telemetry type should be correct when using default" 159 ); 160 }); 161 162 // When a dynamic suggestion includes `telemetryType`, it should be used as the 163 // telemetry type. 164 add_task(async function telemetryType_override() { 165 Assert.equal( 166 QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({ 167 suggestionType: "abcdefg", 168 data: { 169 result: { 170 payload: { 171 telemetryType: "telemetry_type_override", 172 }, 173 }, 174 }, 175 }), 176 "telemetry_type_override", 177 "Telemetry type should be correct when overridden" 178 ); 179 }); 180 181 add_task(async function basic() { 182 let queries = [ 183 { 184 query: "no match", 185 expected: [], 186 }, 187 { 188 query: "aaa keyword", 189 expected: [EXPECTED_AAA_RESULT], 190 }, 191 { 192 query: "bbb keyword", 193 expected: [EXPECTED_BBB_RESULT], 194 }, 195 { 196 query: "aaa bbb keyword", 197 // The "aaa" suggestion has a higher score than "bbb". 198 expected: [EXPECTED_AAA_RESULT], 199 }, 200 { 201 query: "ccc keyword", 202 expected: [EXPECTED_CCC_RESULT], 203 }, 204 { 205 query: "ddd keyword", 206 expected: [EXPECTED_DDD_RESULT], 207 }, 208 { 209 query: "ccc ddd keyword", 210 // The "ccc" suggestion has a higher score than "ddd". 211 expected: [EXPECTED_CCC_RESULT], 212 }, 213 ]; 214 215 await doQueries(queries); 216 }); 217 218 // When only one dynamic suggestion type is enabled, only its result should be 219 // returned. This task assumes multiples types were added to remote settings in 220 // the setup task. 221 add_task(async function oneSuggestionType() { 222 await withSuggestionTypesPref("bbb", async () => { 223 await doQueries([ 224 { 225 query: "aaa keyword", 226 expected: [], 227 }, 228 { 229 query: "bbb keyword", 230 expected: [EXPECTED_BBB_RESULT], 231 }, 232 { 233 query: "aaa bbb keyword", 234 expected: [EXPECTED_BBB_RESULT], 235 }, 236 { 237 query: "doesn't match", 238 expected: [], 239 }, 240 ]); 241 }); 242 }); 243 244 // When no dynamic suggestion types are enabled, no results should be added. 245 add_task(async function disabled() { 246 await withSuggestionTypesPref("", async () => { 247 await doQueries( 248 ["aaa keyword", "bbb keyword", "aaa bbb keyword"].map(query => ({ 249 query, 250 expected: [], 251 })) 252 ); 253 }); 254 }); 255 256 // Dynamic suggestions shouldn't be added when `all` is disabled unless they 257 // define `bypassSuggestAll`. 258 add_task(async function allDisabled() { 259 UrlbarPrefs.set("suggest.quicksuggest.all", false); 260 261 // The sponsored pref shouldn't matter. 262 for (let sponsoredEnabled of [true, false]) { 263 UrlbarPrefs.set("suggest.quicksuggest.sponsored", sponsoredEnabled); 264 265 await withSuggestionTypesPref("aaa,bbb,ccc,ddd", async () => { 266 await doQueries([ 267 { 268 query: "aaa keyword", 269 expected: [], 270 }, 271 { 272 query: "bbb keyword", 273 expected: [], 274 }, 275 { 276 query: "aaa bbb keyword", 277 expected: [], 278 }, 279 280 { 281 query: "ccc keyword", 282 expected: [EXPECTED_CCC_RESULT], 283 }, 284 { 285 query: "ddd keyword", 286 expected: [EXPECTED_DDD_RESULT], 287 }, 288 { 289 query: "ccc ddd keyword", 290 // The "ccc" suggestion has a higher score than "ddd". 291 expected: [EXPECTED_CCC_RESULT], 292 }, 293 ]); 294 }); 295 } 296 297 UrlbarPrefs.set("suggest.quicksuggest.all", true); 298 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 299 await QuickSuggestTestUtils.forceSync(); 300 }); 301 302 // Dynamic suggestions that are sponsored shouldn't be added when sponsored 303 // suggestions are disabled unless they define `bypassSuggestAll`. 304 add_task(async function sponsoredDisabled() { 305 UrlbarPrefs.set("suggest.quicksuggest.all", true); 306 UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); 307 308 await withSuggestionTypesPref("aaa,bbb,ccc,ddd", async () => { 309 await doQueries([ 310 { 311 query: "aaa keyword", 312 expected: [EXPECTED_AAA_RESULT], 313 }, 314 { 315 query: "bbb keyword", 316 expected: [], 317 }, 318 { 319 query: "aaa bbb keyword", 320 expected: [EXPECTED_AAA_RESULT], 321 }, 322 323 { 324 query: "ccc keyword", 325 expected: [EXPECTED_CCC_RESULT], 326 }, 327 { 328 query: "ddd keyword", 329 expected: [EXPECTED_DDD_RESULT], 330 }, 331 { 332 query: "ccc ddd keyword", 333 // The "ccc" suggestion has a higher score than "ddd". 334 expected: [EXPECTED_CCC_RESULT], 335 }, 336 ]); 337 }); 338 339 UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); 340 await QuickSuggestTestUtils.forceSync(); 341 }); 342 343 // Tests the `quickSuggestDynamicSuggestionTypes` Nimbus variable. 344 add_task(async function nimbus() { 345 // Clear `dynamicSuggestionTypes` to make sure the value comes from the Nimbus 346 // variable and not the pref. 347 await withSuggestionTypesPref("", async () => { 348 let cleanup = await UrlbarTestUtils.initNimbusFeature({ 349 quickSuggestDynamicSuggestionTypes: "aaa,bbb", 350 }); 351 await QuickSuggestTestUtils.forceSync(); 352 await doQueries([ 353 { 354 query: "aaa keyword", 355 expected: [EXPECTED_AAA_RESULT], 356 }, 357 { 358 query: "bbb keyword", 359 expected: [EXPECTED_BBB_RESULT], 360 }, 361 { 362 query: "aaa bbb keyword", 363 // The "aaa" suggestion has a higher score than "bbb". 364 expected: [EXPECTED_AAA_RESULT], 365 }, 366 { 367 query: "doesn't match", 368 expected: [], 369 }, 370 ]); 371 372 await cleanup(); 373 }); 374 }); 375 376 // Tests dismissals. Note that dynamic suggestions must define a `dismissal_key` 377 // in order to be dismissable. 378 add_task(async function dismissal() { 379 // Do a search and get the actual result that's returned. 380 let context = createContext("bbb keyword", { 381 providers: [UrlbarProviderQuickSuggest.name], 382 isPrivate: false, 383 }); 384 await check_results({ 385 context, 386 matches: [EXPECTED_BBB_RESULT], 387 }); 388 389 let result = context.results[0]; 390 let { suggestionObject } = result.payload; 391 let { dismissalKey } = suggestionObject; 392 Assert.equal( 393 dismissalKey, 394 "bbb-dismissal-key", 395 "The suggestion should have the expected dismissal key" 396 ); 397 398 // It shouldn't be dismissed yet. 399 Assert.ok( 400 !(await QuickSuggest.isResultDismissed(result)), 401 "The result should not be dismissed yet" 402 ); 403 Assert.ok( 404 !(await QuickSuggest.rustBackend.isRustSuggestionDismissed( 405 suggestionObject 406 )), 407 "The suggestion should not be dismissed yet" 408 ); 409 Assert.ok( 410 !(await QuickSuggest.rustBackend.isDismissedByKey(dismissalKey)), 411 "The dismissal key should not be registered yet" 412 ); 413 414 // Dismiss it. It should be dismissed by its dismissal key. 415 await QuickSuggest.dismissResult(result); 416 417 Assert.ok( 418 await QuickSuggest.isResultDismissed(result), 419 "The result should be dismissed" 420 ); 421 Assert.ok( 422 await QuickSuggest.rustBackend.isRustSuggestionDismissed(suggestionObject), 423 "The suggestion should be dismissed" 424 ); 425 426 await check_results({ 427 context: createContext("bbb keyword", { 428 providers: [UrlbarProviderQuickSuggest.name], 429 isPrivate: false, 430 }), 431 matches: [], 432 }); 433 434 // Clear dismissals and check again. 435 await QuickSuggest.clearDismissedSuggestions(); 436 437 await check_results({ 438 context: createContext("bbb keyword", { 439 providers: [UrlbarProviderQuickSuggest.name], 440 isPrivate: false, 441 }), 442 matches: [EXPECTED_BBB_RESULT], 443 }); 444 445 Assert.ok( 446 !(await QuickSuggest.isResultDismissed(result)), 447 "The result should not be dismissed after clearing dismissals" 448 ); 449 Assert.ok( 450 !(await QuickSuggest.rustBackend.isRustSuggestionDismissed( 451 suggestionObject 452 )), 453 "The suggestion should not be dismissed after clearing dismissals" 454 ); 455 Assert.ok( 456 !(await QuickSuggest.rustBackend.isDismissedByKey(dismissalKey)), 457 "The dismissal key should not be registered after clearing dismissals" 458 ); 459 }); 460 461 // Tests whether the prefs DynamicSuggestion handles clears. 462 add_task(async function clearDismissedSuggestions() { 463 let feature = QuickSuggest.getFeature("DynamicSuggestions"); 464 let sandbox = sinon.createSandbox(); 465 sinon 466 .stub(feature, "primaryUserControlledPreferences") 467 .get(() => ["suggest.realtimeOptIn", "autoFill", "closeOtherPanelsOnOpen"]); 468 469 UrlbarPrefs.set("suggest.realtimeOptIn", false); 470 UrlbarPrefs.set("autoFill", false); 471 UrlbarPrefs.set("closeOtherPanelsOnOpen", false); 472 473 Assert.ok(await QuickSuggest.canClearDismissedSuggestions()); 474 await QuickSuggest.clearDismissedSuggestions(); 475 476 Assert.ok(UrlbarPrefs.get("suggest.realtimeOptIn")); 477 Assert.ok(UrlbarPrefs.get("autoFill")); 478 Assert.ok(UrlbarPrefs.get("closeOtherPanelsOnOpen")); 479 480 sandbox.restore(); 481 }); 482 483 // Tests some suggestions with bad data that desktop ignores. 484 add_task(async function badSuggestions() { 485 await QuickSuggestTestUtils.setRemoteSettingsRecords([ 486 { 487 type: "dynamic-suggestions", 488 suggestion_type: "bad", 489 attachment: [ 490 // Include a good suggestion so we can verify this record was actually 491 // ingested. Change the keyword so we don't confuse ourselves by 492 // searching for an "aaa" keyword and getting a urlbar result whose 493 // telemetry type and dynamic suggestion type is "bad". 494 { 495 ...REMOTE_SETTINGS_RECORDS[0].attachment[0], 496 keywords: ["good actually"], 497 }, 498 // `data` is missing -- Rust actually allows this since `data` is 499 // defined as `Option<serde_json::Value>`, but desktop doesn't. 500 { 501 keywords: ["bad"], 502 }, 503 // `data` isn't an object 504 { 505 data: 123, 506 keywords: ["bad"], 507 }, 508 // `data.result` is missing 509 { 510 data: {}, 511 keywords: ["bad"], 512 }, 513 // `data.result` isn't an object 514 { 515 data: { 516 result: 123, 517 }, 518 keywords: ["bad"], 519 }, 520 // `data.result.payload` isn't an object 521 { 522 data: { 523 result: { 524 payload: 123, 525 }, 526 }, 527 keywords: ["bad"], 528 }, 529 ], 530 }, 531 ]); 532 533 await withSuggestionTypesPref("bad", async () => { 534 // Verify the good suggestion was ingested. 535 await check_results({ 536 context: createContext("good actually", { 537 providers: [UrlbarProviderQuickSuggest.name], 538 isPrivate: false, 539 }), 540 matches: [ 541 { 542 ...EXPECTED_AAA_RESULT, 543 payload: { 544 ...EXPECTED_AAA_RESULT.payload, 545 telemetryType: "bad", 546 suggestionType: "bad", 547 }, 548 }, 549 ], 550 }); 551 552 // No "bad" suggestions should be matched. 553 await check_results({ 554 context: createContext("bad", { 555 providers: [UrlbarProviderQuickSuggest.name], 556 isPrivate: false, 557 }), 558 matches: [], 559 }); 560 }); 561 562 // Clean up. 563 await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS); 564 }); 565 566 async function doQueries(queries) { 567 for (let { query, expected } of queries) { 568 info( 569 "Doing query: " + 570 JSON.stringify({ 571 query, 572 expected, 573 }) 574 ); 575 576 await check_results({ 577 context: createContext(query, { 578 providers: [UrlbarProviderQuickSuggest.name], 579 isPrivate: false, 580 }), 581 matches: expected, 582 }); 583 } 584 } 585 586 async function withSuggestionTypesPref(prefValue, callback) { 587 // Use `Services` to get the original pref value since `UrlbarPrefs` will 588 // parse the string value into a `Set`. 589 let originalPrefValue = Services.prefs.getCharPref( 590 "browser.urlbar.quicksuggest.dynamicSuggestionTypes" 591 ); 592 593 // Changing the pref (or Nimbus variable) to a different value will trigger 594 // ingest, so force sync afterward (or at least wait for ingest to finish). 595 UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", prefValue); 596 await QuickSuggestTestUtils.forceSync(); 597 598 await callback(); 599 600 UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", originalPrefValue); 601 await QuickSuggestTestUtils.forceSync(); 602 } 603 604 function makeExpectedResult({ 605 title, 606 url, 607 telemetryType, 608 suggestionType, 609 isSponsored = false, 610 isBestMatch = false, 611 suggestedIndex = -1, 612 isSuggestedIndexRelativeToGroup = true, 613 isRichSuggestion = undefined, 614 }) { 615 return { 616 type: UrlbarUtils.RESULT_TYPE.URL, 617 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 618 heuristic: false, 619 isBestMatch, 620 suggestedIndex, 621 isRichSuggestion, 622 isSuggestedIndexRelativeToGroup, 623 payload: { 624 title, 625 url, 626 isSponsored, 627 telemetryType, 628 suggestionType, 629 source: "rust", 630 provider: "Dynamic", 631 isManageable: true, 632 helpUrl: QuickSuggest.HELP_URL, 633 }, 634 }; 635 }