test_quicksuggest_yelp_ml.js (16076B)
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 Yelp suggestions served by the Suggest ML backend. 6 7 "use strict"; 8 9 ChromeUtils.defineESModuleGetters(this, { 10 MLSuggest: "moz-src:///browser/components/urlbar/private/MLSuggest.sys.mjs", 11 }); 12 13 const REMOTE_SETTINGS_RECORDS = [ 14 { 15 type: "yelp-suggestions", 16 attachment: { 17 // The "coffee" subject is important: It's how `YelpSuggestions` looks up 18 // the Yelp icon and score right now. 19 subjects: ["coffee"], 20 preModifiers: [], 21 postModifiers: [], 22 locationSigns: ["in", "nearby"], 23 yelpModifiers: [], 24 icon: "1234", 25 score: 0.5, 26 }, 27 }, 28 ...QuickSuggestTestUtils.geonamesRecords(), 29 ...QuickSuggestTestUtils.geonamesAlternatesRecords(), 30 ]; 31 32 const WATERLOO_RESULT = { 33 url: "https://www.yelp.com/search?find_desc=burgers&find_loc=Waterloo%2C+IA", 34 title: "burgers in Waterloo, IA", 35 }; 36 37 const YOKOHAMA_RESULT = { 38 url: "https://www.yelp.com/search?find_desc=burgers&find_loc=Yokohama%2C+Kanagawa", 39 title: "burgers in Yokohama, Kanagawa", 40 }; 41 42 let gSandbox; 43 let gMakeSuggestionsStub; 44 45 add_setup(async function init() { 46 // Stub `MLSuggest`. 47 gSandbox = sinon.createSandbox(); 48 gSandbox.stub(MLSuggest, "initialize"); 49 gSandbox.stub(MLSuggest, "shutdown"); 50 gMakeSuggestionsStub = gSandbox.stub(MLSuggest, "makeSuggestions"); 51 52 // Set up Rust Yelp suggestions that can be matched on the keyword "coffee". 53 await QuickSuggestTestUtils.ensureQuickSuggestInit({ 54 prefs: [ 55 ["browser.ml.enable", true], 56 ["quicksuggest.mlEnabled", true], 57 ["suggest.quicksuggest.all", true], 58 ["suggest.quicksuggest.sponsored", true], 59 ["yelp.mlEnabled", true], 60 ["yelp.serviceResultDistinction", false], 61 ], 62 remoteSettingsRecords: REMOTE_SETTINGS_RECORDS, 63 }); 64 65 await MerinoTestUtils.initGeolocation(); 66 }); 67 68 // Yelp ML should be disabled when the relevant prefs are disabled. 69 add_task(async function yelpDisabled() { 70 gMakeSuggestionsStub.returns({ intent: "yelp_intent", subject: "burgers" }); 71 let expectedResult = makeExpectedResult(YOKOHAMA_RESULT); 72 73 let tests = [ 74 // These disable the Yelp feature itself, including Rust suggestions. 75 "suggest.quicksuggest.sponsored", 76 "suggest.yelp", 77 "yelp.featureGate", 78 79 // These disable Yelp ML suggestions but leave the Yelp feature enabled. 80 // This test doesn't add any Yelp data to remote settings, but if it did, 81 // Yelp Rust suggestions would still be triggered. 82 "yelp.mlEnabled", 83 "browser.ml.enable", 84 "quicksuggest.mlEnabled", 85 86 // pref combinations 87 { 88 prefs: { 89 "suggest.quicksuggest.sponsored": true, 90 "suggest.quicksuggest.all": true, 91 }, 92 expected: true, 93 }, 94 { 95 prefs: { 96 "suggest.quicksuggest.sponsored": true, 97 "suggest.quicksuggest.all": false, 98 }, 99 expected: false, 100 }, 101 { 102 prefs: { 103 "suggest.quicksuggest.sponsored": false, 104 "suggest.quicksuggest.all": true, 105 }, 106 expected: false, 107 }, 108 { 109 prefs: { 110 "suggest.quicksuggest.sponsored": false, 111 "suggest.quicksuggest.all": false, 112 }, 113 expected: false, 114 }, 115 ]; 116 for (let test of tests) { 117 info("Starting subtest: " + JSON.stringify(test)); 118 119 let prefs; 120 let expected; 121 if (typeof test == "string") { 122 // A string value is a pref name, and we'll set it to false and expect no 123 // suggestions. 124 prefs = { [test]: false }; 125 expected = false; 126 } else { 127 ({ prefs, expected } = test); 128 } 129 130 // Before setting the prefs, first make sure the suggestion is added. 131 info("Doing search 1"); 132 await check_results({ 133 context: createContext("burgers", { 134 providers: [UrlbarProviderQuickSuggest.name], 135 isPrivate: false, 136 }), 137 matches: [expectedResult], 138 }); 139 140 // Also get the original pref values. 141 let originalPrefs = Object.fromEntries( 142 Object.keys(prefs).map(name => [name, UrlbarPrefs.get(name)]) 143 ); 144 145 // Now set the prefs. 146 info("Setting prefs and doing search 2"); 147 for (let [name, value] of Object.entries(prefs)) { 148 UrlbarPrefs.set(name, value); 149 } 150 await check_results({ 151 context: createContext("burgers", { 152 providers: [UrlbarProviderQuickSuggest.name], 153 isPrivate: false, 154 }), 155 matches: expected ? [expectedResult] : [], 156 }); 157 158 // Revert. 159 for (let [name, value] of Object.entries(originalPrefs)) { 160 UrlbarPrefs.set(name, value); 161 } 162 await QuickSuggestTestUtils.forceSync(); 163 164 // Make sure Yelp is added again. 165 info("Doing search 3 after reverting the prefs"); 166 await check_results({ 167 context: createContext("burgers", { 168 providers: [UrlbarProviderQuickSuggest.name], 169 isPrivate: false, 170 }), 171 matches: [expectedResult], 172 }); 173 } 174 }); 175 176 // Runs through a variety of mock intents. 177 add_task(async function intents() { 178 let tests = [ 179 { 180 desc: "subject with no location", 181 ml: { intent: "yelp_intent", subject: "burgers" }, 182 expected: YOKOHAMA_RESULT, 183 }, 184 { 185 desc: "subject with null city and null state", 186 ml: { 187 intent: "yelp_intent", 188 subject: "burgers", 189 location: { city: null, state: null }, 190 }, 191 expected: YOKOHAMA_RESULT, 192 }, 193 { 194 desc: "subject with city", 195 ml: { 196 intent: "yelp_intent", 197 subject: "burgers", 198 location: { city: "Waterloo", state: null }, 199 }, 200 expected: WATERLOO_RESULT, 201 }, 202 { 203 desc: "subject with state abbreviation", 204 ml: { 205 intent: "yelp_intent", 206 subject: "burgers", 207 location: { city: null, state: "IA" }, 208 }, 209 expected: null, 210 }, 211 { 212 desc: "subject with state name", 213 ml: { 214 intent: "yelp_intent", 215 subject: "burgers", 216 location: { city: null, state: "Iowa" }, 217 }, 218 expected: { 219 url: "https://www.yelp.com/search?find_desc=burgers&find_loc=Iowa", 220 title: "burgers in Iowa", 221 }, 222 }, 223 { 224 desc: "subject with city and state", 225 ml: { 226 intent: "yelp_intent", 227 subject: "burgers", 228 location: { city: "Waterloo", state: "IA" }, 229 }, 230 expected: WATERLOO_RESULT, 231 }, 232 { 233 desc: "no subject with no location", 234 ml: { 235 intent: "yelp_intent", 236 subject: "", 237 }, 238 expected: null, 239 }, 240 { 241 desc: "no subject with null city and null state", 242 ml: { 243 intent: "yelp_intent", 244 subject: "", 245 location: { city: null, state: null }, 246 }, 247 expected: null, 248 }, 249 { 250 desc: "no subject with city", 251 ml: { 252 intent: "yelp_intent", 253 subject: "", 254 location: { city: "Waterloo", state: null }, 255 }, 256 expected: null, 257 }, 258 { 259 desc: "no subject with state", 260 ml: { 261 intent: "yelp_intent", 262 subject: "", 263 location: { city: null, state: "IA" }, 264 }, 265 expected: null, 266 }, 267 { 268 desc: "no subject with city and state", 269 ml: { 270 intent: "yelp_intent", 271 subject: "", 272 location: { city: "Waterloo", state: "IA" }, 273 }, 274 expected: null, 275 }, 276 { 277 desc: "unrecognized intent", 278 ml: { intent: "unrecognized_intent" }, 279 expected: null, 280 }, 281 { 282 desc: "only Rust returns a suggestion", 283 query: "coffee", 284 ml: null, 285 expected: { 286 source: "rust", 287 provider: "Yelp", 288 url: "https://www.yelp.com/search?find_desc=coffee&find_loc=Yokohama%2C+Kanagawa", 289 title: "coffee in Yokohama, Kanagawa", 290 }, 291 }, 292 293 { 294 desc: "both ML and Rust return a suggestion", 295 query: "coffee", 296 ml: { 297 intent: "yelp_intent", 298 subject: "coffee", 299 location: { city: "Waterloo", state: null }, 300 }, 301 expected: { 302 url: "https://www.yelp.com/search?find_desc=coffee&find_loc=Waterloo%2C+IA", 303 title: "coffee in Waterloo, IA", 304 }, 305 }, 306 ]; 307 308 for (let { desc, ml, expected, query = "test" } of tests) { 309 info("Doing subtest: " + JSON.stringify({ desc, query, ml })); 310 311 // Do a query with ML enabled. If the expected result is from Rust, the 312 // query shouldn't return any results because *only* ML results are returned 313 // when ML is enabled. 314 gMakeSuggestionsStub.returns(ml); 315 await check_results({ 316 context: createContext(query, { 317 providers: [UrlbarProviderQuickSuggest.name], 318 isPrivate: false, 319 }), 320 matches: 321 expected && expected.source != "rust" 322 ? [makeExpectedResult(expected)] 323 : [], 324 }); 325 326 // If the expected result is from Rust, disable ML and query again to make 327 // sure it matches. 328 if (expected?.source == "rust") { 329 UrlbarPrefs.set("yelp.mlEnabled", false); 330 await check_results({ 331 context: createContext(query, { 332 providers: [UrlbarProviderQuickSuggest.name], 333 isPrivate: false, 334 }), 335 matches: [makeExpectedResult(expected)], 336 }); 337 UrlbarPrefs.set("yelp.mlEnabled", true); 338 } 339 } 340 }); 341 342 // The search string passed in to `MLSuggest.makeSuggestions()` should be 343 // trimmed and lowercased. 344 add_task(async function searchString() { 345 let searchStrings = []; 346 gMakeSuggestionsStub.callsFake(str => { 347 searchStrings.push(str); 348 return { 349 intent: "yelp_intent", 350 subject: "burgers", 351 }; 352 }); 353 354 await check_results({ 355 context: createContext(" AaA bBb CcC ", { 356 providers: [UrlbarProviderQuickSuggest.name], 357 isPrivate: false, 358 }), 359 matches: [makeExpectedResult(YOKOHAMA_RESULT)], 360 }); 361 362 Assert.deepEqual( 363 searchStrings, 364 ["aaa bbb ccc"], 365 "The search string passed in to MLSuggest should be trimmed and lowercased" 366 ); 367 }); 368 369 // The metadata cache should be populated from the "coffee" Rust suggestion when 370 // it's present in remote settings. 371 add_task(async function cache_fromRust() { 372 await doCacheTest({ 373 expectedScore: REMOTE_SETTINGS_RECORDS[0].attachment.score, 374 expectedRust: { 375 source: "rust", 376 provider: "Yelp", 377 url: "https://www.yelp.com/search?find_desc=coffee&find_loc=Yokohama%2C+Kanagawa", 378 title: "coffee in Yokohama, Kanagawa", 379 }, 380 }); 381 }); 382 383 // The metadata cache should be populated with default values when the "coffee" 384 // Rust suggestion is not present in remote settings. 385 add_task(async function cache_defaultValues() { 386 await QuickSuggestTestUtils.setRemoteSettingsRecords([ 387 ...QuickSuggestTestUtils.geonamesRecords(), 388 ...QuickSuggestTestUtils.geonamesAlternatesRecords(), 389 ]); 390 await doCacheTest({ 391 // This value is hardcoded in `YelpSuggestions` as the default. 392 expectedScore: 0.25, 393 expectedRust: null, 394 }); 395 await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS); 396 }); 397 398 async function doCacheTest({ expectedScore, expectedRust }) { 399 // Do a search with ML disabled to verify the Rust suggestion is matched as 400 // expected. 401 info("Doing search with ML disabled"); 402 UrlbarPrefs.set("yelp.mlEnabled", false); 403 await check_results({ 404 context: createContext("coffee", { 405 providers: [UrlbarProviderQuickSuggest.name], 406 isPrivate: false, 407 }), 408 matches: expectedRust ? [makeExpectedResult(expectedRust)] : [], 409 }); 410 UrlbarPrefs.set("yelp.mlEnabled", true); 411 412 gMakeSuggestionsStub.returns({ 413 intent: "yelp_intent", 414 subject: "burgers", 415 location: { city: "Waterloo", state: null }, 416 }); 417 418 // Stub `YelpSuggestions.makeResult()` so we can get the suggestion object 419 // passed into it. 420 let passedSuggestion; 421 let feature = QuickSuggest.getFeature("YelpSuggestions"); 422 let stub = gSandbox 423 .stub(feature, "makeResult") 424 .callsFake((queryContext, suggestion, searchString) => { 425 passedSuggestion = suggestion; 426 return stub.wrappedMethod.call( 427 feature, 428 queryContext, 429 suggestion, 430 searchString 431 ); 432 }); 433 434 // Do a search with ML enabled. 435 info("Doing search with ML enabled"); 436 feature._test_invalidateMetadataCache(); 437 await check_results({ 438 context: createContext("test", { 439 providers: [UrlbarProviderQuickSuggest.name], 440 isPrivate: false, 441 }), 442 matches: [makeExpectedResult(WATERLOO_RESULT)], 443 }); 444 445 stub.restore(); 446 447 // The score of the ML suggestion passed into `makeResult()` should have been 448 // taken from the metadata cache. 449 Assert.ok( 450 passedSuggestion, 451 "makeResult should have been called and passed the suggestion" 452 ); 453 Assert.equal( 454 passedSuggestion.score, 455 expectedScore, 456 "The suggestion should have borrowed its score from the Rust 'coffee' suggestion" 457 ); 458 } 459 460 // Tests the "Not relevant" command: a dismissed suggestion shouldn't be added. 461 add_task(async function notRelevant() { 462 let burgersIntent = { intent: "yelp_intent", subject: "burgers" }; 463 let waterlooIntent = { 464 intent: "yelp_intent", 465 subject: "burgers", 466 location: { city: "Waterloo" }, 467 }; 468 469 gMakeSuggestionsStub.returns(burgersIntent); 470 let result = makeExpectedResult(YOKOHAMA_RESULT); 471 472 info("Doing initial search to verify the suggestion is matched"); 473 await check_results({ 474 context: createContext("burgers", { 475 providers: [UrlbarProviderQuickSuggest.name], 476 isPrivate: false, 477 }), 478 matches: [result], 479 }); 480 481 let dismissalPromise = TestUtils.topicObserved( 482 "quicksuggest-dismissals-changed" 483 ); 484 triggerCommand({ 485 result, 486 command: "not_relevant", 487 feature: QuickSuggest.getFeature("YelpSuggestions"), 488 expectedCountsByCall: { 489 removeResult: 1, 490 }, 491 }); 492 await dismissalPromise; 493 494 Assert.ok( 495 await QuickSuggest.isResultDismissed(result), 496 "The result should be dismissed" 497 ); 498 499 info("Doing search for dismissed suggestion"); 500 await check_results({ 501 context: createContext("burgers", { 502 providers: [UrlbarProviderQuickSuggest.name], 503 isPrivate: false, 504 }), 505 matches: [], 506 }); 507 508 // Yelp suggestions are dismissed by URL excluding location, so all 509 // "ramen in <valid location>" results should be dismissed. 510 gMakeSuggestionsStub.returns(waterlooIntent); 511 await check_results({ 512 context: createContext("burgers in waterloo", { 513 providers: [UrlbarProviderQuickSuggest.name], 514 isPrivate: false, 515 }), 516 matches: [], 517 }); 518 519 info("Doing search for a suggestion that wasn't dismissed"); 520 gMakeSuggestionsStub.returns({ intent: "yelp_intent", subject: "ramen" }); 521 await check_results({ 522 context: createContext("ramen", { 523 providers: [UrlbarProviderQuickSuggest.name], 524 isPrivate: false, 525 }), 526 matches: [ 527 makeExpectedResult({ 528 url: "https://www.yelp.com/search?find_desc=ramen&find_loc=Yokohama%2C+Kanagawa", 529 title: "ramen in Yokohama, Kanagawa", 530 }), 531 ], 532 }); 533 534 info("Clearing dismissed suggestions"); 535 await QuickSuggest.clearDismissedSuggestions(); 536 537 info("Doing search for un-dismissed suggestion"); 538 gMakeSuggestionsStub.returns(burgersIntent); 539 await check_results({ 540 context: createContext("burgers", { 541 providers: [UrlbarProviderQuickSuggest.name], 542 isPrivate: false, 543 }), 544 matches: [result], 545 }); 546 gMakeSuggestionsStub.returns(waterlooIntent); 547 await check_results({ 548 context: createContext("burgers in waterloo", { 549 providers: [UrlbarProviderQuickSuggest.name], 550 isPrivate: false, 551 }), 552 matches: [makeExpectedResult(WATERLOO_RESULT)], 553 }); 554 }); 555 556 function makeExpectedResult({ 557 url, 558 title, 559 source = "ml", 560 provider = "yelp_intent", 561 originalUrl = undefined, 562 // Expect index -1 for amp results because we test 563 // without the search suggestions provider. 564 suggestedIndex = -1, 565 }) { 566 return QuickSuggestTestUtils.yelpResult({ 567 url, 568 title, 569 source, 570 provider, 571 originalUrl, 572 suggestedIndex, 573 }); 574 }