UrlbarTestUtils.sys.mjs (58531B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 5 6 import { 7 UrlbarProvider, 8 UrlbarUtils, 9 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"; 10 11 const lazy = {}; 12 13 ChromeUtils.defineESModuleGetters(lazy, { 14 BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs", 15 BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs", 16 DEFAULT_FORM_HISTORY_PARAM: 17 "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs", 18 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", 19 FormHistoryTestUtils: 20 "resource://testing-common/FormHistoryTestUtils.sys.mjs", 21 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 22 NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs", 23 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 24 TestUtils: "resource://testing-common/TestUtils.sys.mjs", 25 UrlbarController: 26 "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs", 27 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 28 UrlbarSearchUtils: 29 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 30 setTimeout: "resource://gre/modules/Timer.sys.mjs", 31 }); 32 33 /** 34 * Utility class for testing <html:moz-urlbar> elements. 35 */ 36 class UrlbarInputTestUtils { 37 /** 38 * @param {(window: ChromeWindow) => UrlbarInput} getUrlbarInputForWindow 39 */ 40 constructor(getUrlbarInputForWindow) { 41 this.#urlbar = getUrlbarInputForWindow; 42 } 43 44 #urlbar; 45 46 /** 47 * This maps the categories used by the FX_SEARCHBAR_SELECTED_RESULT_METHOD 48 * histogram to its indexes in the `labels` array. This only needs to be 49 * used by tests that need to map from category names to indexes in histogram 50 * snapshots. Actual app code can use these category names directly when 51 * they add to a histogram. 52 */ 53 SELECTED_RESULT_METHODS = { 54 enter: 0, 55 enterSelection: 1, 56 click: 2, 57 arrowEnterSelection: 3, 58 tabEnterSelection: 4, 59 rightClickEnter: 5, 60 }; 61 62 // Fallback to the console. 63 info = console.log; 64 65 /** 66 * Running this init allows helpers to access test scope helpers, like Assert 67 * and SimpleTest. Note this initialization is not enforced, thus helpers 68 * should always check the properties set here and provide a fallback path. 69 * 70 * @param {object} scope The global scope where tests are being run. 71 */ 72 init(scope) { 73 if (!scope) { 74 throw new Error("Must initialize UrlbarInputTestUtils with a test scope"); 75 } 76 // If you add other properties to `this`, null them in uninit(). 77 this.Assert = scope.Assert; 78 this.info = scope.info; 79 this.registerCleanupFunction = scope.registerCleanupFunction; 80 81 if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { 82 this.initXPCShellDependencies(); 83 } else { 84 // xpcshell doesn't support EventUtils. 85 this.EventUtils = scope.EventUtils; 86 this.SimpleTest = scope.SimpleTest; 87 } 88 89 this.registerCleanupFunction(() => { 90 this.Assert = null; 91 this.info = console.log; 92 this.registerCleanupFunction = null; 93 this.EventUtils = null; 94 this.SimpleTest = null; 95 }); 96 } 97 98 /** 99 * Waits to a search to be complete. 100 * 101 * @param {ChromeWindow} win The window containing the urlbar 102 */ 103 async promiseSearchComplete(win) { 104 let waitForQuery = () => { 105 return this.promisePopupOpen(win, () => {}).then( 106 () => this.#urlbar(win).lastQueryContextPromise 107 ); 108 }; 109 /** @type {UrlbarQueryContext} */ 110 let context = await waitForQuery(); 111 if (this.#urlbar(win).searchMode) { 112 // Search mode may start a second query. 113 context = await waitForQuery(); 114 } 115 if (this.#urlbar(win).view.oneOffSearchButtons?._rebuilding) { 116 await new Promise(resolve => 117 this.#urlbar(win).view.oneOffSearchButtons.addEventListener( 118 "rebuild", 119 resolve, 120 { 121 once: true, 122 } 123 ) 124 ); 125 } 126 return context; 127 } 128 129 /** 130 * Starts a search for a given string and waits for the search to be complete. 131 * 132 * @param {object} options The options object. 133 * @param {ChromeWindow} options.window The window containing the urlbar 134 * @param {string} options.value the search string 135 * @param {Function} options.waitForFocus The SimpleTest function 136 * @param {boolean} [options.fireInputEvent] whether an input event should be 137 * used when starting the query (simulates the user's typing, sets 138 * userTypedValued, triggers engagement event telemetry, etc.) 139 * @param {number} [options.selectionStart] The input's selectionStart 140 * @param {number} [options.selectionEnd] The input's selectionEnd 141 * @param {boolean} [options.reopenOnBlur] Whether this method should repoen 142 * the view if the input is blurred before the query finishes. This is 143 * necessary to work around spurious blurs in CI, which close the view 144 * and cancel the query, defeating the typical use of this method where 145 * your test waits for the query to finish. However, this behavior 146 * isn't always desired, for example if your test intentionally blurs 147 * the input before the query finishes. In that case, pass false. 148 * @returns {Promise} 149 * The promise for the last query context. 150 */ 151 async promiseAutocompleteResultPopup({ 152 window, 153 value, 154 waitForFocus, 155 fireInputEvent = true, 156 selectionStart = -1, 157 selectionEnd = -1, 158 reopenOnBlur = true, 159 }) { 160 if (this.SimpleTest) { 161 await this.SimpleTest.promiseFocus(window); 162 } else { 163 await new Promise(resolve => waitForFocus(resolve, window)); 164 } 165 166 const setup = () => { 167 this.#urlbar(window).focus(); 168 // Using the value setter in some cases may trim and fetch unexpected 169 // results, then pick an alternate path. 170 if ( 171 lazy.UrlbarPrefs.get("trimURLs") && 172 value != lazy.BrowserUIUtils.trimURL(value) 173 ) { 174 this.#urlbar(window)._setValue(value); 175 fireInputEvent = true; 176 } else { 177 this.#urlbar(window).value = value; 178 } 179 if (selectionStart >= 0 && selectionEnd >= 0) { 180 this.#urlbar(window).selectionEnd = selectionEnd; 181 this.#urlbar(window).selectionStart = selectionStart; 182 } 183 184 // An input event will start a new search, so be careful not to start a 185 // search if we fired an input event since that would start two searches. 186 if (fireInputEvent) { 187 // This is necessary to get the urlbar to set gBrowser.userTypedValue. 188 this.fireInputEvent(window); 189 } else { 190 this.#urlbar(window).setPageProxyState("invalid"); 191 this.#urlbar(window).startQuery(); 192 } 193 }; 194 setup(); 195 196 // In Linux TV test, as there is case that the input field lost the focus 197 // until showing popup, timeout failure happens since the expected poup 198 // never be shown. To avoid this, if losing the focus, retry setup to open 199 // popup. 200 if (reopenOnBlur) { 201 this.#urlbar(window).inputField.addEventListener("blur", setup, { 202 once: true, 203 }); 204 } 205 const result = await this.promiseSearchComplete(window); 206 if (reopenOnBlur) { 207 this.#urlbar(window).inputField.removeEventListener("blur", setup); 208 } 209 return result; 210 } 211 212 /** 213 * Waits for a result to be added at a certain index. Since we implement lazy 214 * results replacement, even if we have a result at an index, it may be 215 * related to the previous query, this methods ensures the result is current. 216 * 217 * @param {ChromeWindow} win The window containing the urlbar 218 * @param {number} index The index to look for 219 * @throws {Error} When the index exceeds the number of available results 220 */ 221 async waitForAutocompleteResultAt(win, index) { 222 // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement. 223 await this.promiseSearchComplete(win); 224 let container = this.getResultsContainer(win); 225 if (index >= container.children.length) { 226 throw new Error("Not enough results"); 227 } 228 return container.children[index]; 229 } 230 231 /** 232 * Returns the oneOffSearchButtons object for the urlbar. 233 * 234 * @param {ChromeWindow} win The window containing the urlbar 235 * @returns {object} The oneOffSearchButtons 236 */ 237 getOneOffSearchButtons(win) { 238 return this.#urlbar(win).view.oneOffSearchButtons; 239 } 240 241 /** 242 * Returns a specific button of a result. 243 * 244 * @param {ChromeWindow} win The window containing the urlbar 245 * @param {string} buttonName The name of the button, e.g. "menu", "0", etc. 246 * @param {number} resultIndex The index of the result 247 * @returns {HTMLSpanElement} The button 248 */ 249 getButtonForResultIndex(win, buttonName, resultIndex) { 250 return this.getRowAt(win, resultIndex).querySelector( 251 `.urlbarView-button-${buttonName}` 252 ); 253 } 254 255 /** 256 * Show the result menu button regardless of the result being hovered or 257 + selected. 258 * 259 * @param {ChromeWindow} win The window containing the urlbar 260 */ 261 disableResultMenuAutohide(win) { 262 let container = this.getResultsContainer(win); 263 let attr = "disable-resultmenu-autohide"; 264 container.toggleAttribute(attr, true); 265 this.registerCleanupFunction?.(() => { 266 container.toggleAttribute(attr, false); 267 }); 268 } 269 270 /** 271 * Opens the result menu of a specific result. 272 * 273 * @param {ChromeWindow} win The window containing the urlbar 274 * @param {object} [options] The options object. 275 * @param {number} [options.resultIndex] The index of the result. Defaults 276 * to the current selected index. 277 * @param {boolean} [options.byMouse] Whether to open the menu by mouse or 278 * keyboard. 279 * @param {string} [options.activationKey] Key to activate the button with, 280 * defaults to KEY_Enter. 281 */ 282 async openResultMenu( 283 win, 284 { 285 resultIndex = this.#urlbar(win).view.selectedRowIndex, 286 byMouse = false, 287 activationKey = "KEY_Enter", 288 } = {} 289 ) { 290 this.Assert?.ok(this.#urlbar(win).view.isOpen, "view should be open"); 291 let menuButton = this.getButtonForResultIndex( 292 win, 293 "result-menu", 294 resultIndex 295 ); 296 this.Assert?.ok( 297 menuButton, 298 `found the menu button at result index ${resultIndex}` 299 ); 300 let promiseMenuOpen = lazy.BrowserTestUtils.waitForEvent( 301 this.#urlbar(win).view.resultMenu, 302 "popupshown" 303 ); 304 if (byMouse) { 305 this.info( 306 `synthesizing mousemove on row to make the menu button visible` 307 ); 308 await this.EventUtils.promiseElementReadyForUserInput( 309 menuButton.closest(".urlbarView-row"), 310 win, 311 this.info 312 ); 313 this.info(`got mousemove, now clicking the menu button`); 314 this.EventUtils.synthesizeMouseAtCenter(menuButton, {}, win); 315 this.info(`waiting for the menu popup to open via mouse`); 316 } else { 317 this.info(`selecting the result at index ${resultIndex}`); 318 while (this.#urlbar(win).view.selectedRowIndex != resultIndex) { 319 this.EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); 320 } 321 if (this.getSelectedElement(win) != menuButton) { 322 this.EventUtils.synthesizeKey("KEY_Tab", {}, win); 323 } 324 this.Assert?.equal( 325 this.getSelectedElement(win), 326 menuButton, 327 `selected the menu button at result index ${resultIndex}` 328 ); 329 this.EventUtils.synthesizeKey(activationKey, {}, win); 330 this.info(`waiting for ${activationKey} to open the menu popup`); 331 } 332 await promiseMenuOpen; 333 this.Assert?.equal( 334 this.#urlbar(win).view.resultMenu.state, 335 "open", 336 "Checking popup state" 337 ); 338 } 339 340 /** 341 * Opens the result menu of a specific result and gets a menu item by either 342 * accesskey or command name. Either `accesskey` or `command` must be given. 343 * 344 * @param {object} options 345 * The options object. 346 * @param {ChromeWindow} options.window 347 * The window containing the urlbar. 348 * @param {string} [options.accesskey] 349 * The access key of the menu item to return. 350 * @param {string} [options.command] 351 * The command name of the menu item to return. 352 * @param {number} [options.resultIndex] 353 * The index of the result. Defaults to the current selected index. 354 * @param {boolean} [options.openByMouse] 355 * Whether to open the menu by mouse or keyboard. 356 * @param {Array} [options.submenuSelectors] 357 * If the command is in the top-level result menu, leave this as an empty 358 * array. If it's in a submenu, set this to an array where each element i is 359 * a selector that can be used to get the i'th menu item that opens a 360 * submenu. 361 * @returns {Promise<XULElement>} 362 * Returns the menu item element. 363 */ 364 async openResultMenuAndGetItem({ 365 window, 366 accesskey, 367 command, 368 resultIndex = this.#urlbar(window).view.selectedRowIndex, 369 openByMouse = false, 370 submenuSelectors = [], 371 }) { 372 await this.openResultMenu(window, { resultIndex, byMouse: openByMouse }); 373 374 // Open the sequence of submenus that contains the item. 375 for (let selector of submenuSelectors) { 376 let menuitem = 377 this.#urlbar(window).view.resultMenu.querySelector(selector); 378 if (!menuitem) { 379 throw new Error("Submenu item not found for selector: " + selector); 380 } 381 382 let promisePopup = lazy.BrowserTestUtils.waitForEvent( 383 this.#urlbar(window).view.resultMenu, 384 "popupshown" 385 ); 386 387 if (AppConstants.platform == "macosx") { 388 // Synthesized clicks don't work in the native Mac menu. 389 this.info( 390 "Calling openMenu() on submenu item with selector: " + selector 391 ); 392 menuitem.openMenu(true); 393 } else { 394 this.info("Clicking submenu item with selector: " + selector); 395 this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, window); 396 } 397 398 this.info("Waiting for submenu popupshown event"); 399 await promisePopup; 400 this.info("Got the submenu popupshown event"); 401 } 402 403 // Now get the item. 404 let menuitem; 405 if (accesskey) { 406 await lazy.BrowserTestUtils.waitForCondition(() => { 407 menuitem = this.#urlbar(window).view.resultMenu.querySelector( 408 `menuitem[accesskey=${accesskey}]` 409 ); 410 return menuitem; 411 }, "Waiting for strings to load"); 412 } else if (command) { 413 menuitem = this.#urlbar(window).view.resultMenu.querySelector( 414 `menuitem[data-command=${command}]` 415 ); 416 } else { 417 throw new Error("accesskey or command must be specified"); 418 } 419 420 return menuitem; 421 } 422 423 /** 424 * Opens the result menu of a specific result and presses an access key to 425 * activate a menu item. 426 * 427 * @param {ChromeWindow} win The window containing the urlbar 428 * @param {string} accesskey The access key to press once the menu is open 429 * @param {object} [options] The options object. 430 * @param {number} [options.resultIndex] The index of the result. Defaults 431 * to the current selected index. 432 * @param {boolean} [options.openByMouse] Whether to open the menu by mouse 433 * or keyboard. 434 */ 435 async openResultMenuAndPressAccesskey( 436 win, 437 accesskey, 438 { 439 resultIndex = this.#urlbar(win).view.selectedRowIndex, 440 openByMouse = false, 441 } = {} 442 ) { 443 let menuitem = await this.openResultMenuAndGetItem({ 444 accesskey, 445 resultIndex, 446 openByMouse, 447 window: win, 448 }); 449 if (!menuitem) { 450 throw new Error("Menu item not found for accesskey: " + accesskey); 451 } 452 453 let promiseCommand = lazy.BrowserTestUtils.waitForEvent( 454 this.#urlbar(win).view.resultMenu, 455 "command" 456 ); 457 458 if (AppConstants.platform == "macosx") { 459 // The native Mac menu doesn't support access keys. 460 this.info("calling doCommand() to activate menu item"); 461 menuitem.doCommand(); 462 this.#urlbar(win).view.resultMenu.hidePopup(true); 463 } else { 464 this.info(`pressing access key (${accesskey}) to activate menu item`); 465 this.EventUtils.synthesizeKey(accesskey, {}, win); 466 } 467 468 this.info("waiting for command event"); 469 await promiseCommand; 470 this.info("got the command event"); 471 } 472 473 /** 474 * Opens the result menu of a specific result and clicks a menu item with a 475 * specified command name. 476 * 477 * @param {ChromeWindow} win 478 * The window containing the urlbar. 479 * @param {string|Array} commandOrArray 480 * If the command is in the top-level result menu, set this to the command 481 * name. If it's in a submenu, set this to an array where each element i is 482 * a selector that can be used to click the i'th menu item that opens a 483 * submenu, and the last element is the command name. 484 * @param {object} options 485 * The options object. 486 * @param {number} [options.resultIndex] 487 * The index of the result. Defaults to the current selected index. 488 * @param {boolean} [options.openByMouse] 489 * Whether to open the menu by mouse or keyboard. 490 */ 491 async openResultMenuAndClickItem( 492 win, 493 commandOrArray, 494 { 495 resultIndex = this.#urlbar(win).view.selectedRowIndex, 496 openByMouse = false, 497 } = {} 498 ) { 499 let submenuSelectors = Array.isArray(commandOrArray) 500 ? commandOrArray 501 : [commandOrArray]; 502 let command = submenuSelectors.pop(); 503 504 let menuitem = await this.openResultMenuAndGetItem({ 505 resultIndex, 506 openByMouse, 507 command, 508 submenuSelectors, 509 window: win, 510 }); 511 if (!menuitem) { 512 throw new Error("Menu item not found for command: " + command); 513 } 514 515 let promiseCommand = lazy.BrowserTestUtils.waitForEvent( 516 this.#urlbar(win).view.resultMenu, 517 "command" 518 ); 519 520 if (AppConstants.platform == "macosx") { 521 // Synthesized clicks don't work in the native Mac menu. 522 this.info("calling doCommand() to activate menu item"); 523 menuitem.doCommand(); 524 this.#urlbar(win).view.resultMenu.hidePopup(true); 525 } else { 526 this.info("Clicking menu item with command: " + command); 527 this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, win); 528 } 529 530 this.info("Waiting for command event"); 531 await promiseCommand; 532 this.info("Got the command event"); 533 } 534 535 /** 536 * Returns true if the oneOffSearchButtons are visible. 537 * 538 * @param {ChromeWindow} win The window containing the urlbar 539 * @returns {boolean} True if the buttons are visible. 540 */ 541 getOneOffSearchButtonsVisible(win) { 542 let buttons = this.getOneOffSearchButtons(win); 543 return buttons.style.display != "none" && !buttons.container.hidden; 544 } 545 546 /** 547 * Gets an abstracted representation of the result at an index. 548 * 549 * @param {ChromeWindow} win The window containing the urlbar 550 * @param {number} index The index to look for 551 * @returns {Promise<object>} An object with numerous properties describing the result. 552 */ 553 async getDetailsOfResultAt(win, index) { 554 let element = await this.waitForAutocompleteResultAt(win, index); 555 let details = {}; 556 let result = element.result; 557 details.result = result; 558 let { url, postData } = UrlbarUtils.getUrlFromResult(result); 559 details.url = url; 560 details.postData = postData; 561 details.type = result.type; 562 details.source = result.source; 563 details.heuristic = result.heuristic; 564 details.autofill = !!result.autofill; 565 details.image = 566 element.getElementsByClassName("urlbarView-favicon")[0]?.src; 567 details.title = result.getDisplayableValueAndHighlights("title").value; 568 details.tags = "tags" in result.payload ? result.payload.tags : []; 569 details.isSponsored = result.payload.isSponsored; 570 details.userContextId = result.payload.userContextId; 571 let actions = element.getElementsByClassName("urlbarView-action"); 572 let urls = element.getElementsByClassName("urlbarView-url"); 573 let typeIcon = element.querySelector(".urlbarView-type-icon"); 574 await win.document.l10n.translateFragment(element); 575 details.displayed = { 576 title: element.getElementsByClassName("urlbarView-title")[0]?.textContent, 577 action: actions.length ? actions[0].textContent : null, 578 url: urls.length ? urls[0].textContent : null, 579 typeIcon: typeIcon 580 ? win.getComputedStyle(typeIcon)["background-image"] 581 : null, 582 }; 583 details.element = { 584 action: element.getElementsByClassName("urlbarView-action")[0], 585 row: element, 586 separator: element.getElementsByClassName( 587 "urlbarView-title-separator" 588 )[0], 589 title: element.getElementsByClassName("urlbarView-title")[0], 590 url: element.getElementsByClassName("urlbarView-url")[0], 591 }; 592 if (details.type == UrlbarUtils.RESULT_TYPE.SEARCH) { 593 details.searchParams = { 594 engine: result.payload.engine, 595 keyword: result.payload.keyword, 596 query: result.payload.query, 597 suggestion: result.payload.suggestion, 598 inPrivateWindow: result.payload.inPrivateWindow, 599 isPrivateEngine: result.payload.isPrivateEngine, 600 }; 601 } else if (details.type == UrlbarUtils.RESULT_TYPE.KEYWORD) { 602 details.keyword = result.payload.keyword; 603 } else if (details.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) { 604 details.dynamicType = result.payload.dynamicType; 605 } 606 return details; 607 } 608 609 /** 610 * Gets the currently selected element. 611 * 612 * @param {ChromeWindow} win The window containing the urlbar. 613 * @returns {HtmlElement|XulElement} The selected element. 614 */ 615 getSelectedElement(win) { 616 return this.#urlbar(win).view.selectedElement || null; 617 } 618 619 /** 620 * Gets the index of the currently selected element. 621 * 622 * @param {ChromeWindow} win The window containing the urlbar. 623 * @returns {number} The selected index. 624 */ 625 getSelectedElementIndex(win) { 626 return this.#urlbar(win).view.selectedElementIndex; 627 } 628 629 /** 630 * Gets the row at a specific index. 631 * 632 * @param {ChromeWindow} win The window containing the urlbar. 633 * @param {number} index The index to look for. 634 * @returns {HTMLElement|XulElement} The selected row. 635 */ 636 getRowAt(win, index) { 637 return this.getResultsContainer(win).children.item(index); 638 } 639 640 /** 641 * Gets the currently selected row. If the selected element is a descendant of 642 * a row, this will return the ancestor row. 643 * 644 * @param {ChromeWindow} win The window containing the urlbar. 645 * @returns {HTMLElement|XulElement} The selected row. 646 */ 647 getSelectedRow(win) { 648 return this.getRowAt(win, this.getSelectedRowIndex(win)); 649 } 650 651 /** 652 * Gets the index of the currently selected element. 653 * 654 * @param {ChromeWindow} win The window containing the urlbar. 655 * @returns {number} The selected row index. 656 */ 657 getSelectedRowIndex(win) { 658 return this.#urlbar(win).view.selectedRowIndex; 659 } 660 661 /** 662 * Selects the element at the index specified. 663 * 664 * @param {ChromeWindow} win The window containing the urlbar. 665 * @param {number} index The index to select. 666 */ 667 setSelectedRowIndex(win, index) { 668 this.#urlbar(win).view.selectedRowIndex = index; 669 } 670 671 /** 672 * Gets the results container div for the address bar. 673 * 674 * @param {ChromeWindow} win 675 * @returns {HTMLDivElement} 676 */ 677 getResultsContainer(win) { 678 return this.#urlbar(win).view.panel.querySelector(".urlbarView-results"); 679 } 680 681 /** 682 * Gets the number of results. 683 * You must wait for the query to be complete before using this. 684 * 685 * @param {ChromeWindow} win The window containing the urlbar 686 * @returns {number} the number of results. 687 */ 688 getResultCount(win) { 689 return this.getResultsContainer(win).children.length; 690 } 691 692 /** 693 * Ensures at least one search suggestion is present. 694 * 695 * @param {ChromeWindow} win The window containing the urlbar 696 * @returns {Promise<number>} 697 * The index of the first suggestion 698 * @throws {Error} When the index exceeds the number of available results 699 */ 700 promiseSuggestionsPresent(win) { 701 // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement. When 702 // we do that, we'll have to be sure the suggestions we find are relevant 703 // for the current query. For now let's just wait for the search to be 704 // complete. 705 return this.promiseSearchComplete(win).then(context => { 706 // Look for search suggestions. 707 let firstSearchSuggestionIndex = context.results.findIndex( 708 r => r.type == UrlbarUtils.RESULT_TYPE.SEARCH && r.payload.suggestion 709 ); 710 if (firstSearchSuggestionIndex == -1) { 711 throw new Error("Cannot find a search suggestion"); 712 } 713 return firstSearchSuggestionIndex; 714 }); 715 } 716 717 /** 718 * Waits for the given number of connections to an http server. 719 * 720 * @param {object} httpserver an HTTP Server instance 721 * @param {number} count Number of connections to wait for 722 * @returns {Promise} resolved when all the expected connections were started. 723 */ 724 promiseSpeculativeConnections(httpserver, count) { 725 if (!httpserver) { 726 throw new Error("Must provide an http server"); 727 } 728 return lazy.BrowserTestUtils.waitForCondition( 729 () => httpserver.connectionNumber == count, 730 "Waiting for speculative connection setup" 731 ); 732 } 733 734 /** 735 * Waits for the popup to be shown. 736 * 737 * @param {ChromeWindow} win The window containing the urlbar 738 * @param {Function} openFn Function to be used to open the popup. 739 * @returns {Promise} resolved once the popup is closed 740 */ 741 async promisePopupOpen(win, openFn) { 742 if (!openFn) { 743 throw new Error("openFn should be supplied to promisePopupOpen"); 744 } 745 await openFn(); 746 let urlbar = this.#urlbar(win); 747 if (urlbar.view.isOpen) { 748 return; 749 } 750 this.info("Waiting for the urlbar view to open"); 751 await new Promise(resolve => { 752 urlbar.controller.addListener({ 753 onViewOpen() { 754 urlbar.controller.removeListener(this); 755 resolve(); 756 }, 757 }); 758 }); 759 this.info("Urlbar view opened"); 760 } 761 762 /** 763 * Waits for the popup to be hidden. 764 * 765 * @param {ChromeWindow} win The window containing the urlbar 766 * @param {Function} [closeFn] Function to be used to close the popup, if not 767 * supplied it will default to a closing the popup directly. 768 * @returns {Promise} resolved once the popup is closed 769 */ 770 async promisePopupClose(win, closeFn = null) { 771 let urlbar = this.#urlbar(win); 772 let closePromise = new Promise(resolve => { 773 if (!urlbar.view.isOpen) { 774 resolve(); 775 return; 776 } 777 urlbar.controller.addListener({ 778 onViewClose() { 779 urlbar.controller.removeListener(this); 780 resolve(); 781 }, 782 }); 783 }); 784 if (closeFn) { 785 this.info("Awaiting custom close function"); 786 await closeFn(); 787 this.info("Done awaiting custom close function"); 788 } else { 789 this.info("Closing the view directly"); 790 urlbar.view.close(); 791 } 792 this.info("Waiting for the view to close"); 793 await closePromise; 794 this.info("Urlbar view closed"); 795 } 796 797 /** 798 * Open the input field context menu and run a task on it. 799 * 800 * @param {ChromeWindow} win the current window 801 * @param {Function} task a task function to run, gets the contextmenu popup 802 * as argument. 803 */ 804 async withContextMenu(win, task) { 805 let textBox = this.#urlbar(win).querySelector("moz-input-box"); 806 let cxmenu = textBox.menupopup; 807 let openPromise = lazy.BrowserTestUtils.waitForEvent(cxmenu, "popupshown"); 808 this.EventUtils.synthesizeMouseAtCenter( 809 this.#urlbar(win).inputField, 810 { 811 type: "contextmenu", 812 button: 2, 813 }, 814 win 815 ); 816 await openPromise; 817 // On Mac sometimes the menuitems are not ready. 818 await new Promise(win.requestAnimationFrame); 819 try { 820 await task(cxmenu); 821 } finally { 822 // Close the context menu if the task didn't pick anything. 823 if (cxmenu.state == "open" || cxmenu.state == "showing") { 824 let closePromise = lazy.BrowserTestUtils.waitForEvent( 825 cxmenu, 826 "popuphidden" 827 ); 828 cxmenu.hidePopup(); 829 await closePromise; 830 } 831 } 832 } 833 834 /** 835 * @param {ChromeWindow} win The browser window 836 * @returns {boolean} Whether the popup is open 837 */ 838 isPopupOpen(win) { 839 return this.#urlbar(win).view.isOpen; 840 } 841 842 /** 843 * Asserts that the input is in a given search mode, or no search mode. Can 844 * only be used if UrlbarTestUtils has been initialized with init(). 845 * 846 * @param {ChromeWindow} window 847 * The browser window. 848 * @param {object} expectedSearchMode 849 * The expected search mode object. 850 */ 851 async assertSearchMode(window, expectedSearchMode) { 852 this.Assert.equal( 853 !!this.#urlbar(window).searchMode, 854 this.#urlbar(window).hasAttribute("searchmode"), 855 "Urlbar should never be in search mode without the corresponding attribute." 856 ); 857 858 this.Assert.equal( 859 !!this.#urlbar(window).searchMode, 860 !!expectedSearchMode, 861 "searchMode should exist on moz-urlbar" 862 ); 863 864 let results = this.#urlbar(window).querySelector(".urlbarView-results"); 865 await lazy.BrowserTestUtils.waitForCondition( 866 () => 867 results.hasAttribute("actionmode") == 868 (this.#urlbar(window).searchMode?.source == 869 UrlbarUtils.RESULT_SOURCE.ACTIONS) 870 ); 871 this.Assert.ok(true, "Urlbar results have proper actionmode attribute"); 872 873 if (!expectedSearchMode) { 874 // Check the input's placeholder. 875 const prefName = 876 "browser.urlbar.placeholderName" + 877 (lazy.PrivateBrowsingUtils.isWindowPrivate(window) ? ".private" : ""); 878 let engineName = Services.prefs.getStringPref(prefName, ""); 879 let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled"); 880 881 let expectedPlaceholder; 882 if (keywordEnabled && engineName) { 883 expectedPlaceholder = { 884 id: "urlbar-placeholder-with-name", 885 args: { name: engineName }, 886 }; 887 } else if (keywordEnabled && !engineName) { 888 expectedPlaceholder = { id: "urlbar-placeholder" }; 889 } else { 890 expectedPlaceholder = { id: "urlbar-placeholder-keyword-disabled" }; 891 } 892 893 await lazy.BrowserTestUtils.waitForCondition(() => { 894 let l10nAttributes = window.document.l10n.getAttributes( 895 this.#urlbar(window).inputField 896 ); 897 return ( 898 l10nAttributes.id == expectedPlaceholder.id && 899 l10nAttributes.args?.name == expectedPlaceholder.args?.name 900 ); 901 }); 902 this.Assert.ok( 903 true, 904 "Expected placeholder l10n when search mode is inactive" 905 ); 906 return; 907 } 908 909 // Default to full search mode for less verbose tests. 910 expectedSearchMode = { ...expectedSearchMode }; 911 if (!expectedSearchMode.hasOwnProperty("isPreview")) { 912 expectedSearchMode.isPreview = false; 913 } 914 915 let isGeneralPurposeEngine = false; 916 if (expectedSearchMode.engineName) { 917 let engine = Services.search.getEngineByName( 918 expectedSearchMode.engineName 919 ); 920 isGeneralPurposeEngine = engine.isGeneralPurposeEngine; 921 expectedSearchMode.isGeneralPurposeEngine = isGeneralPurposeEngine; 922 } 923 924 // expectedSearchMode may come from UrlbarUtils.LOCAL_SEARCH_MODES. The 925 // objects in that array include useful metadata like icon URIs and pref 926 // names that are not usually included in actual search mode objects. For 927 // convenience, ignore those properties if they aren't also present in the 928 // urlbar's actual search mode object. 929 let ignoreProperties = [ 930 "icon", 931 "pref", 932 "restrict", 933 "telemetryLabel", 934 "uiLabel", 935 ]; 936 for (let prop of ignoreProperties) { 937 if ( 938 prop in expectedSearchMode && 939 !(prop in this.#urlbar(window).searchMode) 940 ) { 941 this.info( 942 `Ignoring unimportant property '${prop}' in expected search mode` 943 ); 944 delete expectedSearchMode[prop]; 945 } 946 } 947 948 this.Assert.deepEqual( 949 this.#urlbar(window).searchMode, 950 expectedSearchMode, 951 "Expected searchMode" 952 ); 953 954 // Only the addressbar still has the legacy search mode indicator. 955 if (this.#urlbar(window).sapName == "urlbar") { 956 // Check the textContent and l10n attributes of the indicator and label. 957 let expectedTextContent = ""; 958 let expectedL10n = { id: null, args: null }; 959 if (expectedSearchMode.engineName) { 960 expectedTextContent = expectedSearchMode.engineName; 961 } else if (expectedSearchMode.source) { 962 let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source); 963 this.Assert.ok(name, "Expected result source should have a name"); 964 expectedL10n = { id: `urlbar-search-mode-${name}`, args: null }; 965 } else { 966 this.Assert.ok(false, "Unexpected searchMode"); 967 } 968 969 if (expectedTextContent) { 970 this.Assert.equal( 971 this.#urlbar(window)._searchModeIndicatorTitle.textContent, 972 expectedTextContent, 973 "Expected textContent" 974 ); 975 } 976 this.Assert.deepEqual( 977 window.document.l10n.getAttributes( 978 this.#urlbar(window)._searchModeIndicatorTitle 979 ), 980 expectedL10n, 981 "Expected l10n" 982 ); 983 } 984 985 // Check the input's placeholder. 986 let expectedPlaceholderL10n; 987 if (this.#urlbar(window).sapName == "searchbar") { 988 // Placeholder stays constant in searchbar. 989 expectedPlaceholderL10n = { 990 id: "searchbar-input", 991 args: null, 992 }; 993 } else if (expectedSearchMode.engineName) { 994 expectedPlaceholderL10n = { 995 id: isGeneralPurposeEngine 996 ? "urlbar-placeholder-search-mode-web-2" 997 : "urlbar-placeholder-search-mode-other-engine", 998 args: { name: expectedSearchMode.engineName }, 999 }; 1000 } else if (expectedSearchMode.source) { 1001 let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source); 1002 expectedPlaceholderL10n = { 1003 id: `urlbar-placeholder-search-mode-other-${name}`, 1004 args: null, 1005 }; 1006 } 1007 this.Assert.deepEqual( 1008 window.document.l10n.getAttributes(this.#urlbar(window).inputField), 1009 expectedPlaceholderL10n, 1010 "Expected placeholder l10n when search mode is active" 1011 ); 1012 1013 // If this is an engine search mode, check that all results are either 1014 // search results with the same engine or have the same host as the engine. 1015 // Search mode preview can show other results since it is not supposed to 1016 // start a query. 1017 if ( 1018 expectedSearchMode.engineName && 1019 !expectedSearchMode.isPreview && 1020 this.isPopupOpen(window) 1021 ) { 1022 let resultCount = this.getResultCount(window); 1023 for (let i = 0; i < resultCount; i++) { 1024 let result = await this.getDetailsOfResultAt(window, i); 1025 if (result.source == UrlbarUtils.RESULT_SOURCE.SEARCH) { 1026 this.Assert.equal( 1027 expectedSearchMode.engineName, 1028 result.searchParams.engine, 1029 "Search mode result matches engine name." 1030 ); 1031 } else { 1032 let engine = Services.search.getEngineByName( 1033 expectedSearchMode.engineName 1034 ); 1035 let engineRootDomain = 1036 lazy.UrlbarSearchUtils.getRootDomainFromEngine(engine); 1037 let resultUrl = new URL(result.url); 1038 this.Assert.ok( 1039 resultUrl.hostname.includes(engineRootDomain), 1040 "Search mode result matches engine host." 1041 ); 1042 } 1043 } 1044 } 1045 } 1046 1047 /** 1048 * Enters search mode by clicking a one-off. The view must already be open 1049 * before you call this. Can only be used if UrlbarTestUtils has been 1050 * initialized with init(). 1051 * 1052 * @param {ChromeWindow} window 1053 * The window to operate on. 1054 * @param {object} searchMode 1055 * If given, the one-off matching this search mode will be clicked; it 1056 * should be a full search mode object as described in 1057 * UrlbarInput.setSearchMode. If not given, the first one-off is clicked. 1058 */ 1059 async enterSearchMode(window, searchMode = null) { 1060 this.info(`Enter Search Mode ${JSON.stringify(searchMode)}`); 1061 1062 // Ensure any pending query is complete. 1063 await this.promiseSearchComplete(window); 1064 1065 // Ensure the the one-offs are finished rebuilding and visible. 1066 let oneOffs = this.getOneOffSearchButtons(window); 1067 await lazy.TestUtils.waitForCondition( 1068 () => !oneOffs._rebuilding, 1069 "Waiting for one-offs to finish rebuilding" 1070 ); 1071 this.Assert.equal( 1072 UrlbarTestUtils.getOneOffSearchButtonsVisible(window), 1073 true, 1074 "One-offs are visible" 1075 ); 1076 1077 let buttons = oneOffs.getSelectableButtons(true); 1078 if (!searchMode) { 1079 searchMode = { engineName: buttons[0].engine.name }; 1080 let engine = Services.search.getEngineByName(searchMode.engineName); 1081 if (engine.isGeneralPurposeEngine) { 1082 searchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH; 1083 } 1084 } 1085 1086 if (!searchMode.entry) { 1087 searchMode.entry = "oneoff"; 1088 } 1089 1090 let oneOff = buttons.find(o => 1091 searchMode.engineName 1092 ? o.engine.name == searchMode.engineName 1093 : o.source == searchMode.source 1094 ); 1095 this.Assert.ok(oneOff, "Found one-off button for search mode"); 1096 this.EventUtils.synthesizeMouseAtCenter(oneOff, {}, window); 1097 await this.promiseSearchComplete(window); 1098 this.Assert.ok(this.isPopupOpen(window), "Urlbar view is still open."); 1099 await this.assertSearchMode(window, searchMode); 1100 } 1101 1102 /** 1103 * Removes the scheme from an url according to user prefs. 1104 * 1105 * @param {string} url 1106 * The url that is supposed to be trimmed. 1107 * @param {object} [options] 1108 * Options for the trimming. 1109 * @param {boolean} [options.removeSingleTrailingSlash] 1110 * Remove trailing slash, when trimming enabled. 1111 * @returns {string} 1112 * The sanitized URL. 1113 */ 1114 trimURL(url, { removeSingleTrailingSlash = true } = {}) { 1115 if (!lazy.UrlbarPrefs.get("trimURLs")) { 1116 return url; 1117 } 1118 1119 let sanitizedURL = url; 1120 if (removeSingleTrailingSlash) { 1121 sanitizedURL = 1122 lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(sanitizedURL); 1123 } 1124 1125 // Also remove emphasis markers if present. 1126 if (lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps")) { 1127 sanitizedURL = sanitizedURL.replace(/^<?https:\/\/>?/, ""); 1128 } else { 1129 sanitizedURL = sanitizedURL.replace(/^<?http:\/\/>?/, ""); 1130 } 1131 1132 return sanitizedURL; 1133 } 1134 1135 /** 1136 * Returns the trimmed protocol with slashes. 1137 * 1138 * @returns {string} The trimmed protocol including slashes. Returns an empty 1139 * string, when the protocol trimming is disabled. 1140 */ 1141 getTrimmedProtocolWithSlashes() { 1142 if (Services.prefs.getBoolPref("browser.urlbar.trimURLs")) { 1143 return lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps") 1144 ? "https://" 1145 : "http://"; // eslint-disable-this-line @microsoft/sdl/no-insecure-url 1146 } 1147 return ""; 1148 } 1149 1150 /** 1151 * Exits search mode. If neither `backspace` nor `clickClose` is given, we'll 1152 * default to backspacing. Can only be used if UrlbarTestUtils has been 1153 * initialized with init(). 1154 * 1155 * @param {ChromeWindow} window 1156 * The window to operate on. 1157 * @param {object} options 1158 * Options object 1159 * @param {boolean} [options.backspace] 1160 * Exits search mode by backspacing at the beginning of the search string. 1161 * @param {boolean} [options.clickClose] 1162 * Exits search mode by clicking the close button on the search mode 1163 * indicator. 1164 * @param {boolean} [options.waitForSearch] 1165 * Whether the test should wait for a search after exiting search mode. 1166 * Defaults to true. 1167 */ 1168 async exitSearchMode( 1169 window, 1170 { backspace, clickClose, waitForSearch = true } = {} 1171 ) { 1172 let urlbar = this.#urlbar(window); 1173 // If the Urlbar is not extended, ignore the clickClose parameter. The close 1174 // button is not clickable in this state. This state might be encountered on 1175 // Linux, where prefers-reduced-motion is enabled in automation. 1176 if (!urlbar.hasAttribute("breakout-extend") && clickClose) { 1177 if (waitForSearch) { 1178 let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); 1179 urlbar.searchMode = null; 1180 await searchPromise; 1181 } else { 1182 urlbar.searchMode = null; 1183 } 1184 return; 1185 } 1186 1187 if (!backspace && !clickClose) { 1188 backspace = true; 1189 } 1190 1191 if (backspace) { 1192 let urlbarValue = urlbar.value; 1193 urlbar.selectionStart = urlbar.selectionEnd = 0; 1194 if (waitForSearch) { 1195 let searchPromise = this.promiseSearchComplete(window); 1196 this.EventUtils.synthesizeKey("KEY_Backspace", {}, window); 1197 await searchPromise; 1198 } else { 1199 this.EventUtils.synthesizeKey("KEY_Backspace", {}, window); 1200 } 1201 this.Assert.equal( 1202 urlbar.value, 1203 urlbarValue, 1204 "Urlbar value hasn't changed." 1205 ); 1206 await this.assertSearchMode(window, null); 1207 } else if (clickClose) { 1208 // We need to hover the indicator to make the close button clickable in the 1209 // test. 1210 let indicator = urlbar.querySelector("#urlbar-search-mode-indicator"); 1211 this.EventUtils.synthesizeMouseAtCenter( 1212 indicator, 1213 { type: "mouseover" }, 1214 window 1215 ); 1216 let closeButton = urlbar.querySelector( 1217 "#urlbar-search-mode-indicator-close" 1218 ); 1219 if (waitForSearch) { 1220 let searchPromise = this.promiseSearchComplete(window); 1221 this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window); 1222 await searchPromise; 1223 } else { 1224 this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window); 1225 } 1226 await this.assertSearchMode(window, null); 1227 } 1228 } 1229 1230 /** 1231 * Returns the userContextId (container id) for the last search. 1232 * 1233 * @param {ChromeWindow} win The browser window 1234 * @returns {Promise<number>} 1235 * resolved when fetching is complete. Its value is a userContextId 1236 */ 1237 async promiseUserContextId(win) { 1238 const defaultId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; 1239 let context = await this.#urlbar(win).lastQueryContextPromise; 1240 return context.userContextId || defaultId; 1241 } 1242 1243 /** 1244 * Dispatches an input event to the input field. 1245 * 1246 * @param {ChromeWindow} win The browser window 1247 */ 1248 fireInputEvent(win) { 1249 // Set event.data to the last character in the input, for a couple of 1250 // reasons: It simulates the user typing, and it's necessary for autofill. 1251 let event = new InputEvent("input", { 1252 data: this.#urlbar(win).value[this.#urlbar(win).value.length - 1] || null, 1253 }); 1254 this.#urlbar(win).inputField.dispatchEvent(event); 1255 } 1256 1257 /** 1258 * Returns a new mock controller. This is useful for xpcshell tests. 1259 * 1260 * @param {object} options Additional options to pass to the UrlbarController 1261 * constructor. 1262 * @returns {UrlbarController} A new controller. 1263 */ 1264 newMockController(options = {}) { 1265 return new lazy.UrlbarController( 1266 Object.assign( 1267 { 1268 input: { 1269 isPrivate: false, 1270 onFirstResult() { 1271 return false; 1272 }, 1273 getSearchSource() { 1274 return "dummy-search-source"; 1275 }, 1276 window: { 1277 location: { 1278 href: AppConstants.BROWSER_CHROME_URL, 1279 }, 1280 }, 1281 }, 1282 }, 1283 options 1284 ) 1285 ); 1286 } 1287 1288 /** 1289 * Initializes some external components used by the urlbar. This is necessary 1290 * in xpcshell tests but not in browser tests. 1291 */ 1292 async initXPCShellDependencies() { 1293 // The FormHistoryStartup component must be initialized since urlbar uses 1294 // form history. 1295 Cc["@mozilla.org/satchel/form-history-startup;1"] 1296 .getService(Ci.nsIObserver) 1297 .observe(null, "profile-after-change", null); 1298 } 1299 1300 /** 1301 * Enrolls in a mock Nimbus feature. 1302 * 1303 * @param {object} value 1304 * Define any desired Nimbus variables in this object. 1305 * @param {string} [feature] 1306 * The feature to init. 1307 * @param {string} [enrollmentType] 1308 * The enrollment type, either "rollout" (default) or "config". 1309 * @returns {Promise<() => Promise<void>>} 1310 * A cleanup function that will unenroll the feature, returns a promise. 1311 */ 1312 async initNimbusFeature( 1313 value = {}, 1314 feature = "urlbar", 1315 enrollmentType = "rollout" 1316 ) { 1317 this.info("initNimbusFeature awaiting ExperimentAPI.init"); 1318 const initializedExperimentAPI = await lazy.ExperimentAPI.init(); 1319 1320 this.info("initNimbusFeature awaiting ExperimentAPI.ready"); 1321 await lazy.ExperimentAPI.ready(); 1322 1323 this.info( 1324 `initNimbusFeature awaiting NimbusTestUtils.enrollWithFeatureConfig` 1325 ); 1326 const doExperimentCleanup = 1327 await lazy.NimbusTestUtils.enrollWithFeatureConfig( 1328 { 1329 featureId: lazy.NimbusFeatures[feature].featureId, 1330 value, 1331 }, 1332 { 1333 isRollout: enrollmentType === "rollout", 1334 } 1335 ); 1336 1337 this.info("initNimbusFeature done"); 1338 1339 const cleanup = async () => { 1340 await doExperimentCleanup(); 1341 if (initializedExperimentAPI) { 1342 // Only reset if we're in an xpcshell-test and actually initialized the 1343 // ExperimentAPI. 1344 lazy.ExperimentAPI._resetForTests(); 1345 } 1346 }; 1347 1348 this.registerCleanupFunction?.(async () => { 1349 // If `cleanup()` has already been called (i.e., by the caller), it will 1350 // throw an error here. 1351 try { 1352 await cleanup(); 1353 } catch (error) {} 1354 }); 1355 1356 return cleanup; 1357 } 1358 1359 /** 1360 * Simulate that user clicks moz-urlbar and inputs text into it. 1361 * 1362 * @param {ChromeWindow} win 1363 * The browser window containing target moz-urlbar. 1364 * @param {string} text 1365 * The text to be input. 1366 */ 1367 async inputIntoURLBar(win, text) { 1368 if (this.#urlbar(win).focused) { 1369 this.#urlbar(win).select(); 1370 } else { 1371 this.EventUtils.synthesizeMouseAtCenter( 1372 this.#urlbar(win).inputField, 1373 {}, 1374 win 1375 ); 1376 await lazy.TestUtils.waitForCondition(() => this.#urlbar(win).focused); 1377 } 1378 if (text.length > 1) { 1379 // Set most of the string directly instead of going through sendString, 1380 // so that we don't make life unnecessarily hard for consumers by 1381 // possibly starting multiple searches. 1382 this.#urlbar(win)._setValue(text.substr(0, text.length - 1)); 1383 } 1384 this.EventUtils.sendString(text.substr(-1, 1), win); 1385 } 1386 1387 /** 1388 * Checks the urlbar value fomatting for a given URL. 1389 * 1390 * @param {ChromeWindow} win 1391 * The input in this window will be tested. 1392 * @param {string} urlFormatString 1393 * The URL to test. The parts the are expected to be de-emphasized should be 1394 * wrapped in "<" and ">" chars. 1395 * @param {object} [options] 1396 * Options object. 1397 * @param {string} [options.clobberedURLString] 1398 * Normally the URL is de-emphasized in-place, thus it's enough to pass 1399 * urlString. In some cases however the formatter may decide to replace 1400 * the URL with a fixed one, because it can't properly guess a host. In 1401 * that case clobberedURLString is the expected de-emphasized value. The 1402 * parts the are expected to be de-emphasized should be wrapped in "<" 1403 * and ">" chars. 1404 * @param {string} [options.additionalMsg] 1405 * Additional message to use for Assert.equal. 1406 * @param {number} [options.selectionType] 1407 * The selectionType for which the input should be checked. 1408 */ 1409 async checkFormatting( 1410 win, 1411 urlFormatString, 1412 { 1413 clobberedURLString = null, 1414 additionalMsg = null, 1415 selectionType = Ci.nsISelectionController.SELECTION_URLSECONDARY, 1416 } = {} 1417 ) { 1418 await new Promise(resolve => win.requestAnimationFrame(resolve)); 1419 let selectionController = this.#urlbar(win).editor.selectionController; 1420 let selection = selectionController.getSelection(selectionType); 1421 let value = this.#urlbar(win).editor.rootElement.textContent; 1422 let result = ""; 1423 for (let i = 0; i < selection.rangeCount; i++) { 1424 let range = selection.getRangeAt(i).toString(); 1425 let pos = value.indexOf(range); 1426 result += value.substring(0, pos) + "<" + range + ">"; 1427 value = value.substring(pos + range.length); 1428 } 1429 result += value; 1430 this.Assert.equal( 1431 result, 1432 clobberedURLString || urlFormatString, 1433 "Correct part of the URL is de-emphasized" + 1434 (additionalMsg ? ` (${additionalMsg})` : "") 1435 ); 1436 } 1437 1438 searchModeSwitcherPopup(win) { 1439 return this.#urlbar(win).querySelector(".searchmode-switcher-popup"); 1440 } 1441 1442 async openSearchModeSwitcher(win) { 1443 let popup = this.searchModeSwitcherPopup(win); 1444 let button = this.#urlbar(win).querySelector(".searchmode-switcher"); 1445 this.Assert.ok(lazy.BrowserTestUtils.isVisible(button)); 1446 await this.EventUtils.promiseElementReadyForUserInput(button, win); 1447 1448 let promiseMenuOpen = lazy.BrowserTestUtils.waitForPopupEvent( 1449 popup, 1450 "shown" 1451 ); 1452 let rebuildPromise = lazy.BrowserTestUtils.waitForEvent(popup, "rebuild"); 1453 // Ensure the pop-up opens. 1454 button.open = true; 1455 await Promise.all([promiseMenuOpen, rebuildPromise]); 1456 1457 return popup; 1458 } 1459 1460 searchModeSwitcherPopupClosed(win) { 1461 return lazy.BrowserTestUtils.waitForPopupEvent( 1462 this.searchModeSwitcherPopup(win), 1463 "hidden" 1464 ); 1465 } 1466 1467 /** 1468 * Gets the icon url of the search mode switcher icon. 1469 * 1470 * @param {ChromeWindow} win 1471 * @returns {?string} 1472 */ 1473 getSearchModeSwitcherIcon(win) { 1474 let searchModeSwitcherButton = this.#urlbar(win).querySelector( 1475 ".searchmode-switcher-icon" 1476 ); 1477 1478 // match and capture the URL inside `url("...")` 1479 let re = /url\("([^"]+)"\)/; 1480 let { listStyleImage } = win.getComputedStyle(searchModeSwitcherButton); 1481 return listStyleImage.match(re)?.[1] ?? null; 1482 } 1483 1484 async openTrustPanel(win) { 1485 let btn = win.document.getElementById("trust-icon"); 1486 let popupShown = lazy.BrowserTestUtils.waitForEvent( 1487 win.document, 1488 "popupshown" 1489 ); 1490 this.EventUtils.synthesizeMouseAtCenter(btn, {}, win); 1491 await popupShown; 1492 } 1493 1494 async openTrustPanelSubview(win, viewId) { 1495 let view = win.document.getElementById(viewId); 1496 let shown = lazy.BrowserTestUtils.waitForEvent(view, "ViewShown"); 1497 this.EventUtils.synthesizeMouseAtCenter( 1498 win.document.getElementById("trustpanel-popup-connection"), 1499 {}, 1500 win 1501 ); 1502 await shown; 1503 } 1504 1505 async closeTrustPanel(win) { 1506 let popupHidden = lazy.BrowserTestUtils.waitForEvent( 1507 win.document, 1508 "popuphidden" 1509 ); 1510 this.EventUtils.synthesizeKey("VK_ESCAPE", {}, win); 1511 await popupHidden; 1512 } 1513 1514 async selectMenuItem(menupopup, targetSelector) { 1515 let target = menupopup.querySelector(targetSelector); 1516 let selected; 1517 for (let i = 0; i < menupopup.children.length; i++) { 1518 this.EventUtils.synthesizeKey("KEY_ArrowDown", {}, menupopup.ownerGlobal); 1519 await lazy.BrowserTestUtils.waitForCondition(() => { 1520 let current = menupopup.querySelector("[_moz-menuactive]"); 1521 if (selected != current) { 1522 selected = current; 1523 return true; 1524 } 1525 return false; 1526 }); 1527 if (selected == target) { 1528 break; 1529 } 1530 } 1531 } 1532 } 1533 1534 UrlbarInputTestUtils.prototype.formHistory = { 1535 /** 1536 * Adds values to the urlbar's form history. 1537 * 1538 * @param {Array} values 1539 * The form history entries to remove. 1540 * @returns {Promise} resolved once the operation is complete. 1541 */ 1542 add(values = []) { 1543 return lazy.FormHistoryTestUtils.add( 1544 lazy.DEFAULT_FORM_HISTORY_PARAM, 1545 values 1546 ); 1547 }, 1548 1549 /** 1550 * Removes values from the urlbar's form history. If you want to remove all 1551 * history, use clearFormHistory. 1552 * 1553 * @param {Array} values 1554 * The form history entries to remove. 1555 * @returns {Promise} resolved once the operation is complete. 1556 */ 1557 remove(values = []) { 1558 return lazy.FormHistoryTestUtils.remove( 1559 lazy.DEFAULT_FORM_HISTORY_PARAM, 1560 values 1561 ); 1562 }, 1563 1564 /** 1565 * Removes all values from the urlbar's form history. If you want to remove 1566 * individual values, use removeFormHistory. 1567 * 1568 * @returns {Promise} resolved once the operation is complete. 1569 */ 1570 clear() { 1571 return lazy.FormHistoryTestUtils.clear(lazy.DEFAULT_FORM_HISTORY_PARAM); 1572 }, 1573 1574 /** 1575 * Searches the urlbar's form history. 1576 * 1577 * @param {object} criteria 1578 * Criteria to narrow the search. See FormHistory.search. 1579 * @returns {Promise} 1580 * A promise resolved with an array of found form history entries. 1581 */ 1582 search(criteria = {}) { 1583 return lazy.FormHistoryTestUtils.search( 1584 lazy.DEFAULT_FORM_HISTORY_PARAM, 1585 criteria 1586 ); 1587 }, 1588 1589 /** 1590 * Returns a promise that's resolved on the next form history change. 1591 * 1592 * @param {string} change 1593 * Null to listen for any change, or one of: add, remove, update 1594 * @returns {Promise} 1595 * Resolved on the next specified form history change. 1596 */ 1597 promiseChanged(change = null) { 1598 return lazy.TestUtils.topicObserved( 1599 "satchel-storage-changed", 1600 (subject, data) => !change || data == "formhistory-" + change 1601 ); 1602 }, 1603 }; 1604 1605 /** 1606 * A test provider. If you need a test provider whose behavior is different 1607 * from this, then consider modifying the implementation below if you think the 1608 * new behavior would be useful for other tests. Otherwise, you can create a 1609 * new TestProvider instance and then override its methods. 1610 */ 1611 class TestProvider extends UrlbarProvider { 1612 /** 1613 * Constructor. 1614 * 1615 * @param {object} options 1616 * Constructor options 1617 * @param {Array} [options.results] 1618 * An array of UrlbarResult objects that will be the provider's results. 1619 * @param {string} [options.name] 1620 * The provider's name. Provider names should be unique. 1621 * @param {Values<typeof UrlbarUtils.PROVIDER_TYPE>} [options.type] 1622 * The provider's type. 1623 * @param {number} [options.priority] 1624 * The provider's priority. Built-in providers have a priority of zero. 1625 * @param {number} [options.addTimeout] 1626 * If non-zero, each result will be added on this timeout. If zero, all 1627 * results will be added immediately and synchronously. 1628 * If there's no results, the query will be completed after this timeout. 1629 * @param {Function} [options.getViewTemplate] 1630 * If given, override the UrlbarProvider.getViewTemplate(). 1631 * @param {Function} [options.getViewUpdate] 1632 * If given, override the UrlbarProvider.getViewUpdate(). 1633 * @param {Function} [options.onCancel] 1634 * If given, a function that will be called when the provider's cancelQuery 1635 * method is called. 1636 * @param {Function} [options.onSelection] 1637 * If given, a function that will be called when 1638 * {@link UrlbarView.#selectElement} method is called. 1639 * @param {Function} [options.onEngagement] 1640 * If given, a function that will be called when engagement. 1641 * @param {Function} [options.onAbandonment] 1642 * If given, a function that will be called when abandonment. 1643 * @param {Function} [options.onImpression] 1644 * If given, a function that will be called when an engagement or 1645 * abandonment has occured. 1646 * @param {Function} [options.onSearchSessionEnd] 1647 * If given, a function that will be called when a search session 1648 * concludes. 1649 * @param {Function} [options.delayResultsPromise] 1650 * If given, we'll await on this before returning results. 1651 */ 1652 constructor({ 1653 results = [], 1654 name = "TestProvider" + Services.uuid.generateUUID(), 1655 type = UrlbarUtils.PROVIDER_TYPE.PROFILE, 1656 priority = 0, 1657 addTimeout = 0, 1658 getViewTemplate = null, 1659 getViewUpdate = null, 1660 onCancel = null, 1661 onSelection = null, 1662 onEngagement = null, 1663 onAbandonment = null, 1664 onImpression = null, 1665 onSearchSessionEnd = null, 1666 delayResultsPromise = null, 1667 } = {}) { 1668 if (delayResultsPromise && addTimeout) { 1669 throw new Error( 1670 "Can't provide both `addTimeout` and `delayResultsPromise`" 1671 ); 1672 } 1673 super(); 1674 this.results = results; 1675 this.priority = priority; 1676 this.addTimeout = addTimeout; 1677 this.delayResultsPromise = delayResultsPromise; 1678 this._name = name; 1679 this._type = type; 1680 this._onCancel = onCancel; 1681 this._onSelection = onSelection; 1682 1683 // As this has been a common source of mistakes, auto-upgrade the provider 1684 // type to heuristic if any result is heuristic. 1685 if (!type && this.results?.some(r => r.heuristic)) { 1686 this._type = UrlbarUtils.PROVIDER_TYPE.HEURISTIC; 1687 } 1688 1689 if (getViewTemplate) { 1690 this.getViewTemplate = getViewTemplate.bind(this); 1691 } 1692 1693 if (getViewUpdate) { 1694 this.getViewUpdate = getViewUpdate.bind(this); 1695 } 1696 1697 if (onEngagement) { 1698 this.onEngagement = onEngagement.bind(this); 1699 } 1700 1701 if (onAbandonment) { 1702 this.onAbandonment = onAbandonment.bind(this); 1703 } 1704 1705 if (onImpression) { 1706 this.onImpression = onAbandonment.bind(this); 1707 } 1708 1709 if (onSearchSessionEnd) { 1710 this.onSearchSessionEnd = onSearchSessionEnd.bind(this); 1711 } 1712 } 1713 1714 get name() { 1715 return this._name; 1716 } 1717 1718 get type() { 1719 return this._type; 1720 } 1721 1722 getPriority(_context) { 1723 return this.priority; 1724 } 1725 1726 async isActive(_context) { 1727 return true; 1728 } 1729 1730 async startQuery(context, addCallback) { 1731 if (!this.results.length && this.addTimeout) { 1732 await new Promise(resolve => lazy.setTimeout(resolve, this.addTimeout)); 1733 } 1734 if (this.delayResultsPromise) { 1735 await this.delayResultsPromise; 1736 } 1737 for (let result of this.results) { 1738 if (!this.addTimeout) { 1739 addCallback(this, result); 1740 } else { 1741 await new Promise(resolve => { 1742 lazy.setTimeout(() => { 1743 addCallback(this, result); 1744 resolve(); 1745 }, this.addTimeout); 1746 }); 1747 } 1748 } 1749 } 1750 1751 cancelQuery(_context) { 1752 this._onCancel?.(); 1753 } 1754 1755 onSelection(result, element) { 1756 this._onSelection?.(result, element); 1757 } 1758 } 1759 1760 UrlbarInputTestUtils.prototype.TestProvider = TestProvider; 1761 1762 export var UrlbarTestUtils = new UrlbarInputTestUtils(window => window.gURLBar); 1763 export var SearchbarTestUtils = new UrlbarInputTestUtils(window => 1764 window.document.getElementById("searchbar-new") 1765 );