head.js (38451B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { XPCOMUtils } = ChromeUtils.importESModule( 5 "resource://gre/modules/XPCOMUtils.sys.mjs" 6 ); 7 const { AppConstants } = ChromeUtils.importESModule( 8 "resource://gre/modules/AppConstants.sys.mjs" 9 ); 10 11 var { UrlbarMuxer, UrlbarProvider, UrlbarQueryContext, UrlbarUtils } = 12 ChromeUtils.importESModule( 13 "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs" 14 ); 15 16 ChromeUtils.defineESModuleGetters(this, { 17 HttpServer: "resource://testing-common/httpd.sys.mjs", 18 PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", 19 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 20 SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", 21 TestUtils: "resource://testing-common/TestUtils.sys.mjs", 22 UrlbarController: 23 "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs", 24 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 25 UrlbarProviderOpenTabs: 26 "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs", 27 UrlbarProvidersManager: 28 "moz-src:///browser/components/urlbar/UrlbarProvidersManager.sys.mjs", 29 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 30 UrlbarTokenizer: 31 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 32 sinon: "resource://testing-common/Sinon.sys.mjs", 33 }); 34 35 ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { 36 const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( 37 "resource://testing-common/QuickSuggestTestUtils.sys.mjs" 38 ); 39 module.init(this); 40 return module; 41 }); 42 43 ChromeUtils.defineLazyGetter(this, "MerinoTestUtils", () => { 44 const { MerinoTestUtils: module } = ChromeUtils.importESModule( 45 "resource://testing-common/MerinoTestUtils.sys.mjs" 46 ); 47 module.init(this); 48 return module; 49 }); 50 51 ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { 52 const { UrlbarTestUtils: module } = ChromeUtils.importESModule( 53 "resource://testing-common/UrlbarTestUtils.sys.mjs" 54 ); 55 module.init(this); 56 return module; 57 }); 58 59 ChromeUtils.defineLazyGetter(this, "GeolocationTestUtils", () => { 60 const { GeolocationTestUtils: module } = ChromeUtils.importESModule( 61 "resource://testing-common/GeolocationTestUtils.sys.mjs" 62 ); 63 module.init(this); 64 return module; 65 }); 66 67 ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { 68 return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( 69 Ci.nsIObserver 70 ).wrappedJSObject; 71 }); 72 73 // Most tests need a profile to be setup, so do that here. 74 do_get_profile(); 75 76 SearchTestUtils.init(this); 77 78 const SUGGESTIONS_ENGINE_NAME = "Suggestions"; 79 const TAIL_SUGGESTIONS_ENGINE_NAME = "Tail Suggestions"; 80 81 const SEARCH_GLASS_ICON = "chrome://global/skin/icons/search-glass.svg"; 82 83 /** 84 * Gets the database connection. If the Places connection is invalid it will 85 * try to create a new connection. 86 * 87 * @param [optional] aForceNewConnection 88 * Forces creation of a new connection to the database. When a 89 * connection is asyncClosed it cannot anymore schedule async statements, 90 * though connectionReady will keep returning true (Bug 726990). 91 * 92 * @returns The database connection or null if unable to get one. 93 */ 94 var gDBConn; 95 function DBConn(aForceNewConnection) { 96 if (!aForceNewConnection) { 97 let db = PlacesUtils.history.DBConnection; 98 if (db.connectionReady) { 99 return db; 100 } 101 } 102 103 // If the Places database connection has been closed, create a new connection. 104 if (!gDBConn || aForceNewConnection) { 105 let file = Services.dirsvc.get("ProfD", Ci.nsIFile); 106 file.append("places.sqlite"); 107 let dbConn = (gDBConn = Services.storage.openDatabase(file)); 108 109 TestUtils.topicObserved("profile-before-change").then(() => 110 dbConn.asyncClose() 111 ); 112 } 113 114 return gDBConn.connectionReady ? gDBConn : null; 115 } 116 117 /** 118 * @param {string} searchString The search string to insert into the context. 119 * @param {object} properties Overrides for the default values. 120 * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled 121 * required options. 122 */ 123 function createContext(searchString = "foo", properties = {}) { 124 info(`Creating new queryContext with searchString: ${searchString}`); 125 let context = new UrlbarQueryContext( 126 Object.assign( 127 { 128 allowAutofill: UrlbarPrefs.get("autoFill"), 129 isPrivate: true, 130 maxResults: UrlbarPrefs.get("maxRichResults"), 131 sapName: "urlbar", 132 searchString, 133 }, 134 properties 135 ) 136 ); 137 let tokens = UrlbarTokenizer.tokenize(context); 138 context.tokens = tokens; 139 return context; 140 } 141 142 /** 143 * Waits for the given notification from the supplied controller. 144 * 145 * @param {UrlbarController} controller The controller to wait for a response from. 146 * @param {string} notification The name of the notification to wait for. 147 * @param {boolean} expected Wether the notification is expected. 148 * @returns {Promise} A promise that is resolved with the arguments supplied to 149 * the notification. 150 */ 151 function promiseControllerNotification( 152 controller, 153 notification, 154 expected = true 155 ) { 156 return new Promise((resolve, reject) => { 157 let proxifiedObserver = new Proxy( 158 {}, 159 { 160 get: (target, name) => { 161 if (name == notification) { 162 return (...args) => { 163 controller.removeListener(proxifiedObserver); 164 if (expected) { 165 resolve(args); 166 } else { 167 reject(); 168 } 169 }; 170 } 171 return () => false; 172 }, 173 } 174 ); 175 controller.addListener(proxifiedObserver); 176 }); 177 } 178 179 function convertToUtf8(str) { 180 return String.fromCharCode(...new TextEncoder().encode(str)); 181 } 182 183 /** 184 * Helper function to clear the existing providers and register a basic provider 185 * that returns only the results given. 186 * 187 * @param {Array} results The results for the provider to return. 188 * @param {Function} [onCancel] Optional, called when the query provider 189 * receives a cancel instruction. 190 * @param {UrlbarUtils.PROVIDER_TYPE} type The provider type. 191 * @param {string} [name] Optional, use as the provider name. 192 * If none, a default name is chosen. 193 * @returns {UrlbarProvider} The provider 194 */ 195 function registerBasicTestProvider(results = [], onCancel, type, name) { 196 let provider = new UrlbarTestUtils.TestProvider({ 197 results, 198 onCancel, 199 type, 200 name, 201 }); 202 UrlbarProvidersManager.registerProvider(provider); 203 registerCleanupFunction(() => 204 UrlbarProvidersManager.unregisterProvider(provider) 205 ); 206 return provider; 207 } 208 209 // Creates an HTTP server for the test. 210 function makeTestServer(port = -1) { 211 let httpServer = new HttpServer(); 212 httpServer.start(port); 213 registerCleanupFunction(() => httpServer.stop(() => {})); 214 return httpServer; 215 } 216 217 /** 218 * Sets up a search engine that provides some suggestions by appending strings 219 * onto the search query. 220 * 221 * @param {Function} suggestionsFn 222 * A function that returns an array of suggestion strings given a 223 * search string. If not given, a default function is used. 224 * @param {object} options 225 * Options for the check. 226 * @param {string} [options.name] 227 * The name of the engine to install. 228 * @returns {nsISearchEngine} The new engine. 229 */ 230 async function addTestSuggestionsEngine( 231 suggestionsFn = null, 232 { name = SUGGESTIONS_ENGINE_NAME } = {} 233 ) { 234 // This port number should match the number in engine-suggestions.xml. 235 let server = makeTestServer(); 236 server.registerPathHandler("/suggest", (req, resp) => { 237 let params = new URLSearchParams(req.queryString); 238 let searchStr = params.get("q"); 239 let suggestions = suggestionsFn 240 ? suggestionsFn(searchStr) 241 : [searchStr].concat(["foo", "bar"].map(s => searchStr + " " + s)); 242 let data = [searchStr, suggestions]; 243 resp.setHeader("Content-Type", "application/json", false); 244 resp.write(JSON.stringify(data)); 245 }); 246 await SearchTestUtils.installSearchExtension({ 247 name, 248 search_url: `http://localhost:${server.identity.primaryPort}/search`, 249 suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`, 250 suggest_url_get_params: "?q={searchTerms}", 251 }); 252 let engine = Services.search.getEngineByName(name); 253 return engine; 254 } 255 256 /** 257 * Sets up a search engine that provides some tail suggestions by creating an 258 * array that mimics Google's tail suggestion responses. 259 * 260 * @param {Function} suggestionsFn 261 * A function that returns an array that mimics Google's tail suggestion 262 * responses. See bug 1626897. 263 * NOTE: Consumers specifying suggestionsFn must include searchStr as a 264 * part of the array returned by suggestionsFn. 265 * @returns {nsISearchEngine} The new engine. 266 */ 267 async function addTestTailSuggestionsEngine(suggestionsFn = null) { 268 // This port number should match the number in engine-tail-suggestions.xml. 269 let server = makeTestServer(); 270 server.registerPathHandler("/suggest", (req, resp) => { 271 let params = new URLSearchParams(req.queryString); 272 let searchStr = params.get("q"); 273 let suggestions = suggestionsFn 274 ? suggestionsFn(searchStr) 275 : [ 276 "what time is it in t", 277 ["what is the time today texas"].concat( 278 ["toronto", "tunisia"].map(s => searchStr + s.slice(1)) 279 ), 280 [], 281 { 282 "google:irrelevantparameter": [], 283 "google:suggestdetail": [{}].concat( 284 ["toronto", "tunisia"].map(s => ({ 285 mp: "… ", 286 t: s, 287 })) 288 ), 289 }, 290 ]; 291 let data = suggestions; 292 let jsonString = JSON.stringify(data); 293 // This script must be evaluated as UTF-8 for this to write out the bytes of 294 // the string in UTF-8. If it's evaluated as Latin-1, the written bytes 295 // will be the result of UTF-8-encoding the result-string *twice*, which 296 // will break the "… " match prefixes. 297 let stringOfUtf8Bytes = convertToUtf8(jsonString); 298 resp.setHeader("Content-Type", "application/json", false); 299 resp.write(stringOfUtf8Bytes); 300 }); 301 await SearchTestUtils.installSearchExtension({ 302 name: TAIL_SUGGESTIONS_ENGINE_NAME, 303 search_url: `http://localhost:${server.identity.primaryPort}/search`, 304 suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`, 305 suggest_url_get_params: "?q={searchTerms}", 306 }); 307 let engine = Services.search.getEngineByName("Tail Suggestions"); 308 return engine; 309 } 310 311 /** 312 * Creates a function that can be provided to the new engine 313 * utility function to mimic a search engine that returns 314 * rich suggestions. 315 * 316 * @param {string} searchStr 317 * The string being searched for. 318 * 319 * @returns {object} 320 * A JSON object mimicing the data format returned by 321 * a search engine. 322 */ 323 function defaultRichSuggestionsFn(searchStr) { 324 let suffixes = ["toronto", "tunisia", "tacoma", "taipei"]; 325 return [ 326 "what time is it in t", 327 suffixes.map(s => searchStr + s.slice(1)), 328 [], 329 { 330 "google:irrelevantparameter": [], 331 "google:suggestdetail": suffixes.map((suffix, i) => { 332 // Set every other suggestion as a rich suggestion so we can 333 // test how they are handled and ordered when interleaved. 334 if (i % 2) { 335 return {}; 336 } 337 return { 338 a: "description", 339 dc: "#FFFFFF", 340 i: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", 341 t: "Title", 342 }; 343 }), 344 }, 345 ]; 346 } 347 348 async function addOpenPages( 349 uri, 350 count = 1, 351 userContextId = 0, 352 tabGroupId = null 353 ) { 354 for (let i = 0; i < count; i++) { 355 await UrlbarProviderOpenTabs.registerOpenTab( 356 uri.spec, 357 userContextId, 358 tabGroupId, 359 false 360 ); 361 } 362 } 363 364 async function removeOpenPages( 365 aUri, 366 aCount = 1, 367 aUserContextId = 0, 368 tabGroupId = null 369 ) { 370 for (let i = 0; i < aCount; i++) { 371 await UrlbarProviderOpenTabs.unregisterOpenTab( 372 aUri.spec, 373 aUserContextId, 374 tabGroupId, 375 false 376 ); 377 } 378 } 379 380 /** 381 * Helper for tests that generate search results but aren't interested in 382 * suggestions, such as autofill tests. Installs a test engine and disables 383 * suggestions. 384 */ 385 function testEngine_setup() { 386 add_setup(async () => { 387 await cleanupPlaces(); 388 let engine = await addTestSuggestionsEngine(); 389 let oldDefaultEngine = await Services.search.getDefault(); 390 391 registerCleanupFunction(async () => { 392 Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); 393 Services.prefs.clearUserPref("browser.urlbar.contextualSearch.enabled"); 394 Services.prefs.clearUserPref( 395 "browser.search.separatePrivateDefault.ui.enabled" 396 ); 397 Services.search.setDefault( 398 oldDefaultEngine, 399 Ci.nsISearchService.CHANGE_REASON_UNKNOWN 400 ); 401 }); 402 403 Services.search.setDefault( 404 engine, 405 Ci.nsISearchService.CHANGE_REASON_UNKNOWN 406 ); 407 Services.prefs.setBoolPref( 408 "browser.search.separatePrivateDefault.ui.enabled", 409 false 410 ); 411 Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); 412 Services.prefs.setBoolPref( 413 "browser.urlbar.scotchBonnet.enableOverride", 414 false 415 ); 416 }); 417 } 418 419 async function cleanupPlaces() { 420 Services.prefs.clearUserPref("browser.urlbar.autoFill"); 421 422 await PlacesUtils.bookmarks.eraseEverything(); 423 await PlacesUtils.history.clear(); 424 } 425 426 /** 427 * Creates a UrlbarResult for a bookmark result. 428 * 429 * @param {UrlbarQueryContext} queryContext 430 * The context that this result will be displayed in. 431 * @param {object} options 432 * Options for the result. 433 * @param {string} [options.title] 434 * The page title. 435 * @param {string} options.uri 436 * The page URI. 437 * @param {string} [options.iconUri] 438 * A URI for the page's icon. 439 * @param {Array} [options.tags] 440 * An array of string tags. Defaults to an empty array. 441 * @param {boolean} [options.heuristic] 442 * True if this is a heuristic result. Defaults to false. 443 * @param {number} [options.source] 444 * Where the results should be sourced from. See {@link UrlbarUtils.RESULT_SOURCE}. 445 * @returns {UrlbarResult} 446 */ 447 function makeBookmarkResult( 448 queryContext, 449 { 450 title, 451 uri, 452 iconUri, 453 tags = [], 454 heuristic = false, 455 source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS, 456 } 457 ) { 458 return new UrlbarResult({ 459 type: UrlbarUtils.RESULT_TYPE.URL, 460 source, 461 heuristic, 462 payload: { 463 url: uri, 464 title, 465 tags, 466 // Check against undefined so consumers can pass in the empty string. 467 icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`, 468 isBlockable: 469 source == UrlbarUtils.RESULT_SOURCE.HISTORY ? true : undefined, 470 blockL10n: 471 source == UrlbarUtils.RESULT_SOURCE.HISTORY 472 ? { id: "urlbar-result-menu-remove-from-history" } 473 : undefined, 474 helpUrl: 475 source == UrlbarUtils.RESULT_SOURCE.HISTORY 476 ? Services.urlFormatter.formatURLPref("app.support.baseURL") + 477 "awesome-bar-result-menu" 478 : undefined, 479 }, 480 highlights: { 481 url: UrlbarUtils.HIGHLIGHT.TYPED, 482 title: UrlbarUtils.HIGHLIGHT.TYPED, 483 tags: UrlbarUtils.HIGHLIGHT.TYPED, 484 }, 485 }); 486 } 487 488 /** 489 * Creates a UrlbarResult for a form history result. 490 * 491 * @param {UrlbarQueryContext} queryContext 492 * The context that this result will be displayed in. 493 * @param {object} options 494 * Options for the result. 495 * @param {string} options.suggestion 496 * The form history suggestion. 497 * @param {string} options.engineName 498 * The name of the engine that will do the search when the result is picked. 499 * @returns {UrlbarResult} 500 */ 501 function makeFormHistoryResult(queryContext, { suggestion, engineName }) { 502 return new UrlbarResult({ 503 type: UrlbarUtils.RESULT_TYPE.SEARCH, 504 source: UrlbarUtils.RESULT_SOURCE.HISTORY, 505 payload: { 506 engine: engineName, 507 suggestion, 508 title: suggestion, 509 lowerCaseSuggestion: suggestion.toLocaleLowerCase(), 510 isBlockable: true, 511 blockL10n: { id: "urlbar-result-menu-remove-from-history" }, 512 helpUrl: 513 Services.urlFormatter.formatURLPref("app.support.baseURL") + 514 "awesome-bar-result-menu", 515 }, 516 highlights: { 517 suggestion: UrlbarUtils.HIGHLIGHT.SUGGESTED, 518 title: UrlbarUtils.HIGHLIGHT.SUGGESTED, 519 }, 520 }); 521 } 522 523 /** 524 * Creates a UrlbarResult for an omnibox extension result. For more information, 525 * see the documentation for omnibox.SuggestResult: 526 * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/omnibox/SuggestResult 527 * 528 * @param {UrlbarQueryContext} queryContext 529 * The context that this result will be displayed in. 530 * @param {object} options 531 * Options for the result. 532 * @param {string} options.content 533 * The string displayed when the result is highlighted. 534 * @param {string} options.description 535 * The string displayed in the address bar dropdown. 536 * @param {string} options.keyword 537 * The keyword associated with the extension returning the result. 538 * @param {boolean} [options.heuristic] 539 * True if this is a heuristic result. Defaults to false. 540 * @returns {UrlbarResult} 541 */ 542 function makeOmniboxResult( 543 queryContext, 544 { content, description, keyword, heuristic = false } 545 ) { 546 return new UrlbarResult({ 547 type: UrlbarUtils.RESULT_TYPE.OMNIBOX, 548 source: UrlbarUtils.RESULT_SOURCE.ADDON, 549 heuristic, 550 payload: { 551 title: description, 552 content, 553 keyword, 554 icon: UrlbarUtils.ICON.EXTENSION, 555 }, 556 highlights: { 557 title: UrlbarUtils.HIGHLIGHT.TYPED, 558 content: UrlbarUtils.HIGHLIGHT.TYPED, 559 keyword: UrlbarUtils.HIGHLIGHT.TYPED, 560 }, 561 }); 562 } 563 564 /** 565 * Creates a UrlbarResult for an switch-to-tab result. 566 * 567 * @param {UrlbarQueryContext} queryContext 568 * The context that this result will be displayed in. 569 * @param {object} options 570 * Options for the result. 571 * @param {string} options.uri 572 * The page URI. 573 * @param {string} [options.title] 574 * The page title. 575 * @param {string} [options.iconUri] 576 * A URI for the page icon. 577 * @param {number} [options.userContextId] 578 * An id of the userContext in which the tab is located. 579 * @param {string} [options.tabGroup] 580 * An id of the tab group in which the tab is located. 581 * @returns {UrlbarResult} 582 */ 583 function makeTabSwitchResult( 584 queryContext, 585 { uri, title, iconUri, userContextId, tabGroup } 586 ) { 587 return new UrlbarResult({ 588 type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH, 589 source: UrlbarUtils.RESULT_SOURCE.TABS, 590 payload: { 591 url: uri, 592 title, 593 // Check against undefined so consumers can pass in the empty string. 594 icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`, 595 userContextId: userContextId || 0, 596 tabGroup, 597 }, 598 highlights: { 599 url: UrlbarUtils.HIGHLIGHT.TYPED, 600 title: UrlbarUtils.HIGHLIGHT.TYPED, 601 }, 602 }); 603 } 604 605 /** 606 * Creates a UrlbarResult for a keyword search result. 607 * 608 * @param {UrlbarQueryContext} queryContext 609 * The context that this result will be displayed in. 610 * @param {object} options 611 * Options for the result. 612 * @param {string} options.uri 613 * The page URI. 614 * @param {string} options.keyword 615 * The page's search keyword. 616 * @param {string} [options.title] 617 * The title for the bookmarked keyword page. 618 * @param {string} [options.iconUri] 619 * A URI for the engine's icon. 620 * @param {string} [options.postData] 621 * The search POST data. 622 * @param {boolean} [options.heuristic] 623 * True if this is a heuristic result. Defaults to false. 624 * @returns {UrlbarResult} 625 */ 626 function makeKeywordSearchResult( 627 queryContext, 628 { uri, keyword, title, iconUri, postData, heuristic = false } 629 ) { 630 return new UrlbarResult({ 631 type: UrlbarUtils.RESULT_TYPE.KEYWORD, 632 source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, 633 heuristic, 634 payload: { 635 title: title || uri, 636 url: uri, 637 keyword, 638 input: queryContext.searchString, 639 postData: postData || null, 640 icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`, 641 }, 642 highlights: { 643 title: UrlbarUtils.HIGHLIGHT.TYPED, 644 url: UrlbarUtils.HIGHLIGHT.TYPED, 645 keyword: UrlbarUtils.HIGHLIGHT.TYPED, 646 input: UrlbarUtils.HIGHLIGHT.TYPED, 647 }, 648 }); 649 } 650 651 /** 652 * Creates a UrlbarResult for a remote tab result. 653 * 654 * @param {UrlbarQueryContext} queryContext 655 * The context that this result will be displayed in. 656 * @param {object} options 657 * Options for the result. 658 * @param {string} options.uri 659 * The page URI. 660 * @param {string} options.device 661 * The name of the device that the remote tab comes from. 662 * @param {string} [options.title] 663 * The page title. 664 * @param {number} [options.lastUsed] 665 * The last time the remote tab was visited, in epoch seconds. Defaults 666 * to 0. 667 * @param {string} [options.iconUri] 668 * A URI for the page's icon. 669 * @returns {UrlbarResult} 670 */ 671 function makeRemoteTabResult( 672 queryContext, 673 { uri, device, title, iconUri, lastUsed = 0 } 674 ) { 675 let payload = { 676 url: uri, 677 device, 678 // Check against undefined so consumers can pass in the empty string. 679 icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`, 680 lastUsed: lastUsed * 1000, 681 }; 682 683 // Check against undefined so consumers can pass in the empty string. 684 if (typeof title != "undefined") { 685 payload.title = title; 686 } else { 687 payload.title = uri; 688 } 689 690 return new UrlbarResult({ 691 type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB, 692 source: UrlbarUtils.RESULT_SOURCE.TABS, 693 payload, 694 highlights: { 695 url: UrlbarUtils.HIGHLIGHT.TYPED, 696 title: UrlbarUtils.HIGHLIGHT.TYPED, 697 device: UrlbarUtils.HIGHLIGHT.TYPED, 698 }, 699 }); 700 } 701 702 /** 703 * Creates a UrlbarResult for a search result. 704 * 705 * @param {UrlbarQueryContext} queryContext 706 * The context that this result will be displayed in. 707 * @param {object} options 708 * Options for the result. 709 * @param {string} [options.suggestion] 710 * The suggestion offered by the search engine. 711 * @param {string} [options.tailPrefix] 712 * The characters placed at the end of a Google "tail" suggestion. See 713 * {@link https://firefox-source-docs.mozilla.org/browser/urlbar/nontechnical-overview.html#search-suggestions} 714 * @param {*} [options.tail] 715 * The details of the URL bar tail 716 * @param {number} [options.tailOffsetIndex] 717 * The index of the first character in the tail suggestion that should be 718 * @param {string} [options.engineName] 719 * The name of the engine providing the suggestion. Leave blank if there 720 * is no suggestion. 721 * @param {string} [options.uri] 722 * The URI that the search result will navigate to. 723 * @param {string} [options.query] 724 * The query that started the search. This overrides 725 * `queryContext.searchString`. This is useful when the query that will show 726 * up in the result object will be different from what was typed. For example, 727 * if a leading restriction token will be used. 728 * @param {string} [options.alias] 729 * The alias for the search engine, if the search is an alias search. 730 * @param {string} [options.engineIconUri] 731 * A URI for the engine's icon. 732 * @param {boolean} [options.heuristic] 733 * True if this is a heuristic result. Defaults to false. 734 * @param {boolean} [options.providesSearchMode] 735 * Whether search mode is entered when this result is selected. 736 * @param {string} [options.providerName] 737 * The name of the provider offering this result. The test suite will not 738 * check which provider offered a result unless this option is specified. 739 * @param {boolean} [options.inPrivateWindow] 740 * If the window to test is a private window. 741 * @param {boolean} [options.isPrivateEngine] 742 * If the engine is a private engine. 743 * @param {string} [options.searchUrlDomainWithoutSuffix] 744 * For tab-to-search results, the search engine domain without the public 745 * suffix. 746 * @param {number} [options.type] 747 * The type of the search result. Defaults to UrlbarUtils.RESULT_TYPE.SEARCH. 748 * @param {number} [options.source] 749 * The source of the search result. Defaults to UrlbarUtils.RESULT_SOURCE.SEARCH. 750 * @param {boolean} [options.satisfiesAutofillThreshold] 751 * If this search should appear in the autofill section of the box 752 * @param {boolean} [options.trending] 753 * If the search result is a trending result. `Defaults to false`. 754 * @param {boolean} [options.isRichSuggestion] 755 * If the search result is a rich result. `Defaults to false`. 756 * @returns {UrlbarResult} 757 */ 758 function makeSearchResult( 759 queryContext, 760 { 761 suggestion, 762 tailPrefix, 763 tail, 764 tailOffsetIndex, 765 engineName, 766 alias, 767 uri, 768 query, 769 engineIconUri, 770 providesSearchMode, 771 providerName, 772 inPrivateWindow, 773 isPrivateEngine, 774 searchUrlDomainWithoutSuffix, 775 heuristic = false, 776 trending = false, 777 isRichSuggestion = false, 778 type = UrlbarUtils.RESULT_TYPE.SEARCH, 779 source = UrlbarUtils.RESULT_SOURCE.SEARCH, 780 satisfiesAutofillThreshold = false, 781 } 782 ) { 783 // Tail suggestion common cases, handled here to reduce verbosity in tests. 784 if (tail) { 785 if (!tailPrefix && !isRichSuggestion) { 786 tailPrefix = "… "; 787 } 788 if (!tailOffsetIndex) { 789 tailOffsetIndex = suggestion.indexOf(tail); 790 } 791 } 792 793 let payload = { 794 engine: engineName, 795 suggestion, 796 tailPrefix, 797 tail, 798 tailOffsetIndex, 799 keyword: alias, 800 // Check against undefined so consumers can pass in the empty string. 801 query: 802 typeof query != "undefined" ? query : queryContext.trimmedSearchString, 803 icon: engineIconUri, 804 providesSearchMode, 805 inPrivateWindow, 806 isPrivateEngine, 807 }; 808 809 if (providesSearchMode) { 810 // No title. 811 } else if (payload.tail && payload.tailOffsetIndex >= 0) { 812 payload.title = payload.tail; 813 } else if (payload.suggestion != undefined) { 814 payload.title = payload.suggestion; 815 } else if (payload.query != undefined) { 816 payload.title = payload.query; 817 } 818 819 if (uri) { 820 payload.url = uri; 821 } 822 823 if (providerName == "UrlbarProviderTabToSearch") { 824 if (searchUrlDomainWithoutSuffix.startsWith("www.")) { 825 searchUrlDomainWithoutSuffix = searchUrlDomainWithoutSuffix.substring(4); 826 } 827 payload.searchUrlDomainWithoutSuffix = searchUrlDomainWithoutSuffix; 828 payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold; 829 payload.isGeneralPurposeEngine = false; 830 } 831 832 if (providerName == "UrlbarProviderTokenAliasEngines") { 833 payload.keywords = alias?.toLowerCase(); 834 } 835 836 if (typeof suggestion == "string") { 837 payload.lowerCaseSuggestion = suggestion.toLocaleLowerCase(); 838 payload.trending = trending; 839 } 840 841 if (isRichSuggestion) { 842 payload.icon = 843 "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; 844 payload.description = "description"; 845 } 846 847 return new UrlbarResult({ 848 type, 849 source, 850 heuristic, 851 isRichSuggestion, 852 providerName, 853 payload, 854 highlights: { 855 engine: UrlbarUtils.HIGHLIGHT.TYPED, 856 suggestion: UrlbarUtils.HIGHLIGHT.SUGGESTED, 857 tail: UrlbarUtils.HIGHLIGHT.SUGGESTED, 858 keyword: providesSearchMode ? UrlbarUtils.HIGHLIGHT.TYPED : undefined, 859 query: UrlbarUtils.HIGHLIGHT.TYPED, 860 }, 861 }); 862 } 863 864 /** 865 * Creates a UrlbarResult for a history result. 866 * 867 * @param {UrlbarQueryContext} queryContext 868 * The context that this result will be displayed in. 869 * @param {object} options Options for the result. 870 * @param {string} options.title 871 * The page title. 872 * @param {string} options.uri 873 * The page URI. 874 * @param {Array} [options.tags] 875 * An array of string tags. Defaults to an empty array. 876 * @param {string} [options.iconUri] 877 * A URI for the page's icon. 878 * @param {boolean} [options.heuristic] 879 * True if this is a heuristic result. Defaults to false. 880 * @param {string} options.providerName 881 * The name of the provider offering this result. The test suite will not 882 * check which provider offered a result unless this option is specified. 883 * @param {number} [options.source] 884 * The source of the result 885 * @returns {UrlbarResult} 886 */ 887 function makeVisitResult( 888 queryContext, 889 { 890 title, 891 uri, 892 iconUri, 893 providerName, 894 tags = [], 895 heuristic = false, 896 source = UrlbarUtils.RESULT_SOURCE.HISTORY, 897 } 898 ) { 899 let payload = { 900 url: uri, 901 }; 902 903 if (title != undefined) { 904 payload.title = title; 905 } 906 907 if ( 908 !heuristic && 909 providerName != "UrlbarProviderAboutPages" && 910 source == UrlbarUtils.RESULT_SOURCE.HISTORY 911 ) { 912 payload.isBlockable = true; 913 payload.blockL10n = { id: "urlbar-result-menu-remove-from-history" }; 914 payload.helpUrl = 915 Services.urlFormatter.formatURLPref("app.support.baseURL") + 916 "awesome-bar-result-menu"; 917 } 918 919 if (iconUri) { 920 payload.icon = iconUri; 921 } else if ( 922 iconUri === undefined && 923 source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL 924 ) { 925 payload.icon = `page-icon:${uri}`; 926 } 927 928 if (!heuristic && tags) { 929 payload.tags = tags; 930 } 931 932 return new UrlbarResult({ 933 type: UrlbarUtils.RESULT_TYPE.URL, 934 source, 935 heuristic, 936 providerName, 937 payload, 938 highlights: { 939 url: UrlbarUtils.HIGHLIGHT.TYPED, 940 title: UrlbarUtils.HIGHLIGHT.TYPED, 941 fallbackTitle: UrlbarUtils.HIGHLIGHT.TYPED, 942 tags: UrlbarUtils.HIGHLIGHT.TYPED, 943 }, 944 }); 945 } 946 947 /** 948 * Creates a UrlbarResult for a calculator result. 949 * 950 * @param {UrlbarQueryContext} queryContext 951 * The context that this result will be displayed in. 952 * @param {object} options 953 * Options for the result. 954 * @param {string} options.value 955 * The value of the calculator result. 956 * @returns {UrlbarResult} 957 */ 958 function makeCalculatorResult(queryContext, { value }) { 959 return new UrlbarResult({ 960 type: UrlbarUtils.RESULT_TYPE.DYNAMIC, 961 source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, 962 payload: { 963 value, 964 input: queryContext.searchString, 965 dynamicType: "calculator", 966 }, 967 }); 968 } 969 970 /** 971 * Creates a UrlbarResult for a global action result. 972 * 973 * @param {object} options 974 * @param {Array} [options.actionsResults] 975 * An array of action descriptors. 976 * @param {string} [options.query] 977 * The query passed to actions when they run. 978 * It is "" for contextual search, otherwise it is queryContext.searchString. 979 * @param {number} [options.inputLength] 980 * Original input length. 981 * @param {boolean} [options.showOnboardingLabel] 982 * Whether the “press Tab” hint should appear. 983 * @returns {UrlbarResult} 984 */ 985 function makeGlobalActionsResult({ 986 actionsResults, 987 query, 988 inputLength, 989 showOnboardingLabel = false, 990 }) { 991 const payload = { 992 actionsResults, 993 dynamicType: "actions", 994 query, 995 input: query, 996 inputLength, 997 showOnboardingLabel, 998 }; 999 1000 return new UrlbarResult({ 1001 type: UrlbarUtils.RESULT_TYPE.DYNAMIC, 1002 source: UrlbarUtils.RESULT_SOURCE.ACTIONS, 1003 providerName: "UrlbarProviderGlobalActions", 1004 payload, 1005 }); 1006 } 1007 1008 /** 1009 * Checks that the results returned by a UrlbarController match those in 1010 * the param `matches`. 1011 * 1012 * @param {object} options Options for the check. 1013 * @param {UrlbarQueryContext} options.context 1014 * The context for this query. 1015 * @param {string} [options.incompleteSearch] 1016 * A search will be fired for this string and then be immediately canceled by 1017 * the query in `context`. 1018 * @param {string} [options.autofilled] 1019 * The autofilled value in the first result. 1020 * @param {string} [options.completed] 1021 * The value that would be filled if the autofill result was confirmed. 1022 * Has no effect if `autofilled` is not specified. 1023 * @param {object} [options.conditionalPayloadProperties] 1024 * An object mapping payload property names to objects { optional, ignore }. 1025 * See the code below. 1026 * @param {Array} options.matches 1027 * An array of UrlbarResults. 1028 */ 1029 async function check_results({ 1030 context, 1031 incompleteSearch, 1032 autofilled, 1033 completed, 1034 matches = [], 1035 conditionalPayloadProperties = {}, 1036 } = {}) { 1037 if (!context) { 1038 return; 1039 } 1040 1041 // At this point frecency could still be updating due to latest pages 1042 // updates. 1043 // This is not a problem in real life, but autocomplete tests should 1044 // return reliable resultsets, thus we have to wait. 1045 await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); 1046 1047 const controller = UrlbarTestUtils.newMockController({ 1048 input: { 1049 isPrivate: context.isPrivate, 1050 onFirstResult() { 1051 return false; 1052 }, 1053 getSearchSource() { 1054 return "dummy-search-source"; 1055 }, 1056 window: { 1057 location: { 1058 href: AppConstants.BROWSER_CHROME_URL, 1059 }, 1060 }, 1061 }, 1062 }); 1063 controller.setView({ 1064 get visibleResults() { 1065 return context.results; 1066 }, 1067 controller: { 1068 removeResult() {}, 1069 }, 1070 }); 1071 1072 if (incompleteSearch) { 1073 let incompleteContext = createContext(incompleteSearch, { 1074 isPrivate: context.isPrivate, 1075 }); 1076 controller.startQuery(incompleteContext); 1077 } 1078 await controller.startQuery(context); 1079 1080 if (autofilled) { 1081 Assert.ok(context.results[0], "There is a first result."); 1082 Assert.ok( 1083 context.results[0].autofill, 1084 "The first result is an autofill result" 1085 ); 1086 Assert.equal( 1087 context.results[0].autofill.value, 1088 autofilled, 1089 "The correct value was autofilled." 1090 ); 1091 if (completed) { 1092 Assert.equal( 1093 context.results[0].payload.url, 1094 completed, 1095 "The completed autofill value is correct." 1096 ); 1097 } 1098 } 1099 if (context.results.length != matches.length) { 1100 info("Actual results: " + JSON.stringify(context.results)); 1101 } 1102 Assert.equal( 1103 context.results.length, 1104 matches.length, 1105 "Found the expected number of results." 1106 ); 1107 1108 let propertiesToCheck = { 1109 type: {}, 1110 source: {}, 1111 heuristic: {}, 1112 isBestMatch: { map: v => !!v }, 1113 providerName: { optional: true }, 1114 suggestedIndex: { optional: true }, 1115 isSuggestedIndexRelativeToGroup: { optional: true, map: v => !!v }, 1116 exposureTelemetry: { optional: true }, 1117 isRichSuggestion: { optional: true }, 1118 richSuggestionIconVariation: { optional: true }, 1119 }; 1120 1121 // Payload properties to conditionally check. Properties not specified here 1122 // will always be checked. 1123 // 1124 // ignore: 1125 // Always ignore the property. 1126 // optional: 1127 // Ignore the property if it's not in the expected result. 1128 conditionalPayloadProperties = { 1129 frecency: { optional: true }, 1130 lastVisit: { optional: true }, 1131 // `suggestionObject` is only used for dismissing Suggest Rust results, and 1132 // important properties in this object are reflected in the top-level 1133 // payload object, so ignore it. There are Suggest tests specifically for 1134 // dismissals that indirectly test the important aspects of this property. 1135 suggestionObject: { ignore: true }, 1136 ...conditionalPayloadProperties, 1137 }; 1138 1139 for (let i = 0; i < matches.length; i++) { 1140 let actual = context.results[i]; 1141 let expected = matches[i]; 1142 info( 1143 `Comparing results at index ${i}:` + 1144 " actual=" + 1145 JSON.stringify(actual) + 1146 " expected=" + 1147 JSON.stringify(expected) 1148 ); 1149 1150 for (let [key, { optional, map }] of Object.entries(propertiesToCheck)) { 1151 if (!optional || expected[key] !== undefined) { 1152 map ??= v => v; 1153 Assert.equal( 1154 map(actual[key]), 1155 map(expected[key]), 1156 `result.${key} at result index ${i}` 1157 ); 1158 } 1159 } 1160 1161 if ( 1162 actual.type == UrlbarUtils.RESULT_TYPE.SEARCH && 1163 actual.source == UrlbarUtils.RESULT_SOURCE.SEARCH && 1164 actual.providerName == "UrlbarProviderHeuristicFallback" 1165 ) { 1166 expected.payload.icon = SEARCH_GLASS_ICON; 1167 } 1168 1169 if (actual.payload?.url) { 1170 try { 1171 const payloadUrlProtocol = new URL(actual.payload.url).protocol; 1172 if ( 1173 !UrlbarUtils.PROTOCOLS_WITH_ICONS.includes(payloadUrlProtocol) && 1174 actual.source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL 1175 ) { 1176 expected.payload.icon = UrlbarUtils.ICON.DEFAULT; 1177 } 1178 } catch (e) { 1179 console.error(e); 1180 } 1181 } 1182 1183 if (expected.payload) { 1184 let expectedKeys = new Set(Object.keys(expected.payload)); 1185 let actualKeys = new Set(Object.keys(actual.payload)); 1186 1187 for (let key of actualKeys.union(expectedKeys)) { 1188 let condition = conditionalPayloadProperties[key]; 1189 if ( 1190 condition?.ignore || 1191 (condition?.optional && !expected.hasOwnProperty(key)) 1192 ) { 1193 continue; 1194 } 1195 1196 Assert.deepEqual( 1197 actual.payload[key], 1198 expected.payload[key], 1199 `result.payload.${key} at result index ${i}` 1200 ); 1201 } 1202 } 1203 } 1204 } 1205 1206 /** 1207 * Returns the frecency of an origin. 1208 * 1209 * @param {string} prefix 1210 * The origin's prefix, e.g., "http://". 1211 * @param {string} aHost 1212 * The origin's host. 1213 * @returns {number} The origin's frecency. 1214 */ 1215 async function getOriginFrecency(prefix, aHost) { 1216 let db = await PlacesUtils.promiseDBConnection(); 1217 let rows = await db.execute( 1218 ` 1219 SELECT frecency 1220 FROM moz_origins 1221 WHERE prefix = :prefix AND host = :host 1222 `, 1223 { prefix, host: aHost } 1224 ); 1225 Assert.equal(rows.length, 1); 1226 return rows[0].getResultByIndex(0); 1227 } 1228 1229 /** 1230 * Returns the origin autofill frecency threshold. 1231 * 1232 * @returns {number} 1233 * The threshold. 1234 */ 1235 async function getOriginAutofillThreshold() { 1236 return PlacesUtils.metadata.get("origin_frecency_threshold", 2.0); 1237 } 1238 1239 /** 1240 * Checks that origins appear in a given order in the database. 1241 * 1242 * @param {string} host The "fixed" host, without "www." 1243 * @param {Array} prefixOrder The prefixes (scheme + www.) sorted appropriately. 1244 */ 1245 async function checkOriginsOrder(host, prefixOrder) { 1246 await PlacesUtils.withConnectionWrapper("checkOriginsOrder", async db => { 1247 let prefixes = ( 1248 await db.execute( 1249 `SELECT prefix || iif(instr(host, "www.") = 1, "www.", "") 1250 FROM moz_origins 1251 WHERE host = :host OR host = "www." || :host 1252 ORDER BY ROWID ASC 1253 `, 1254 { host } 1255 ) 1256 ).map(r => r.getResultByIndex(0)); 1257 Assert.deepEqual(prefixes, prefixOrder); 1258 }); 1259 } 1260 1261 function daysAgo(days) { 1262 let date = new Date(); 1263 date.setDate(date.getDate() - days); 1264 return date; 1265 }